Files
gitea-migration/phase8_teardown.sh
S 316d318b5e feat: add cross-host SSH trust, state-aware teardown, and configurable migration polling
- Add setup/cross_host_ssh.sh to establish ed25519 SSH trust between
  Unraid and Fedora (required by backup/restore scripts for direct SCP)
- Add ssh_key and authorized_key cleanup handlers to setup/cleanup.sh
- Rewrite phase8 cutover to mark GitHub repos as mirrors instead of
  archiving them (archived repos reject push mirror writes), with a
  JSON state snapshot of pre-cutover settings (description, homepage,
  wiki, projects, Pages) for exact restoration on teardown
- Rewrite phase8 teardown to restore from state snapshot with fallback
  to legacy "— was:" description parsing
- Make migration polling configurable via MIGRATION_POLL_INTERVAL_SEC
  and MIGRATION_POLL_TIMEOUT_SEC in .env (was hardcoded 120s/3s)
- Fix preflight SSL validation: check SSL_MODE instead of always
  requiring SSL_EMAIL, add conditional checks per SSL_MODE
- Add preflight checks 23-24: cross-host SSH connectivity
- Add --start-from range validation and cross_host_ssh.sh to run_all.sh

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 20:50:41 -05:00

213 lines
8.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
# =============================================================================
# phase8_teardown.sh — Reverse the cutover: remove HTTPS, restore GitHub repos
# Steps:
# 1. Remove Nginx gitea.conf + reload
# 2. Remove cert renewal cron
# 3. Optionally remove SSL certificates
# 4. Restore GitHub repo settings from the saved Phase 8 state snapshot
# =============================================================================
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
source "${SCRIPT_DIR}/lib/common.sh"
load_env
require_vars UNRAID_IP UNRAID_SSH_USER \
GITEA_DOMAIN NGINX_CONTAINER_NAME NGINX_CONF_PATH \
SSL_MODE GITHUB_USERNAME GITHUB_TOKEN \
REPO_1_NAME REPO_2_NAME REPO_3_NAME
log_warn "=== Phase 8 Teardown: Cutover ==="
REPOS=("$REPO_1_NAME" "$REPO_2_NAME" "$REPO_3_NAME")
PHASE8_STATE_FILE="$(_project_root)/.manifests/phase8_github_repo_state.json"
github_pages_http_code() {
local repo="$1"
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${GITHUB_TOKEN}" \
-H "Accept: application/json" \
"https://api.github.com/repos/${GITHUB_USERNAME}/${repo}/pages" 2>/dev/null || echo "000"
}
# ---------------------------------------------------------------------------
# Step 1: Remove Nginx config and reload
# ---------------------------------------------------------------------------
if ssh_exec UNRAID "test -f '${NGINX_CONF_PATH}/gitea.conf'" 2>/dev/null; then
printf 'Remove Nginx config for %s? [y/N] ' "$GITEA_DOMAIN"
read -r confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
ssh_exec UNRAID "rm -f '${NGINX_CONF_PATH}/gitea.conf'"
ssh_exec UNRAID "docker exec ${NGINX_CONTAINER_NAME} nginx -s reload" || true
log_success "Nginx config removed and reloaded"
else
log_info "Nginx config preserved"
fi
else
log_info "Nginx config already removed"
fi
# ---------------------------------------------------------------------------
# Step 2: Remove cert renewal cron
# ---------------------------------------------------------------------------
if ssh_exec UNRAID "crontab -l 2>/dev/null | grep -q certbot" 2>/dev/null; then
ssh_exec UNRAID "crontab -l 2>/dev/null | grep -v certbot | crontab -"
log_success "Certbot renewal cron removed"
else
log_info "No certbot cron found"
fi
# ---------------------------------------------------------------------------
# Step 3: Optionally remove SSL certificates (letsencrypt only)
# ---------------------------------------------------------------------------
if [[ "$SSL_MODE" == "letsencrypt" ]]; then
if ssh_exec UNRAID "test -d '/etc/letsencrypt/live/${GITEA_DOMAIN}'" 2>/dev/null; then
printf 'Remove SSL certificates for %s? [y/N] ' "$GITEA_DOMAIN"
read -r confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
ssh_exec UNRAID "rm -rf '/etc/letsencrypt/live/${GITEA_DOMAIN}' '/etc/letsencrypt/archive/${GITEA_DOMAIN}' '/etc/letsencrypt/renewal/${GITEA_DOMAIN}.conf'"
log_success "SSL certificates removed"
else
log_info "SSL certificates preserved"
fi
fi
fi
# ---------------------------------------------------------------------------
# Step 4: Restore GitHub repos
# Primary path: restore from state snapshot written by phase8_cutover.sh.
# Fallback path: if snapshot is missing, restore description from "— was: ..."
# and use legacy defaults for homepage/wiki/projects.
# ---------------------------------------------------------------------------
printf 'Restore GitHub repo settings (description/homepage/wiki/projects/pages)? [y/N] '
read -r confirm
if [[ "$confirm" =~ ^[Yy]$ ]]; then
STATE_AVAILABLE=false
if [[ -f "$PHASE8_STATE_FILE" ]]; then
STATE_AVAILABLE=true
log_info "Using saved Phase 8 state from ${PHASE8_STATE_FILE}"
else
log_warn "No Phase 8 state file found — using fallback restore behavior"
fi
RESTORE_ERRORS=0
for repo in "${REPOS[@]}"; do
CURRENT_DESC=$(github_api GET "/repos/${GITHUB_USERNAME}/${repo}" 2>/dev/null | jq -r '.description // ""')
RESTORE_DESC=""
RESTORE_HOMEPAGE=""
RESTORE_HAS_WIKI=true
RESTORE_HAS_PROJECTS=true
RESTORE_PAGES_ENABLED=false
RESTORE_PAGES_CNAME=""
RESTORE_PAGES_BRANCH=""
RESTORE_PAGES_PATH="/"
if [[ "$STATE_AVAILABLE" == "true" ]] && jq -e --arg repo "$repo" 'has($repo)' "$PHASE8_STATE_FILE" >/dev/null 2>&1; then
REPO_STATE=$(jq -c --arg repo "$repo" '.[$repo]' "$PHASE8_STATE_FILE")
RESTORE_DESC=$(printf '%s' "$REPO_STATE" | jq -r '.description // ""')
RESTORE_HOMEPAGE=$(printf '%s' "$REPO_STATE" | jq -r '.homepage // ""')
RESTORE_HAS_WIKI=$(printf '%s' "$REPO_STATE" | jq -r '.has_wiki // true')
RESTORE_HAS_PROJECTS=$(printf '%s' "$REPO_STATE" | jq -r '.has_projects // true')
RESTORE_PAGES_ENABLED=$(printf '%s' "$REPO_STATE" | jq -r '.pages_enabled // false')
RESTORE_PAGES_CNAME=$(printf '%s' "$REPO_STATE" | jq -r '.pages_cname // ""')
RESTORE_PAGES_BRANCH=$(printf '%s' "$REPO_STATE" | jq -r '.pages_source_branch // ""')
RESTORE_PAGES_PATH=$(printf '%s' "$REPO_STATE" | jq -r '.pages_source_path // "/"')
else
if [[ "$CURRENT_DESC" != "[MIRROR]"* ]]; then
log_info "GitHub repo ${repo} not marked as mirror and no snapshot found — skipping"
continue
fi
if [[ "$CURRENT_DESC" == *" — was: "* ]]; then
RESTORE_DESC="${CURRENT_DESC##* — was: }"
fi
fi
# Restore description/homepage/wiki/projects
RESTORE_PAYLOAD=$(jq -n \
--arg description "$RESTORE_DESC" \
--arg homepage "$RESTORE_HOMEPAGE" \
--argjson has_wiki "$RESTORE_HAS_WIKI" \
--argjson has_projects "$RESTORE_HAS_PROJECTS" \
'{
description: $description,
homepage: $homepage,
has_wiki: $has_wiki,
has_projects: $has_projects
}')
if github_api PATCH "/repos/${GITHUB_USERNAME}/${repo}" "$RESTORE_PAYLOAD" >/dev/null 2>&1; then
log_success "Restored GitHub repo settings: ${repo}"
else
log_error "Failed to restore GitHub repo: ${repo}"
RESTORE_ERRORS=$((RESTORE_ERRORS + 1))
continue
fi
# Restore GitHub Pages state
CURRENT_PAGES_CODE=$(github_pages_http_code "$repo")
if [[ "$RESTORE_PAGES_ENABLED" == "true" ]]; then
if [[ -z "$RESTORE_PAGES_BRANCH" ]]; then
log_warn "Cannot restore Pages for ${repo}: missing source branch in saved state"
RESTORE_ERRORS=$((RESTORE_ERRORS + 1))
continue
fi
PAGES_PAYLOAD=$(jq -n \
--arg branch "$RESTORE_PAGES_BRANCH" \
--arg path "$RESTORE_PAGES_PATH" \
--arg cname "$RESTORE_PAGES_CNAME" \
'{
source: {
branch: $branch,
path: $path
}
} + (if $cname != "" then {cname: $cname} else {} end)')
if [[ "$CURRENT_PAGES_CODE" == "200" ]]; then
if github_api PUT "/repos/${GITHUB_USERNAME}/${repo}/pages" "$PAGES_PAYLOAD" >/dev/null 2>&1; then
log_success "Restored GitHub Pages config for ${repo}"
else
log_warn "Failed to update GitHub Pages config for ${repo}"
RESTORE_ERRORS=$((RESTORE_ERRORS + 1))
fi
else
if github_api POST "/repos/${GITHUB_USERNAME}/${repo}/pages" "$PAGES_PAYLOAD" >/dev/null 2>&1; then
log_success "Recreated GitHub Pages for ${repo}"
else
log_warn "Failed to recreate GitHub Pages for ${repo}"
RESTORE_ERRORS=$((RESTORE_ERRORS + 1))
fi
fi
else
if [[ "$CURRENT_PAGES_CODE" == "200" ]]; then
if github_api DELETE "/repos/${GITHUB_USERNAME}/${repo}/pages" >/dev/null 2>&1; then
log_info "Disabled GitHub Pages for ${repo} (matches pre-cutover state)"
else
log_warn "Failed to disable GitHub Pages for ${repo}"
RESTORE_ERRORS=$((RESTORE_ERRORS + 1))
fi
else
log_info "GitHub Pages already disabled for ${repo}"
fi
fi
done
if [[ "$STATE_AVAILABLE" == "true" ]]; then
if [[ "$RESTORE_ERRORS" -eq 0 ]]; then
rm -f "$PHASE8_STATE_FILE"
log_info "Removed saved Phase 8 state file after successful restore"
else
log_warn "Keeping Phase 8 state file due to restore errors: ${PHASE8_STATE_FILE}"
fi
fi
else
log_info "GitHub repos left as-is"
fi
log_success "Phase 8 teardown complete"