From 3179390af94d57c903b430dbb43a4b35b9ac050c Mon Sep 17 00:00:00 2001 From: S Date: Thu, 26 Feb 2026 15:27:14 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20add=20Phase=207=20=E2=80=94=20Branch=20?= =?UTF-8?q?Protection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- phase7_branch_protection.sh | 91 +++++++++++++++++++++++++++++++++++++ phase7_post_check.sh | 64 ++++++++++++++++++++++++++ phase7_teardown.sh | 37 +++++++++++++++ 3 files changed, 192 insertions(+) create mode 100755 phase7_branch_protection.sh create mode 100755 phase7_post_check.sh create mode 100755 phase7_teardown.sh diff --git a/phase7_branch_protection.sh b/phase7_branch_protection.sh new file mode 100755 index 0000000..be03f59 --- /dev/null +++ b/phase7_branch_protection.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# phase7_branch_protection.sh — Set up branch protection rules on all repos +# Depends on: Phase 4 complete (repos exist on Gitea primary) +# For each repo: +# 1. Check if protection already exists for the target branch +# 2. Create protection rule with configurable settings from .env +# Settings include: block direct push, optionally require PR reviews. +# Idempotent: skips repos that already have protection on the target branch. +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env +require_vars GITEA_ADMIN_TOKEN GITEA_INTERNAL_URL GITEA_ORG_NAME \ + REPO_1_NAME REPO_2_NAME REPO_3_NAME \ + PROTECTED_BRANCH REQUIRE_PR_REVIEW REQUIRED_APPROVALS + +phase_header 7 "Branch Protection" + +REPOS=("$REPO_1_NAME" "$REPO_2_NAME" "$REPO_3_NAME") + +SUCCESS=0 +FAILED=0 + +for repo in "${REPOS[@]}"; do + log_info "--- Processing repo: ${repo} ---" + + # ------------------------------------------------------------------------- + # Idempotency: check if branch protection already exists + # The branch_protections endpoint returns the rule if it exists (200) + # or 404 if not. We URL-encode the branch name for safety. + # ------------------------------------------------------------------------- + if gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/branch_protections/${PROTECTED_BRANCH}" >/dev/null 2>&1; then + log_info "Branch protection for '${PROTECTED_BRANCH}' already exists on ${repo} — skipping" + SUCCESS=$((SUCCESS + 1)) + continue + fi + + # ------------------------------------------------------------------------- + # Create branch protection rule + # Key settings: + # - enable_push=false: blocks direct pushes (must use PRs) + # - enable_status_check=true: required CI checks must pass + # - required_approvals: configurable from .env + # ------------------------------------------------------------------------- + log_info "Creating branch protection for '${PROTECTED_BRANCH}' on ${repo}..." + + PROTECTION_PAYLOAD=$(jq -n \ + --arg branch_name "$PROTECTED_BRANCH" \ + --argjson enable_push false \ + --argjson enable_push_whitelist false \ + --argjson require_signed_commits false \ + --argjson enable_status_check true \ + --argjson enable_approvals_whitelist "${REQUIRE_PR_REVIEW}" \ + --argjson required_approvals "${REQUIRED_APPROVALS}" \ + '{ + branch_name: $branch_name, + enable_push: $enable_push, + enable_push_whitelist: $enable_push_whitelist, + require_signed_commits: $require_signed_commits, + enable_status_check: $enable_status_check, + enable_approvals_whitelist: $enable_approvals_whitelist, + required_approvals: $required_approvals + }') + + if gitea_api POST "/repos/${GITEA_ORG_NAME}/${repo}/branch_protections" "$PROTECTION_PAYLOAD" >/dev/null; then + log_success "Branch protection created for ${repo}" + SUCCESS=$((SUCCESS + 1)) + else + log_error "Failed to create branch protection for ${repo}" + FAILED=$((FAILED + 1)) + fi +done + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +printf '\n' +TOTAL=${#REPOS[@]} +log_info "Results: ${SUCCESS} succeeded, ${FAILED} failed (out of ${TOTAL})" + +if [[ $FAILED -gt 0 ]]; then + log_error "Some repos failed — check logs above" + exit 1 +fi + +log_success "Phase 7 complete — branch protection applied" diff --git a/phase7_post_check.sh b/phase7_post_check.sh new file mode 100755 index 0000000..e012235 --- /dev/null +++ b/phase7_post_check.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# phase7_post_check.sh — Verify Phase 7 (Branch Protection) succeeded +# Checks for each repo: +# 1. Branch protection rule exists for PROTECTED_BRANCH +# 2. Push is blocked (enable_push is false) +# Exits 0 only if ALL checks pass. +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env +require_vars GITEA_ADMIN_TOKEN GITEA_INTERNAL_URL GITEA_ORG_NAME \ + REPO_1_NAME REPO_2_NAME REPO_3_NAME PROTECTED_BRANCH + +log_info "=== Phase 7 Post-Check ===" + +REPOS=("$REPO_1_NAME" "$REPO_2_NAME" "$REPO_3_NAME") +PASS=0 +FAIL=0 + +run_check() { + local description="$1"; shift + if "$@" 2>/dev/null; then + log_success "$description" + PASS=$((PASS + 1)) + else + log_error "FAIL: $description" + FAIL=$((FAIL + 1)) + fi +} + +for repo in "${REPOS[@]}"; do + log_info "--- Checking repo: ${repo} ---" + + # Check 1: Protection rule exists + run_check "Branch protection exists for '${PROTECTED_BRANCH}' on ${repo}" \ + gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/branch_protections/${PROTECTED_BRANCH}" -o /dev/null + + # Check 2: Push is blocked (enable_push should be false) + check_push_blocked() { + local protection + protection=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/branch_protections/${PROTECTED_BRANCH}") + local enable_push + enable_push=$(printf '%s' "$protection" | jq -r '.enable_push') + [[ "$enable_push" == "false" ]] + } + run_check "Direct push blocked on '${PROTECTED_BRANCH}' for ${repo}" check_push_blocked +done + +# Summary +printf '\n' +log_info "Results: ${PASS} passed, ${FAIL} failed" + +if [[ $FAIL -gt 0 ]]; then + log_error "Phase 7 post-check FAILED" + exit 1 +else + log_success "Phase 7 post-check PASSED — branch protection active" + exit 0 +fi diff --git a/phase7_teardown.sh b/phase7_teardown.sh new file mode 100755 index 0000000..3592a6a --- /dev/null +++ b/phase7_teardown.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# phase7_teardown.sh — Remove branch protection rules from all repos +# Deletes the PROTECTED_BRANCH protection rule via API. +# Safe to run if protection rules have already been removed. +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env +require_vars GITEA_ADMIN_TOKEN GITEA_INTERNAL_URL GITEA_ORG_NAME \ + REPO_1_NAME REPO_2_NAME REPO_3_NAME PROTECTED_BRANCH + +log_warn "=== Phase 7 Teardown: Branch Protection ===" + +REPOS=("$REPO_1_NAME" "$REPO_2_NAME" "$REPO_3_NAME") + +printf 'This will remove branch protection for "%s" on all repos. Continue? [y/N] ' "$PROTECTED_BRANCH" +read -r confirm +if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + log_info "Teardown cancelled" + exit 0 +fi + +for repo in "${REPOS[@]}"; do + if gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/branch_protections/${PROTECTED_BRANCH}" >/dev/null 2>&1; then + gitea_api DELETE "/repos/${GITEA_ORG_NAME}/${repo}/branch_protections/${PROTECTED_BRANCH}" >/dev/null 2>&1 || true + log_success "Removed branch protection from ${repo}" + else + log_info "No branch protection on ${repo} — already clean" + fi +done + +log_success "Phase 7 teardown complete"