Co-authored-by: orchestrator-agent <orchestrator@cubecraftlabs.com> Co-committed-by: orchestrator-agent <orchestrator@cubecraftlabs.com>
132 lines
5.5 KiB
Python
Executable File
132 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Inspect or merge a reviewed NestQuest feature PR into dev only."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
|
|
API = "https://code.cubecraftlabs.com/api/v1/repos/Maniacs_Mansion/NestQuest"
|
|
CREDENTIALS = Path("/home/overseer/.llms/credentials.env")
|
|
FULL_SHA = re.compile(r"^[0-9a-f]{40}$")
|
|
|
|
|
|
def token_from_file() -> str:
|
|
for line in CREDENTIALS.read_text().splitlines():
|
|
if line.startswith("GITEA_APPROVER_AGENT_TOKEN="):
|
|
return line.split("=", 1)[1].strip().strip("\"'")
|
|
raise RuntimeError("GITEA_APPROVER_AGENT_TOKEN is unavailable")
|
|
|
|
|
|
def request(path: str, token: str, payload: dict | None = None) -> dict | list:
|
|
body = None if payload is None else json.dumps(payload).encode()
|
|
headers = {"Authorization": "token " + token, "Accept": "application/json"}
|
|
if body is not None:
|
|
headers["Content-Type"] = "application/json"
|
|
call = urllib.request.Request(API + path, data=body, headers=headers)
|
|
try:
|
|
with urllib.request.urlopen(call, timeout=20) as response:
|
|
raw = response.read()
|
|
return json.loads(raw) if raw else {}
|
|
except urllib.error.HTTPError as error:
|
|
raise RuntimeError(f"Gitea returned HTTP {error.code}") from None
|
|
|
|
|
|
def inspect(pr_number: int, token: str) -> dict:
|
|
pr = request(f"/pulls/{pr_number}", token)
|
|
head = (pr.get("head") or {}).get("sha")
|
|
base = (pr.get("base") or {}).get("ref")
|
|
if not isinstance(head, str) or not FULL_SHA.fullmatch(head):
|
|
raise RuntimeError("Gitea returned an invalid PR head")
|
|
status = request(f"/commits/{head}/status", token)
|
|
return {
|
|
"number": pr.get("number"),
|
|
"base": base,
|
|
"head": head,
|
|
"state": pr.get("state"),
|
|
"merged": pr.get("merged"),
|
|
"mergeable": pr.get("mergeable"),
|
|
"check_count": status.get("total_count") or 0,
|
|
"check_state": status.get("state"),
|
|
}
|
|
|
|
|
|
def validate_review(path: Path, head: str) -> None:
|
|
if not path.is_file() or not path.resolve().is_relative_to(Path("/tmp/opencode").resolve()):
|
|
raise RuntimeError("review evidence must be a file under /tmp/opencode")
|
|
text = path.read_text(errors="replace")
|
|
reviewed = re.findall(r"(?m)^REVIEWED_HEAD:\s*([0-9a-f]{40})\s*$", text)
|
|
verdicts = re.findall(r"(?m)^REVIEW_VERDICT:\s*(APPROVED|CHANGES_REQUIRED)\s*$", text)
|
|
if len(reviewed) != 1 or len(verdicts) != 1 or reviewed[0] != head or verdicts[0] != "APPROVED":
|
|
raise RuntimeError("independent review does not approve this exact head")
|
|
|
|
|
|
def has_platform_review(pr_number: int, head: str, token: str) -> bool:
|
|
reviews = request(f"/pulls/{pr_number}/reviews", token)
|
|
if not isinstance(reviews, list):
|
|
raise RuntimeError("Gitea returned invalid review records")
|
|
by_reviewer = [
|
|
review for review in reviews
|
|
if isinstance(review, dict)
|
|
and (review.get("user") or {}).get("login") == "review_agent"
|
|
and review.get("commit_id") == head
|
|
]
|
|
if not by_reviewer:
|
|
return False
|
|
latest = max(by_reviewer, key=lambda review: int(review.get("id") or 0))
|
|
return latest.get("state") == "APPROVED"
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("operation", choices=("inspect", "merge"))
|
|
parser.add_argument("pr", type=int)
|
|
parser.add_argument("reviewed_head", nargs="?")
|
|
parser.add_argument("review_file", nargs="?", type=Path)
|
|
args = parser.parse_args()
|
|
if args.pr < 1:
|
|
parser.error("PR number must be positive")
|
|
if args.operation == "merge" and (not args.reviewed_head or not args.review_file):
|
|
parser.error("merge requires reviewed_head and review_file")
|
|
token = token_from_file()
|
|
before = inspect(args.pr, token)
|
|
if args.operation == "inspect":
|
|
print(json.dumps(before, sort_keys=True))
|
|
return 0
|
|
if not FULL_SHA.fullmatch(args.reviewed_head):
|
|
parser.error("reviewed head must be a full SHA")
|
|
validate_review(args.review_file, args.reviewed_head)
|
|
if not has_platform_review(args.pr, args.reviewed_head, token):
|
|
raise RuntimeError("Gitea lacks an exact-head approval by review_agent")
|
|
if before["base"] != "dev" or before["head"] != args.reviewed_head:
|
|
raise RuntimeError("PR target or current head differs from the approved review")
|
|
if before["state"] != "open" or before["merged"] or before["mergeable"] is not True:
|
|
raise RuntimeError("PR is closed, merged, or not mergeable")
|
|
if before["check_count"] and before["check_state"] != "success":
|
|
raise RuntimeError("a configured commit check has not passed")
|
|
# Gitea rechecks branch protection at the merge endpoint. The caller must
|
|
# independently verify Maestro and task-level evidence before invoking us.
|
|
# Gitea checks head_commit_id atomically while processing the merge.
|
|
request(f"/pulls/{args.pr}/merge", token,
|
|
{"do": "squash", "head_commit_id": args.reviewed_head})
|
|
after = inspect(args.pr, token)
|
|
if after["merged"] is not True or after["head"] != args.reviewed_head:
|
|
raise RuntimeError("merge result is missing or differs from the reviewed head")
|
|
print(json.dumps(after, sort_keys=True))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except (OSError, ValueError, RuntimeError) as error:
|
|
print(f"approval gate failed: {error}", file=sys.stderr)
|
|
raise SystemExit(1)
|