66 lines
3.4 KiB
Python
66 lines
3.4 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
|
|
|
|
UPDATE_SUCCESS_STATUSES = {200, 201}
|
|
|
|
|
|
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) not in UPDATE_SUCCESS_STATUSES 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")
|
|
try:
|
|
payload = json.loads(result)
|
|
except json.JSONDecodeError as error:
|
|
raise PolicyError("draft update upstream response is invalid") from error
|
|
try:
|
|
response_number = _validate_pr_number(
|
|
payload.get("number") if isinstance(payload, dict) else None
|
|
)
|
|
except PolicyError as error:
|
|
raise PolicyError("draft update upstream response is invalid") from error
|
|
if (
|
|
not isinstance(payload, dict)
|
|
or response_number != number
|
|
or payload.get("html_url") != f"{CANONICAL_BASE_URL}/titan/{repo}/pulls/{number}"
|
|
):
|
|
raise PolicyError("draft update upstream response is invalid")
|
|
return result
|