Some checks failed
Smoke Test / smoke (pull_request) Failing after 19s
Architecture Lint / Linter Tests (pull_request) Successful in 22s
Validate Config / YAML Lint (pull_request) Failing after 15s
Validate Config / JSON Validate (pull_request) Successful in 18s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 57s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Shell Script Lint (pull_request) Failing after 1m0s
Validate Config / Cron Syntax Check (pull_request) Successful in 11s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 11s
Validate Config / Playbook Schema Validation (pull_request) Successful in 22s
Architecture Lint / Lint Repository (pull_request) Failing after 22s
PR Checklist / pr-checklist (pull_request) Successful in 4m20s
Add set_branch_protection() and related methods to GiteaClient. Fixes #482 via three-way enforcement: 1. gitea_client.py: Added get/set/delete_branch_protection() API wrappers 2. bin/enable-branch-protection.py: Idempotent script protects main branches of all Timmy_Foundation core repos with: ── required_approvals: 1 (at least one human review) ── require_status_checks: true (CI must pass) ── restrict_merge: true (only admins + reviewers can merge) 3. tests/test_gitea_client_core.py: Added TestBranchProtection suite Usage: after merging, run: bin/enable-branch-protection.py --dry-run # verify bin/enable-branch-protection.py # apply to all core repos This prevents agents from merging their own PRs before human review.
141 lines
4.0 KiB
Python
Executable File
141 lines
4.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
enable-branch-protection — Apply branch protection to Timmy_Foundation repos.
|
|
|
|
Enforces issue #482: agents must not merge before review.
|
|
Applies identical protection rules to the main branch of core repos:
|
|
- required_approvals: 1 (at least one human review)
|
|
- require_status_checks: true (CI must pass)
|
|
- restrict_merge: true (only admins + designated reviewers can merge)
|
|
|
|
Exit codes:
|
|
0 — all requested branches protected (or already protected)
|
|
1 — at least one branch could not be protected
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(REPO_ROOT))
|
|
|
|
from gitea_client import GiteaClient
|
|
|
|
# Core Timmy Foundation repositories
|
|
CORE_REPOS = [
|
|
"Timmy_Foundation/the-nexus",
|
|
"Timmy_Foundation/timmy-home",
|
|
"Timmy_Foundation/timmy-config",
|
|
"Timmy_Foundation/hermes-agent",
|
|
"Timmy_Foundation/.profile",
|
|
"Timmy_Foundation/the-door",
|
|
"Timmy_Foundation/turboquant",
|
|
]
|
|
|
|
PROTECTION_RULE = {
|
|
"required_approvals": 1,
|
|
"require_status_checks": True,
|
|
"restrict_merge": True,
|
|
"dismiss_stale_reviews": False,
|
|
"require_owner_approval": False,
|
|
"allow_force_pushes": False,
|
|
"allow_deletions": False,
|
|
}
|
|
|
|
|
|
def parse_args(argv: list[str]) -> argparse.Namespace:
|
|
p = argparse.ArgumentParser(
|
|
description="Apply branch protection to core repos (Closes #482)"
|
|
)
|
|
p.add_argument(
|
|
"--branch",
|
|
default="main",
|
|
help="Branch to protect (default: main)",
|
|
)
|
|
p.add_argument(
|
|
"--repo",
|
|
action="append",
|
|
help="Limit to specific repo(s); may be repeated. Overrides CORE_REPOS.",
|
|
)
|
|
p.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show what would change without making API calls",
|
|
)
|
|
p.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
help="Print detailed progress",
|
|
)
|
|
return p.parse_args(argv)
|
|
|
|
|
|
def protection_matches(current: dict[str, object], desired: dict[str, object]) -> bool:
|
|
for key, val in desired.items():
|
|
if current.get(key) != val:
|
|
return False
|
|
return True
|
|
|
|
|
|
def protect_branch(
|
|
client: GiteaClient,
|
|
repo: str,
|
|
branch: str,
|
|
dry_run: bool = False,
|
|
verbose: bool = False,
|
|
) -> tuple[bool, str]:
|
|
try:
|
|
current = client.get_branch_protection(repo, branch)
|
|
if current:
|
|
if protection_matches(current, PROTECTION_RULE):
|
|
return True, f"✓ {repo}/{branch} already protected"
|
|
if dry_run:
|
|
return True, f"⚠ {repo}/{branch} would be updated (rule differs)"
|
|
client.set_branch_protection(repo, branch, **PROTECTION_RULE)
|
|
return True, f"✓ {repo}/{branch} protection updated"
|
|
else:
|
|
if dry_run:
|
|
return True, f"+ {repo}/{branch} would be protected"
|
|
client.set_branch_protection(repo, branch, **PROTECTION_RULE)
|
|
return True, f"✓ {repo}/{branch} now protected"
|
|
except Exception as e:
|
|
return False, f"✗ {repo}/{branch}: {e}"
|
|
|
|
|
|
def main(argv: list[str] | None = None) -> int:
|
|
args = parse_args(argv or sys.argv[1:])
|
|
repos = args.repo if args.repo else CORE_REPOS
|
|
branch = args.branch
|
|
|
|
client = GiteaClient()
|
|
|
|
if not args.dry_run:
|
|
try:
|
|
client._get("/version")
|
|
except Exception as e:
|
|
print(f"ERROR: Cannot reach Gitea: {e}", file=sys.stderr)
|
|
return 1
|
|
|
|
all_ok = True
|
|
for repo in repos:
|
|
ok, msg = protect_branch(client, repo, branch, dry_run=args.dry_run, verbose=args.verbose)
|
|
print(msg)
|
|
if not ok:
|
|
all_ok = False
|
|
|
|
if all_ok:
|
|
print(f"\nDone. {len(repos)} branch{'s' if len(repos)!=1 else ''} checked.")
|
|
return 0
|
|
else:
|
|
print("\nSome branches failed. Review errors above.", file=sys.stderr)
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|