49 lines
2.7 KiB
Python

"""Broker-only refresh of an owned draft PR's title and body."""
from __future__ import annotations
import base64
import json
import urllib.request
from typing import Any
import deadline_http
from gitea_api import CANONICAL_BASE_URL
from gitea_api_policy import PolicyError, _draft_title, _validate_body, _validate_pr_number
def request_fields(value: dict[str, Any], token: str) -> tuple[str, int, str, str]:
if set(value) != {"grant", "pr_number", "title", "body"} or not isinstance(value["grant"], str):
raise PolicyError("draft update fields are invalid")
return value["grant"], _validate_pr_number(value["pr_number"]), _draft_title(value["title"], forbidden=(token,)), _validate_body(value["body"], forbidden=(token,))
def matches_pull(value: Any, claims: dict[str, Any], number: int) -> None:
if not isinstance(value, dict) or _validate_pr_number(value.get("number")) != number:
raise PolicyError("draft pull request is invalid")
head, base = value.get("head"), value.get("base")
if not isinstance(head, dict) or not isinstance(base, dict):
raise PolicyError("draft pull request is invalid")
repo, base_repo = head.get("repo"), base.get("repo")
name = repo.get("full_name") if isinstance(repo, dict) else ""
base_name = base_repo.get("full_name") if isinstance(base_repo, dict) else ""
if value.get("state") != "open" or head.get("ref") != claims["ref"] or head.get("sha") != claims["new_head"] or base.get("ref") != claims["base"] or name != f"titan/{claims['repo']}" or base_name != f"titan/{claims['repo']}":
raise PolicyError("draft pull request does not match its task grant")
def update(token: str, repo: str, number: int, title: str, body: str) -> bytes:
"""PATCH only safe draft prose; caller already authenticated ownership."""
payload = json.dumps({"title": title, "body": body}, separators=(",", ":")).encode()
auth = base64.b64encode(f"hermes-automation:{token}".encode()).decode()
request = urllib.request.Request(
f"{CANONICAL_BASE_URL}/api/v1/repos/titan/{repo}/pulls/{number}", payload,
method="PATCH", headers={"Authorization": f"Basic {auth}", "Content-Type": "application/json", "Accept": "application/json"},
)
with deadline_http.open_bounded(request, maximum=2 * 1024 * 1024, timeout=30) as response:
if getattr(response, "status", None) != 200 or response.headers.get_content_type() != "application/json":
raise PolicyError("draft update upstream response is invalid")
result = response.read(2 * 1024 * 1024 + 1)
if len(result) > 2 * 1024 * 1024 or token.encode() in result:
raise PolicyError("draft update upstream response is invalid")
json.loads(result)
return result