#!/usr/bin/env python3 """ Review Gate — Poka-yoke for unreviewed merges. Fails if the current PR has fewer than 1 approving review. Usage in Gitea workflow: - name: Review Approval Gate run: python scripts/review_gate.py env: GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} """ import os import sys import json import subprocess from urllib import request, error GITEA_TOKEN = os.environ.get("GITEA_TOKEN", "") GITEA_URL = os.environ.get("GITEA_URL", "https://forge.alexanderwhitestone.com") REPO = os.environ.get("GITEA_REPO", "") PR_NUMBER = os.environ.get("PR_NUMBER", "") def api_call(method, path): url = f"{GITEA_URL}/api/v1{path}" headers = {"Authorization": f"token {GITEA_TOKEN}"} req = request.Request(url, method=method, headers=headers) try: with request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode()) except error.HTTPError as e: return {"error": e.read().decode(), "status": e.code} def main(): if not GITEA_TOKEN: print("ERROR: GITEA_TOKEN not set") sys.exit(1) if not REPO: print("ERROR: GITEA_REPO not set") sys.exit(1) pr_number = PR_NUMBER if not pr_number: # Try to infer from Gitea Actions environment pr_number = os.environ.get("GITEA_PULL_REQUEST_INDEX", "") if not pr_number: print("ERROR: Could not determine PR number") sys.exit(1) reviews = api_call("GET", f"/repos/{REPO}/pulls/{pr_number}/reviews") if isinstance(reviews, dict) and "error" in reviews: print(f"ERROR fetching reviews: {reviews}") sys.exit(1) approvals = [r for r in reviews if r.get("state") == "APPROVED"] if len(approvals) >= 1: print(f"OK: PR #{pr_number} has {len(approvals)} approving review(s).") sys.exit(0) else: print(f"BLOCKED: PR #{pr_number} has no approving reviews.") print("Merges are not permitted without at least one approval.") sys.exit(1) if __name__ == "__main__": main()