#!/usr/bin/env python3 """ Sync branch protection rules from .gitea/branch-protection/*.yml to Gitea. Correctly uses the Gitea 1.25+ API (not GitHub-style). """ import os import sys import json import urllib.request import yaml GITEA_URL = os.getenv("GITEA_URL", "https://forge.alexanderwhitestone.com") GITEA_TOKEN = os.getenv("GITEA_TOKEN", "") ORG = "Timmy_Foundation" CONFIG_DIR = ".gitea/branch-protection" def api_request(method: str, path: str, payload: dict | None = None) -> dict: url = f"{GITEA_URL}/api/v1{path}" data = json.dumps(payload).encode() if payload else None req = urllib.request.Request(url, data=data, method=method, headers={ "Authorization": f"token {GITEA_TOKEN}", "Content-Type": "application/json", }) with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read().decode()) def apply_protection(repo: str, rules: dict) -> bool: branch = rules.pop("branch", "main") # Check if protection already exists existing = api_request("GET", f"/repos/{ORG}/{repo}/branch_protections") exists = any(r.get("branch_name") == branch for r in existing) payload = { "branch_name": branch, "rule_name": branch, "required_approvals": rules.get("required_approvals", 1), "block_on_rejected_reviews": rules.get("block_on_rejected_reviews", True), "dismiss_stale_approvals": rules.get("dismiss_stale_approvals", True), "block_deletions": rules.get("block_deletions", True), "block_force_push": rules.get("block_force_push", True), "block_admin_merge_override": rules.get("block_admin_merge_override", True), "enable_status_check": rules.get("require_ci_to_merge", False), "status_check_contexts": rules.get("status_check_contexts", []), } try: if exists: api_request("PATCH", f"/repos/{ORG}/{repo}/branch_protections/{branch}", payload) else: api_request("POST", f"/repos/{ORG}/{repo}/branch_protections", payload) print(f"✅ {repo}:{branch} synced") return True except Exception as e: print(f"❌ {repo}:{branch} failed: {e}") return False def main() -> int: if not GITEA_TOKEN: print("ERROR: GITEA_TOKEN not set") return 1 ok = 0 for fname in os.listdir(CONFIG_DIR): if not fname.endswith(".yml"): continue repo = fname[:-4] with open(os.path.join(CONFIG_DIR, fname)) as f: cfg = yaml.safe_load(f) if apply_protection(repo, cfg.get("rules", {})): ok += 1 print(f"\nSynced {ok} repo(s)") return 0 if __name__ == "__main__": raise SystemExit(main())