diff --git a/gitea-branch-protection.sh b/gitea-branch-protection.sh index 09f43d41..a812d003 100644 --- a/gitea-branch-protection.sh +++ b/gitea-branch-protection.sh @@ -1,44 +1,6 @@ #!/bin/bash - -# Apply branch protections to all repositories -# Requires GITEA_TOKEN env var - -REPOS=("hermes-agent" "the-nexus" "timmy-home" "timmy-config") - -for repo in "${REPOS[@]}" -do - curl -X POST "https://forge.alexanderwhitestone.com/api/v1/repos/Timmy_Foundation/$repo/branches/main/protection" \ - -H "Authorization: token $GITEA_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "required_reviews": 1, - "dismiss_stale_reviews": true, - "block_force_push": true, - "block_deletions": true - }' -done -#!/bin/bash - -# Gitea API credentials -GITEA_TOKEN="your-personal-access-token" -GITEA_API="https://forge.alexanderwhitestone.com/api/v1" - -# Repos to protect -REPOS=("hermes-agent" "the-nexus" "timmy-home" "timmy-config") - -for REPO in "${REPO[@]}"; do - echo "Configuring branch protection for $REPO..." - - curl -X POST -H "Authorization: token $GITEA_TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "name": "main", - "require_pull_request": true, - "required_approvals": 1, - "dismiss_stale_approvals": true, - "required_status_checks": '"$(test "$REPO" = "hermes-agent" && echo "true" || echo "false")"', - "block_force_push": true, - "block_delete": true - }' \ - "$GITEA_API/repos/Timmy_Foundation/$REPO/branch_protection" -done +# Wrapper for the canonical branch-protection sync script. +# Usage: ./gitea-branch-protection.sh +set -euo pipefail +cd "$(dirname "$0")" +python3 scripts/sync_branch_protection.py diff --git a/scripts/sync_branch_protection.py b/scripts/sync_branch_protection.py new file mode 100644 index 00000000..12233d3a --- /dev/null +++ b/scripts/sync_branch_protection.py @@ -0,0 +1,81 @@ +#!/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())