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>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
set -euo pipefail
|
||||
|
||||
# =============================================================================
|
||||
# phase8_cutover.sh — HTTPS via Nginx + Archive GitHub repos
|
||||
# phase8_cutover.sh — HTTPS via Nginx + Mark GitHub repos as mirrors
|
||||
# Depends on: All prior phases complete, Nginx running on Unraid
|
||||
# This is the "go live" script — after this, Gitea is the primary git host.
|
||||
#
|
||||
@@ -14,7 +14,7 @@ set -euo pipefail
|
||||
# "letsencrypt" → auto-provision via Certbot container
|
||||
# "existing" → user provides cert paths (SSL_CERT_PATH, SSL_KEY_PATH)
|
||||
#
|
||||
# After HTTPS is live, GitHub repos are archived with a "[MOVED]" description.
|
||||
# After HTTPS is live, GitHub repos are marked as offsite mirrors.
|
||||
# =============================================================================
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
@@ -27,9 +27,11 @@ require_vars UNRAID_IP UNRAID_SSH_USER UNRAID_GITEA_PORT \
|
||||
SSL_MODE GITHUB_USERNAME GITHUB_TOKEN \
|
||||
REPO_1_NAME REPO_2_NAME REPO_3_NAME
|
||||
|
||||
phase_header 8 "Cutover (HTTPS + Archive GitHub)"
|
||||
phase_header 8 "Cutover (HTTPS + Mark GitHub Mirrors)"
|
||||
|
||||
REPOS=("$REPO_1_NAME" "$REPO_2_NAME" "$REPO_3_NAME")
|
||||
PHASE8_STATE_DIR="$(_project_root)/.manifests"
|
||||
PHASE8_STATE_FILE="${PHASE8_STATE_DIR}/phase8_github_repo_state.json"
|
||||
|
||||
# Validate SSL_MODE
|
||||
if [[ "$SSL_MODE" != "letsencrypt" ]] && [[ "$SSL_MODE" != "existing" ]]; then
|
||||
@@ -43,6 +45,93 @@ elif [[ "$SSL_MODE" == "existing" ]]; then
|
||||
require_vars SSL_CERT_PATH SSL_KEY_PATH
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: persist original GitHub repo settings for teardown symmetry
|
||||
# ---------------------------------------------------------------------------
|
||||
init_phase8_state_store() {
|
||||
mkdir -p "$PHASE8_STATE_DIR"
|
||||
if [[ ! -f "$PHASE8_STATE_FILE" ]]; then
|
||||
printf '{}\n' > "$PHASE8_STATE_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
fetch_github_pages_state() {
|
||||
local repo="$1"
|
||||
local tmpfile http_code
|
||||
local pages_enabled=false
|
||||
local pages_cname=""
|
||||
local pages_source_branch=""
|
||||
local pages_source_path="/"
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
http_code=$(curl -s \
|
||||
-H "Authorization: token ${GITHUB_TOKEN}" \
|
||||
-H "Accept: application/json" \
|
||||
-o "$tmpfile" \
|
||||
-w "%{http_code}" \
|
||||
"https://api.github.com/repos/${GITHUB_USERNAME}/${repo}/pages" || echo "000")
|
||||
|
||||
if [[ "$http_code" == "200" ]]; then
|
||||
pages_enabled=true
|
||||
pages_cname=$(jq -r '.cname // ""' "$tmpfile")
|
||||
pages_source_branch=$(jq -r '.source.branch // ""' "$tmpfile")
|
||||
pages_source_path=$(jq -r '.source.path // "/"' "$tmpfile")
|
||||
fi
|
||||
rm -f "$tmpfile"
|
||||
|
||||
jq -n \
|
||||
--argjson pages_enabled "$pages_enabled" \
|
||||
--arg pages_cname "$pages_cname" \
|
||||
--arg pages_source_branch "$pages_source_branch" \
|
||||
--arg pages_source_path "$pages_source_path" \
|
||||
'{
|
||||
pages_enabled: $pages_enabled,
|
||||
pages_cname: $pages_cname,
|
||||
pages_source_branch: $pages_source_branch,
|
||||
pages_source_path: $pages_source_path
|
||||
}'
|
||||
}
|
||||
|
||||
snapshot_repo_state() {
|
||||
local repo="$1"
|
||||
local repo_data="$2"
|
||||
|
||||
init_phase8_state_store
|
||||
|
||||
if jq -e --arg repo "$repo" 'has($repo)' "$PHASE8_STATE_FILE" >/dev/null 2>&1; then
|
||||
log_info "State snapshot already exists for ${repo} — preserving original values"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local current_desc current_homepage current_has_wiki current_has_projects
|
||||
local pages_state tmpfile
|
||||
|
||||
current_desc=$(printf '%s' "$repo_data" | jq -r '.description // ""')
|
||||
current_homepage=$(printf '%s' "$repo_data" | jq -r '.homepage // ""')
|
||||
current_has_wiki=$(printf '%s' "$repo_data" | jq -r '.has_wiki // false')
|
||||
current_has_projects=$(printf '%s' "$repo_data" | jq -r '.has_projects // false')
|
||||
pages_state=$(fetch_github_pages_state "$repo")
|
||||
|
||||
tmpfile=$(mktemp)
|
||||
jq \
|
||||
--arg repo "$repo" \
|
||||
--arg description "$current_desc" \
|
||||
--arg homepage "$current_homepage" \
|
||||
--argjson has_wiki "$current_has_wiki" \
|
||||
--argjson has_projects "$current_has_projects" \
|
||||
--argjson pages "$pages_state" \
|
||||
'.[$repo] = {
|
||||
description: $description,
|
||||
homepage: $homepage,
|
||||
has_wiki: $has_wiki,
|
||||
has_projects: $has_projects
|
||||
} + $pages' \
|
||||
"$PHASE8_STATE_FILE" > "$tmpfile"
|
||||
mv "$tmpfile" "$PHASE8_STATE_FILE"
|
||||
|
||||
log_info "Saved pre-cutover GitHub settings for ${repo}"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper: render Nginx config in HTTP-only or HTTPS mode
|
||||
# Uses the template + sed to strip/modify marker blocks.
|
||||
@@ -273,11 +362,12 @@ fi
|
||||
# Disables wiki and Pages to avoid unnecessary resource usage.
|
||||
# Does NOT archive — archived repos reject pushes, which would break
|
||||
# the push mirrors configured in Phase 6.
|
||||
# Preserves original description by appending after "— was: ".
|
||||
# Persists original mutable settings to a local state file for teardown.
|
||||
# GitHub Actions already disabled in Phase 6 Step D.
|
||||
# ---------------------------------------------------------------------------
|
||||
log_step 11 "Marking GitHub repos as offsite backup..."
|
||||
|
||||
init_phase8_state_store
|
||||
for repo in "${REPOS[@]}"; do
|
||||
# Fetch repo metadata (single API call)
|
||||
REPO_DATA=$(github_api GET "/repos/${GITHUB_USERNAME}/${repo}" 2>/dev/null || echo "{}")
|
||||
@@ -285,10 +375,17 @@ for repo in "${REPOS[@]}"; do
|
||||
|
||||
# Skip if already marked
|
||||
if [[ "$CURRENT_DESC" == "[MIRROR]"* ]]; then
|
||||
if ! jq -e --arg repo "$repo" 'has($repo)' "$PHASE8_STATE_FILE" >/dev/null 2>&1; then
|
||||
log_warn "GitHub repo ${repo} already marked as mirror but no local state snapshot exists"
|
||||
log_warn " → Teardown may not fully restore pre-cutover settings for this repo"
|
||||
fi
|
||||
log_info "GitHub repo ${repo} already marked as mirror — skipping"
|
||||
continue
|
||||
fi
|
||||
|
||||
# Snapshot current mutable state so teardown can restore exactly.
|
||||
snapshot_repo_state "$repo" "$REPO_DATA"
|
||||
|
||||
# Build new description preserving original
|
||||
NEW_DESC="[MIRROR] Offsite backup — primary at https://${GITEA_DOMAIN}/${GITEA_ORG_NAME}/${repo}"
|
||||
if [[ -n "$CURRENT_DESC" ]]; then
|
||||
|
||||
Reference in New Issue
Block a user