#!/usr/bin/env bash # branch-protection.sh — Helper for setting up branch protection on repos # # Source after lib/env.sh: # source "$(dirname "$0")/../lib/env.sh" # source "$(dirname "$0")/lib/branch-protection.sh" # # Required globals: FORGE_TOKEN, FORGE_URL, FORGE_OPS_REPO # # Functions: # setup_vault_branch_protection — Set up admin-only branch protection for main # verify_branch_protection — Verify protection is configured correctly # setup_profile_branch_protection — Set up admin-only branch protection for .profile repos # remove_branch_protection — Remove branch protection (for cleanup/testing) # # Branch protection settings: # - Require 1 approval before merge # - Restrict merge to admin role (not regular collaborators or bots) # - Block direct pushes to main (all changes must go through PR) set -euo pipefail # Internal log helper _bp_log() { if declare -f log >/dev/null 2>&1; then log "branch-protection: $*" else printf '[%s] branch-protection: %s\n' "$(date -u '+%Y-%m-%d %H:%M:%S UTC')" "$*" >&2 fi } # Get ops repo API URL _ops_api() { printf '%s' "${FORGE_URL}/api/v1/repos/${FORGE_OPS_REPO}" } # ----------------------------------------------------------------------------- # setup_vault_branch_protection — Set up admin-only branch protection for main # # Configures the following protection rules: # - Require 1 approval before merge # - Restrict merge to admin role (not regular collaborators or bots) # - Block direct pushes to main (all changes must go through PR) # # Returns: 0 on success, 1 on failure # ----------------------------------------------------------------------------- setup_vault_branch_protection() { local branch="${1:-main}" local api_url api_url="$(_ops_api)" _bp_log "Setting up branch protection for ${branch} on ${FORGE_OPS_REPO}" # Check if branch exists local branch_exists branch_exists=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token ${FORGE_TOKEN}" \ "${api_url}/git/branches/${branch}" 2>/dev/null || echo "0") if [ "$branch_exists" != "200" ]; then _bp_log "ERROR: Branch ${branch} does not exist" return 1 fi # Check if protection already exists local protection_exists protection_exists=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token ${FORGE_TOKEN}" \ "${api_url}/branches/${branch}/protection" 2>/dev/null || echo "0") if [ "$protection_exists" = "200" ]; then _bp_log "Branch protection already exists for ${branch}" _bp_log "Updating existing protection rules" fi # Create/update branch protection # Note: Forgejo API uses "require_signed_commits" and "required_approvals" for approval requirements # The "admin_enforced" field ensures only admins can merge local protection_json protection_json=$(cat </dev/null || true) if [ -z "$protection_json" ] || [ "$protection_json" = "null" ]; then _bp_log "ERROR: No branch protection found for ${branch}" return 1 fi # Extract and validate settings local enable_push enable_merge_commit required_approvals admin_enforced enable_push=$(printf '%s' "$protection_json" | jq -r '.enable_push // true') enable_merge_commit=$(printf '%s' "$protection_json" | jq -r '.enable_merge_commit // false') required_approvals=$(printf '%s' "$protection_json" | jq -r '.required_approvals // 0') admin_enforced=$(printf '%s' "$protection_json" | jq -r '.admin_enforced // false') local errors=0 # Check push is disabled if [ "$enable_push" = "true" ]; then _bp_log "ERROR: enable_push should be false" errors=$((errors + 1)) else _bp_log "OK: Pushes are blocked" fi # Check merge commit is enabled if [ "$enable_merge_commit" != "true" ]; then _bp_log "ERROR: enable_merge_commit should be true" errors=$((errors + 1)) else _bp_log "OK: Merge commits are allowed" fi # Check required approvals if [ "$required_approvals" -lt 1 ]; then _bp_log "ERROR: required_approvals should be at least 1" errors=$((errors + 1)) else _bp_log "OK: Required approvals: ${required_approvals}" fi # Check admin enforced if [ "$admin_enforced" != "true" ]; then _bp_log "ERROR: admin_enforced should be true" errors=$((errors + 1)) else _bp_log "OK: Admin enforcement enabled" fi if [ "$errors" -gt 0 ]; then _bp_log "Verification failed with ${errors} error(s)" return 1 fi _bp_log "Branch protection verified successfully" return 0 } # ----------------------------------------------------------------------------- # setup_profile_branch_protection — Set up admin-only branch protection for .profile repos # # Configures the following protection rules: # - Require 1 approval before merge # - Restrict merge to admin role (not regular collaborators or bots) # - Block direct pushes to main (all changes must go through PR) # # Also creates a 'journal' branch for direct agent journal pushes # # Args: # $1 - Repo path in format 'owner/repo' (e.g., 'dev-bot/.profile') # $2 - Branch to protect (default: main) # # Returns: 0 on success, 1 on failure # ----------------------------------------------------------------------------- setup_profile_branch_protection() { local repo="${1:-}" local branch="${2:-main}" if [ -z "$repo" ]; then _bp_log "ERROR: repo path required (format: owner/repo)" return 1 fi _bp_log "Setting up branch protection for ${branch} on ${repo}" local api_url api_url="${FORGE_URL}/api/v1/repos/${repo}" # Check if branch exists local branch_exists branch_exists=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token ${FORGE_TOKEN}" \ "${api_url}/git/branches/${branch}" 2>/dev/null || echo "0") if [ "$branch_exists" != "200" ]; then _bp_log "ERROR: Branch ${branch} does not exist on ${repo}" return 1 fi # Check if protection already exists local protection_exists protection_exists=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token ${FORGE_TOKEN}" \ "${api_url}/branches/${branch}/protection" 2>/dev/null || echo "0") if [ "$protection_exists" = "200" ]; then _bp_log "Branch protection already exists for ${branch}" _bp_log "Updating existing protection rules" fi # Create/update branch protection local protection_json protection_json=$(cat </dev/null || echo "0") if [ "$journal_exists" != "200" ]; then # Create journal branch from main # Get the commit hash of main local main_commit main_commit=$(curl -sf -H "Authorization: token ${FORGE_TOKEN}" \ "${api_url}/git/refs/heads/${branch}" 2>/dev/null | jq -r '.[0].object.sha' || echo "") if [ -n "$main_commit" ]; then curl -sf -X POST \ -H "Authorization: token ${FORGE_TOKEN}" \ -H "Content-Type: application/json" \ "${api_url}/git/refs" \ -d "{\"ref\":\"refs/heads/${journal_branch}\",\"sha\":\"${main_commit}\"}" >/dev/null 2>&1 || { _bp_log "Warning: failed to create journal branch (may already exist)" } fi fi _bp_log "Journal branch '${journal_branch}' ready for direct pushes" return 0 } # ----------------------------------------------------------------------------- # remove_branch_protection — Remove branch protection (for cleanup/testing) # # Returns: 0 on success, 1 on failure # ----------------------------------------------------------------------------- remove_branch_protection() { local branch="${1:-main}" local api_url api_url="$(_ops_api)" _bp_log "Removing branch protection for ${branch}" # Check if protection exists local protection_exists protection_exists=$(curl -s -o /dev/null -w "%{http_code}" \ -H "Authorization: token ${FORGE_TOKEN}" \ "${api_url}/branches/${branch}/protection" 2>/dev/null || echo "0") if [ "$protection_exists" != "200" ]; then _bp_log "No branch protection found for ${branch}" return 0 fi # Delete protection local http_code http_code=$(curl -s -o /dev/null -w "%{http_code}" \ -X DELETE \ -H "Authorization: token ${FORGE_TOKEN}" \ "${api_url}/branches/${branch}/protection" 2>/dev/null || echo "0") if [ "$http_code" != "204" ]; then _bp_log "ERROR: Failed to remove branch protection (HTTP ${http_code})" return 1 fi _bp_log "Branch protection removed successfully for ${branch}" return 0 } # ----------------------------------------------------------------------------- # Test mode — run when executed directly # ----------------------------------------------------------------------------- if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then # Check required env vars if [ -z "${FORGE_TOKEN:-}" ]; then echo "ERROR: FORGE_TOKEN is required" >&2 exit 1 fi if [ -z "${FORGE_URL:-}" ]; then echo "ERROR: FORGE_URL is required" >&2 exit 1 fi if [ -z "${FORGE_OPS_REPO:-}" ]; then echo "ERROR: FORGE_OPS_REPO is required" >&2 exit 1 fi # Parse command line args case "${1:-help}" in setup) setup_vault_branch_protection "${2:-main}" ;; setup-profile) if [ -z "${2:-}" ]; then echo "ERROR: repo path required (format: owner/repo)" >&2 exit 1 fi setup_profile_branch_protection "${2}" "${3:-main}" ;; verify) verify_branch_protection "${2:-main}" ;; remove) remove_branch_protection "${2:-main}" ;; help|*) echo "Usage: $0 {setup|setup-profile|verify|remove} [args...]" echo "" echo "Commands:" echo " setup [branch] Set up branch protection on ops repo (default: main)" echo " setup-profile [branch] Set up branch protection on .profile repo" echo " verify [branch] Verify branch protection is configured correctly" echo " remove [branch] Remove branch protection (for cleanup/testing)" echo "" echo "Required environment variables:" echo " FORGE_TOKEN Forgejo API token (admin user recommended)" echo " FORGE_URL Forgejo instance URL (e.g., https://codeberg.org)" echo " FORGE_OPS_REPO Ops repo in format owner/repo (e.g., johba/disinto-ops)" exit 0 ;; esac fi