claude-home/server-configs/gitea/apply_branch_protection.py
Cal Corum 93ea435237 Add Gitea Actions workflow templates and automation
- Add Docker build workflow template with semantic versioning
- Add branch protection automation script
- Add deployment strategies documentation
- Add Harbor registry setup guide
- Update Gitea README with runner troubleshooting
- Add workflow template snippets for auto-deploy

Templates support:
- Semantic version validation on PRs
- Docker build and push to Docker Hub
- Discord notifications (success/failure)
- Build summaries and metadata extraction
- GitHub Actions cache optimization

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-05 13:40:17 -06:00

300 lines
12 KiB
Python
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
Apply branch protection rules to all Gitea repositories.
This script uses the Gitea API to apply consistent branch protection rules
across all repositories owned by a user or organization.
"""
import os
import sys
import requests
from typing import Optional
from dataclasses import dataclass
@dataclass
class BranchProtectionConfig:
"""Configuration for branch protection rules."""
# Branch to protect
branch_name: str = "main"
# Push settings
enable_push: bool = False # Disable direct pushes (force PR workflow)
enable_push_whitelist: bool = False
push_whitelist_usernames: list[str] = None
# Pull request approval settings
required_approvals: int = 1
enable_approvals_whitelist: bool = True
approvals_whitelist_usernames: list[str] = None
dismiss_stale_approvals: bool = True
# Status check settings
enable_status_check: bool = True
status_check_contexts: list[str] = None # Empty for now, add when CI/CD is set up
# Merge settings
enable_merge_whitelist: bool = True
merge_whitelist_usernames: list[str] = None
block_on_rejected_reviews: bool = True
block_on_outdated_branch: bool = True # Critical for database migrations
# Other settings
require_signed_commits: bool = False
protected_file_patterns: str = ""
unprotected_file_patterns: str = ""
def __post_init__(self):
"""Initialize mutable default values."""
if self.push_whitelist_usernames is None:
self.push_whitelist_usernames = []
if self.approvals_whitelist_usernames is None:
self.approvals_whitelist_usernames = []
if self.merge_whitelist_usernames is None:
self.merge_whitelist_usernames = []
if self.status_check_contexts is None:
self.status_check_contexts = []
class GiteaBranchProtectionManager:
"""Manages branch protection rules across Gitea repositories."""
def __init__(self, gitea_url: str, api_token: str, owner: str):
"""
Initialize the manager.
Args:
gitea_url: Base URL of your Gitea instance (e.g., https://git.example.com)
api_token: Gitea API token with repo access
owner: Username or organization name
"""
self.gitea_url = gitea_url.rstrip('/')
self.api_token = api_token
self.owner = owner
self.headers = {
'Authorization': f'token {api_token}',
'Content-Type': 'application/json'
}
def get_repositories(self) -> list[dict]:
"""Get all repositories for the authenticated user."""
# Use /user/repos for authenticated requests (works with API token)
url = f'{self.gitea_url}/api/v1/user/repos'
params = {'limit': 100} # Adjust if you have more than 100 repos
try:
response = requests.get(url, headers=self.headers, params=params)
response.raise_for_status()
repos = response.json()
# Filter to only repos owned by the specified owner if needed
if self.owner:
repos = [r for r in repos if r.get('owner', {}).get('login') == self.owner]
return repos
except requests.exceptions.RequestException as e:
print(f"❌ Error fetching repositories: {e}")
sys.exit(1)
def get_existing_branch_protections(self, repo_name: str) -> list[dict]:
"""Get existing branch protections for a repository."""
url = f'{self.gitea_url}/api/v1/repos/{self.owner}/{repo_name}/branch_protections'
try:
response = requests.get(url, headers=self.headers)
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f" ⚠️ Could not fetch existing protections: {e}")
return []
def delete_branch_protection(self, repo_name: str, protection_name: str) -> bool:
"""Delete an existing branch protection rule."""
url = f'{self.gitea_url}/api/v1/repos/{self.owner}/{repo_name}/branch_protections/{protection_name}'
try:
response = requests.delete(url, headers=self.headers)
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
print(f" ⚠️ Could not delete existing protection: {e}")
return False
def apply_branch_protection(self, repo_name: str, config: BranchProtectionConfig) -> bool:
"""
Apply branch protection rules to a repository.
Args:
repo_name: Name of the repository
config: Branch protection configuration
Returns:
True if successful, False otherwise
"""
url = f'{self.gitea_url}/api/v1/repos/{self.owner}/{repo_name}/branch_protections'
payload = {
'branch_name': config.branch_name,
'enable_push': config.enable_push,
'enable_push_whitelist': config.enable_push_whitelist,
'push_whitelist_usernames': config.push_whitelist_usernames,
'required_approvals': config.required_approvals,
'enable_approvals_whitelist': config.enable_approvals_whitelist,
'approvals_whitelist_usernames': config.approvals_whitelist_usernames,
'dismiss_stale_approvals': config.dismiss_stale_approvals,
'enable_status_check': config.enable_status_check,
'status_check_contexts': config.status_check_contexts,
'enable_merge_whitelist': config.enable_merge_whitelist,
'merge_whitelist_usernames': config.merge_whitelist_usernames,
'block_on_rejected_reviews': config.block_on_rejected_reviews,
'block_on_outdated_branch': config.block_on_outdated_branch,
'require_signed_commits': config.require_signed_commits,
'protected_file_patterns': config.protected_file_patterns,
'unprotected_file_patterns': config.unprotected_file_patterns,
}
try:
response = requests.post(url, headers=self.headers, json=payload)
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
print(f" ❌ Error: {e}")
if hasattr(e.response, 'text'):
print(f" Response: {e.response.text}")
return False
def apply_to_all_repos(self, config: BranchProtectionConfig, dry_run: bool = False):
"""
Apply branch protection rules to all repositories.
Args:
config: Branch protection configuration
dry_run: If True, only show what would be done without making changes
"""
repos = self.get_repositories()
if not repos:
print("No repositories found.")
return
print(f"\n{'DRY RUN - ' if dry_run else ''}Applying branch protection to {len(repos)} repositories...\n")
success_count = 0
skip_count = 0
error_count = 0
for repo in repos:
repo_name = repo['name']
print(f"📦 {repo_name}")
# Check if branch exists
default_branch = repo.get('default_branch', 'main')
if config.branch_name != default_branch:
print(f" Default branch is '{default_branch}', protecting '{config.branch_name}' instead")
if dry_run:
print(f" ✓ Would apply protection to '{config.branch_name}' branch")
skip_count += 1
continue
# Check for existing protection
existing_protections = self.get_existing_branch_protections(repo_name)
existing_main_protection = None
for protection in existing_protections:
if protection.get('branch_name') == config.branch_name:
existing_main_protection = protection.get('id') or protection.get('branch_name')
break
# Delete existing protection if it exists
if existing_main_protection:
print(f" 🔄 Updating existing protection...")
self.delete_branch_protection(repo_name, existing_main_protection)
# Apply new protection
if self.apply_branch_protection(repo_name, config):
print(f" ✅ Successfully applied branch protection")
success_count += 1
else:
print(f" ❌ Failed to apply branch protection")
error_count += 1
print(f"\n{'=' * 60}")
print(f"Summary:")
print(f" ✅ Successful: {success_count}")
if dry_run:
print(f" ⏭️ Skipped (dry run): {skip_count}")
if error_count > 0:
print(f" ❌ Errors: {error_count}")
print(f"{'=' * 60}\n")
def main():
"""Main entry point."""
# Configuration - adjust these values or use environment variables
GITEA_URL = os.getenv('GITEA_URL', 'https://git.manticorum.com')
GITEA_TOKEN = os.getenv('GITEA_TOKEN')
GITEA_OWNER = os.getenv('GITEA_OWNER', 'cal')
# Check for required configuration
if not GITEA_TOKEN:
print("❌ Error: GITEA_TOKEN environment variable is required")
print("\nUsage:")
print(" export GITEA_TOKEN='your-api-token-here'")
print(" python apply_branch_protection.py [--dry-run]")
print("\nOptional environment variables:")
print(" GITEA_URL (default: https://git.manticorum.com)")
print(" GITEA_OWNER (default: cal)")
sys.exit(1)
# Parse command line arguments
dry_run = '--dry-run' in sys.argv
# Configure branch protection rules
# Adjust these settings as needed
config = BranchProtectionConfig(
branch_name="main",
enable_push=False, # Disable direct pushes
required_approvals=1,
enable_approvals_whitelist=True,
approvals_whitelist_usernames=[GITEA_OWNER],
dismiss_stale_approvals=True,
enable_status_check=True,
enable_merge_whitelist=True,
merge_whitelist_usernames=[GITEA_OWNER],
block_on_rejected_reviews=True,
block_on_outdated_branch=True, # Critical for DB repos
require_signed_commits=False,
)
# Display configuration
print(f"\n{'=' * 60}")
print(f"Gitea Branch Protection Configuration")
print(f"{'=' * 60}")
print(f"Gitea URL: {GITEA_URL}")
print(f"Owner: {GITEA_OWNER}")
print(f"Branch: {config.branch_name}")
print(f"{'=' * 60}")
print(f"Protection Rules:")
print(f" • Direct pushes: {'Disabled' if not config.enable_push else 'Enabled'}")
print(f" • Required approvals: {config.required_approvals}")
print(f" • Approvals whitelist: {', '.join(config.approvals_whitelist_usernames)}")
print(f" • Dismiss stale approvals: {config.dismiss_stale_approvals}")
print(f" • Status checks enabled: {config.enable_status_check}")
print(f" • Merge whitelist: {', '.join(config.merge_whitelist_usernames)}")
print(f" • Block on rejected reviews: {config.block_on_rejected_reviews}")
print(f" • Block on outdated branch: {config.block_on_outdated_branch}")
print(f" • Require signed commits: {config.require_signed_commits}")
print(f"{'=' * 60}\n")
if dry_run:
print("🔍 DRY RUN MODE - No changes will be made\n")
# Create manager and apply rules
manager = GiteaBranchProtectionManager(GITEA_URL, GITEA_TOKEN, GITEA_OWNER)
manager.apply_to_all_repos(config, dry_run=dry_run)
if __name__ == '__main__':
main()