From 058b85e1468330cca3f98944c40d539804db858b Mon Sep 17 00:00:00 2001 From: S Date: Thu, 26 Feb 2026 15:27:13 -0600 Subject: [PATCH] =?UTF-8?q?feat:=20add=20Phase=206=20=E2=80=94=20GitHub=20?= =?UTF-8?q?Push=20Mirrors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- phase6_github_mirrors.sh | 117 +++++++++++++++++++++++++++++++++++++++ phase6_post_check.sh | 79 ++++++++++++++++++++++++++ phase6_teardown.sh | 52 +++++++++++++++++ 3 files changed, 248 insertions(+) create mode 100755 phase6_github_mirrors.sh create mode 100755 phase6_post_check.sh create mode 100755 phase6_teardown.sh diff --git a/phase6_github_mirrors.sh b/phase6_github_mirrors.sh new file mode 100755 index 0000000..602e7ad --- /dev/null +++ b/phase6_github_mirrors.sh @@ -0,0 +1,117 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# phase6_github_mirrors.sh — Configure push mirrors from Gitea → GitHub +# Depends on: Phase 4 complete (repos exist on Gitea primary) +# For each repo: +# 1. Create push mirror config (Gitea → GitHub) +# 2. Trigger initial sync +# 3. Disable GitHub Actions (since Gitea is now the CI source of truth) +# Push mirrors ensure GitHub stays as an offsite backup. Every push to Gitea +# is automatically mirrored to GitHub on commit + on a schedule. +# Idempotent: skips repos that already have push mirrors configured. +# ============================================================================= + +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 \ + GITHUB_USERNAME GITHUB_MIRROR_TOKEN GITHUB_MIRROR_INTERVAL \ + REPO_1_NAME REPO_2_NAME REPO_3_NAME + +phase_header 6 "GitHub Push Mirrors" + +REPOS=("$REPO_1_NAME" "$REPO_2_NAME" "$REPO_3_NAME") + +SUCCESS=0 +FAILED=0 + +for repo in "${REPOS[@]}"; do + log_info "--- Processing repo: ${repo} ---" + + # ------------------------------------------------------------------------- + # Step A: Check if push mirror already exists + # The push_mirrors endpoint returns an array — if non-empty, skip. + # ------------------------------------------------------------------------- + log_step "A" "Checking existing push mirrors for ${repo}..." + EXISTING_MIRRORS=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/push_mirrors" 2>/dev/null || echo "[]") + MIRROR_COUNT=$(printf '%s' "$EXISTING_MIRRORS" | jq 'length' 2>/dev/null || echo 0) + + if [[ "$MIRROR_COUNT" -gt 0 ]]; then + log_info "Push mirror already configured for ${repo} — skipping" + SUCCESS=$((SUCCESS + 1)) + continue + fi + + # ------------------------------------------------------------------------- + # Step B: Create push mirror + # Configures Gitea to push all refs to GitHub on every commit and on a + # schedule. Uses a dedicated GitHub PAT (GITHUB_MIRROR_TOKEN) which needs + # repo write scope. + # ------------------------------------------------------------------------- + log_step "B" "Creating push mirror for ${repo}..." + MIRROR_PAYLOAD=$(jq -n \ + --arg remote_address "https://github.com/${GITHUB_USERNAME}/${repo}.git" \ + --arg remote_username "$GITHUB_USERNAME" \ + --arg remote_password "$GITHUB_MIRROR_TOKEN" \ + --arg interval "$GITHUB_MIRROR_INTERVAL" \ + '{ + remote_address: $remote_address, + remote_username: $remote_username, + remote_password: $remote_password, + interval: $interval, + sync_on_commit: true + }') + + if gitea_api POST "/repos/${GITEA_ORG_NAME}/${repo}/push_mirrors" "$MIRROR_PAYLOAD" >/dev/null; then + log_success "Push mirror created for ${repo}" + else + log_error "Failed to create push mirror for ${repo}" + FAILED=$((FAILED + 1)) + continue + fi + + # ------------------------------------------------------------------------- + # Step C: Trigger initial sync + # Forces an immediate push to GitHub rather than waiting for the interval. + # ------------------------------------------------------------------------- + log_step "C" "Triggering initial sync for ${repo}..." + gitea_api POST "/repos/${GITEA_ORG_NAME}/${repo}/push_mirrors-sync" "" >/dev/null 2>&1 || true + log_info "Sync triggered (runs async)" + + # ------------------------------------------------------------------------- + # Step D: Disable GitHub Actions on the source repo + # Since Gitea is now the CI source of truth, we disable Actions on GitHub + # to prevent duplicate pipeline runs. The GitHub API doesn't have a direct + # "disable Actions" toggle, but we can disable it via the repository + # settings endpoint if the field is available. If not, log instructions. + # ------------------------------------------------------------------------- + log_step "D" "Disabling GitHub Actions on ${repo}..." + DISABLE_RESPONSE=$(github_api PUT "/repos/${GITHUB_USERNAME}/${repo}/actions/permissions" \ + '{"enabled": false}' 2>/dev/null || echo "FAILED") + + if [[ "$DISABLE_RESPONSE" == "FAILED" ]]; then + log_warn "Could not disable GitHub Actions via API for ${repo}" + log_warn " → Manually disable at: https://github.com/${GITHUB_USERNAME}/${repo}/settings/actions" + else + log_success "GitHub Actions disabled for ${repo}" + fi + + SUCCESS=$((SUCCESS + 1)) +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 6 complete — push mirrors configured" diff --git a/phase6_post_check.sh b/phase6_post_check.sh new file mode 100755 index 0000000..8c61a15 --- /dev/null +++ b/phase6_post_check.sh @@ -0,0 +1,79 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# phase6_post_check.sh — Verify Phase 6 (GitHub Push Mirrors) succeeded +# Checks for each repo: +# 1. Push mirror config exists in Gitea +# 2. Triggers sync and verifies GitHub has matching latest commit +# 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 \ + GITHUB_USERNAME GITHUB_TOKEN \ + REPO_1_NAME REPO_2_NAME REPO_3_NAME + +log_info "=== Phase 6 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: Push mirror exists + check_mirror_exists() { + local mirrors + mirrors=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/$1/push_mirrors") + local count + count=$(printf '%s' "$mirrors" | jq 'length') + [[ "$count" -gt 0 ]] + } + run_check "Push mirror exists for ${repo}" check_mirror_exists "$repo" + + # Check 2: Latest commit SHA matches between Gitea and GitHub + # Trigger a sync first, then compare HEAD commits + check_commit_sync() { + # Trigger sync + gitea_api POST "/repos/${GITEA_ORG_NAME}/$1/push_mirrors-sync" "" >/dev/null 2>&1 || true + # Brief wait for sync to propagate + sleep 3 + + # Get Gitea's latest commit + local gitea_sha github_sha + gitea_sha=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/$1/commits?limit=1" | jq -r '.[0].sha') + # Get GitHub's latest commit + github_sha=$(github_api GET "/repos/${GITHUB_USERNAME}/$1/commits?per_page=1" | jq -r '.[0].sha') + + [[ "$gitea_sha" == "$github_sha" ]] + } + run_check "Latest commit synced to GitHub for ${repo}" check_commit_sync "$repo" +done + +# Summary +printf '\n' +log_info "Results: ${PASS} passed, ${FAIL} failed" + +if [[ $FAIL -gt 0 ]]; then + log_error "Phase 6 post-check FAILED" + exit 1 +else + log_success "Phase 6 post-check PASSED — push mirrors working" + exit 0 +fi diff --git a/phase6_teardown.sh b/phase6_teardown.sh new file mode 100755 index 0000000..34bf434 --- /dev/null +++ b/phase6_teardown.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# phase6_teardown.sh — Remove push mirror config from all repos +# For each repo: fetches mirror ID from Gitea, then deletes it. +# Re-enables GitHub Actions on the source repos. +# Safe to run if mirrors 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 \ + GITHUB_USERNAME GITHUB_TOKEN \ + REPO_1_NAME REPO_2_NAME REPO_3_NAME + +log_warn "=== Phase 6 Teardown: Push Mirrors ===" + +REPOS=("$REPO_1_NAME" "$REPO_2_NAME" "$REPO_3_NAME") + +printf 'This will remove all push mirror configurations. Continue? [y/N] ' +read -r confirm +if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + log_info "Teardown cancelled" + exit 0 +fi + +for repo in "${REPOS[@]}"; do + log_info "--- Processing: ${repo} ---" + + # Get push mirror IDs (there could be multiple, delete all) + MIRRORS=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/push_mirrors" 2>/dev/null || echo "[]") + MIRROR_IDS=$(printf '%s' "$MIRRORS" | jq -r '.[].remote_name' 2>/dev/null || true) + + if [[ -z "$MIRROR_IDS" ]]; then + log_info "No push mirrors found for ${repo} — already clean" + else + for mirror_id in $MIRROR_IDS; do + gitea_api DELETE "/repos/${GITEA_ORG_NAME}/${repo}/push_mirrors/${mirror_id}" >/dev/null 2>&1 || true + log_success "Removed push mirror '${mirror_id}' from ${repo}" + done + fi + + # Re-enable GitHub Actions + github_api PUT "/repos/${GITHUB_USERNAME}/${repo}/actions/permissions" \ + '{"enabled": true}' >/dev/null 2>&1 || true + log_info "GitHub Actions re-enabled for ${repo}" +done + +log_success "Phase 6 teardown complete"