diff --git a/post-migration-check.sh b/post-migration-check.sh new file mode 100755 index 0000000..448d7b7 --- /dev/null +++ b/post-migration-check.sh @@ -0,0 +1,663 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# post-migration-check.sh — Read-only infrastructure state check +# Probes live infrastructure and reports the state of every migration phase. +# No mutations — purely diagnostic. Safe to run at any time. +# +# Three states: +# [DONE] — already exists/running, phase would skip this step +# [TODO] — not done yet, phase would execute this step +# [ERROR] — something is broken (unreachable, invalid token, misconfigured) +# +# Only [ERROR] means something is wrong. [TODO] is normal for phases not yet run. +# Exit code: 0 if no errors, 1 if any [ERROR] found. +# ============================================================================= + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +source "${SCRIPT_DIR}/lib/common.sh" + +load_env + +# --------------------------------------------------------------------------- +# Counters +# --------------------------------------------------------------------------- +TOTAL_DONE=0 +TOTAL_TODO=0 +TOTAL_ERROR=0 + +# Per-phase counters (parallel arrays indexed by phase number 0-9, where 0=connectivity) +declare -a PHASE_DONE PHASE_TODO PHASE_ERROR PHASE_TOTAL +for i in $(seq 0 9); do + PHASE_DONE[$i]=0 + PHASE_TODO[$i]=0 + PHASE_ERROR[$i]=0 + PHASE_TOTAL[$i]=0 +done + +# Current phase being checked (set by section_header) +CURRENT_PHASE=0 + +# --------------------------------------------------------------------------- +# Output helpers +# --------------------------------------------------------------------------- +check_done() { + local msg="$1" + printf ' %b[DONE]%b %s\n' "$_C_GREEN" "$_C_RESET" "$msg" >&2 + TOTAL_DONE=$((TOTAL_DONE + 1)) + PHASE_DONE[$CURRENT_PHASE]=$((PHASE_DONE[CURRENT_PHASE] + 1)) + PHASE_TOTAL[$CURRENT_PHASE]=$((PHASE_TOTAL[CURRENT_PHASE] + 1)) +} + +check_todo() { + local msg="$1" + printf ' %b[TODO]%b %s\n' "$_C_YELLOW" "$_C_RESET" "$msg" >&2 + TOTAL_TODO=$((TOTAL_TODO + 1)) + PHASE_TODO[$CURRENT_PHASE]=$((PHASE_TODO[CURRENT_PHASE] + 1)) + PHASE_TOTAL[$CURRENT_PHASE]=$((PHASE_TOTAL[CURRENT_PHASE] + 1)) +} + +check_error() { + local msg="$1" + printf ' %b[ERROR]%b %s\n' "$_C_RED" "$_C_RESET" "$msg" >&2 + TOTAL_ERROR=$((TOTAL_ERROR + 1)) + PHASE_ERROR[$CURRENT_PHASE]=$((PHASE_ERROR[CURRENT_PHASE] + 1)) + PHASE_TOTAL[$CURRENT_PHASE]=$((PHASE_TOTAL[CURRENT_PHASE] + 1)) +} + +section_header() { + local phase_num="$1" title="$2" + CURRENT_PHASE="$phase_num" + printf '\n%b--- %s ---%b\n' "$_C_BOLD" "$title" "$_C_RESET" >&2 +} + +# --------------------------------------------------------------------------- +# Connectivity gates — track which hosts are reachable +# Later sections skip checks for unreachable hosts rather than spamming errors. +# --------------------------------------------------------------------------- +UNRAID_SSH_OK=false +FEDORA_SSH_OK=false +UNRAID_DOCKER_OK=false +FEDORA_DOCKER_OK=false +GITHUB_API_OK=false +GITEA_API_OK=false +GITEA_BACKUP_API_OK=false + +printf '\n%b=== Post-Migration Check ===%b\n' "$_C_BOLD" "$_C_RESET" >&2 + +# --------------------------------------------------------------------------- +# Connectivity +# --------------------------------------------------------------------------- +section_header 0 "Connectivity" + +# SSH to Unraid +if ssh_exec UNRAID "true" 2>/dev/null; then + check_done "SSH to Unraid (${UNRAID_IP:-})" + UNRAID_SSH_OK=true +else + check_error "SSH to Unraid (${UNRAID_IP:-}) — connection failed" +fi + +# SSH to Fedora +if ssh_exec FEDORA "true" 2>/dev/null; then + check_done "SSH to Fedora (${FEDORA_IP:-})" + FEDORA_SSH_OK=true +else + check_error "SSH to Fedora (${FEDORA_IP:-}) — connection failed" +fi + +# Docker on Unraid +if $UNRAID_SSH_OK; then + if ssh_exec UNRAID "docker info" &>/dev/null; then + check_done "Docker daemon on Unraid" + UNRAID_DOCKER_OK=true + else + check_error "Docker daemon on Unraid — not running" + fi +fi + +# Docker on Fedora +if $FEDORA_SSH_OK; then + if ssh_exec FEDORA "docker info" &>/dev/null; then + check_done "Docker daemon on Fedora" + FEDORA_DOCKER_OK=true + else + check_error "Docker daemon on Fedora — not running" + fi +fi + +# GitHub API +if [[ -n "${GITHUB_TOKEN:-}" ]]; then + GH_USER_RESPONSE=$(curl -sf -H "Authorization: token ${GITHUB_TOKEN}" \ + "https://api.github.com/user" 2>/dev/null || echo "") + if [[ -n "$GH_USER_RESPONSE" ]]; then + GH_ACTUAL_USER=$(printf '%s' "$GH_USER_RESPONSE" | jq -r '.login // ""' 2>/dev/null) + if [[ "$GH_ACTUAL_USER" == "${GITHUB_USERNAME:-}" ]]; then + check_done "GitHub API — token valid for ${GITHUB_USERNAME}" + GITHUB_API_OK=true + elif [[ -n "$GH_ACTUAL_USER" ]]; then + check_error "GitHub API — token belongs to '${GH_ACTUAL_USER}', expected '${GITHUB_USERNAME:-}'" + else + check_error "GitHub API — token returned unexpected response" + fi + else + check_error "GitHub API — request failed (bad token or no internet)" + fi +else + check_error "GitHub API — GITHUB_TOKEN not set" +fi + +# --------------------------------------------------------------------------- +# Phase 1: Gitea on Unraid +# --------------------------------------------------------------------------- +section_header 1 "Phase 1: Gitea on Unraid" + +if ! $UNRAID_DOCKER_OK; then + check_error "Cannot check — Unraid Docker unreachable" +else + # Docker network + if ssh_exec UNRAID "docker network inspect br0" &>/dev/null; then + check_done "Docker network br0 exists" + else + check_todo "Docker network br0 — would create" + fi + + # docker-compose.yml + COMPOSE_DIR="${UNRAID_COMPOSE_DIR:-}/gitea" + if ssh_exec UNRAID "test -f '${COMPOSE_DIR}/docker-compose.yml'" 2>/dev/null; then + check_done "docker-compose.yml deployed at ${COMPOSE_DIR}" + else + check_todo "docker-compose.yml — would deploy to ${COMPOSE_DIR}" + fi + + # app.ini + DATA_PATH="${UNRAID_GITEA_DATA_PATH:-}" + if ssh_exec UNRAID "test -f '${DATA_PATH}/config/app.ini'" 2>/dev/null; then + check_done "app.ini deployed" + else + check_todo "app.ini — would deploy" + fi + + # Gitea container running + healthy + CONTAINER_STATUS=$(ssh_exec UNRAID "docker ps --filter name='^/gitea$' --format '{{.Status}}'" 2>/dev/null || true) + if [[ "$CONTAINER_STATUS" == *"Up"* ]]; then + if [[ "$CONTAINER_STATUS" == *"healthy"* ]]; then + check_done "Gitea container running (healthy)" + else + check_done "Gitea container running (${CONTAINER_STATUS})" + fi + else + check_todo "Gitea container — would start" + fi + + # Gitea HTTP responds + if [[ -n "${GITEA_INTERNAL_URL:-}" ]]; then + VERSION_RESPONSE=$(curl -sf "${GITEA_INTERNAL_URL}/api/v1/version" 2>/dev/null || echo "") + if [[ -n "$VERSION_RESPONSE" ]]; then + ACTUAL_VERSION=$(printf '%s' "$VERSION_RESPONSE" | jq -r '.version // ""' 2>/dev/null) + if [[ -n "$ACTUAL_VERSION" ]]; then + check_done "Gitea API responds (v${ACTUAL_VERSION})" + GITEA_API_OK=true + else + check_error "Gitea API — unexpected response format" + fi + else + check_todo "Gitea API — not responding (container not started?)" + fi + fi + + # Admin auth + if $GITEA_API_OK && [[ -n "${GITEA_ADMIN_USER:-}" ]] && [[ -n "${GITEA_ADMIN_PASSWORD:-}" ]]; then + ADMIN_RESPONSE=$(curl -sf -u "${GITEA_ADMIN_USER}:${GITEA_ADMIN_PASSWORD}" \ + "${GITEA_INTERNAL_URL}/api/v1/user" 2>/dev/null || echo "") + if [[ -n "$ADMIN_RESPONSE" ]]; then + ADMIN_LOGIN=$(printf '%s' "$ADMIN_RESPONSE" | jq -r '.login // ""' 2>/dev/null) + if [[ "$ADMIN_LOGIN" == "${GITEA_ADMIN_USER}" ]]; then + check_done "Admin user '${GITEA_ADMIN_USER}' credentials valid" + else + check_error "Admin auth — returned user '${ADMIN_LOGIN}', expected '${GITEA_ADMIN_USER}'" + fi + else + check_todo "Admin user — would create" + fi + fi + + # API token + if $GITEA_API_OK && [[ -n "${GITEA_ADMIN_TOKEN:-}" ]]; then + TOKEN_RESPONSE=$(curl -sf -H "Authorization: token ${GITEA_ADMIN_TOKEN}" \ + "${GITEA_INTERNAL_URL}/api/v1/user" 2>/dev/null || echo "") + if [[ -n "$TOKEN_RESPONSE" ]]; then + TOKEN_USER=$(printf '%s' "$TOKEN_RESPONSE" | jq -r '.login // ""' 2>/dev/null) + if [[ "$TOKEN_USER" == "${GITEA_ADMIN_USER:-}" ]]; then + check_done "API token valid for '${GITEA_ADMIN_USER}'" + else + check_error "API token — returned user '${TOKEN_USER}', expected '${GITEA_ADMIN_USER:-}'" + fi + else + check_error "API token — rejected by Gitea" + fi + elif $GITEA_API_OK; then + check_todo "API token — would generate" + fi + + # Organization + if $GITEA_API_OK && [[ -n "${GITEA_ADMIN_TOKEN:-}" ]]; then + if gitea_api GET "/orgs/${GITEA_ORG_NAME:-}" >/dev/null 2>&1; then + check_done "Organization '${GITEA_ORG_NAME}' exists" + else + check_todo "Organization '${GITEA_ORG_NAME}' — would create" + fi + fi +fi + +# --------------------------------------------------------------------------- +# Phase 2: Gitea on Fedora +# --------------------------------------------------------------------------- +section_header 2 "Phase 2: Gitea on Fedora" + +if ! $FEDORA_DOCKER_OK; then + check_error "Cannot check — Fedora Docker unreachable" +else + # Docker network + FEDORA_NETWORK="${FEDORA_MACVLAN_PARENT:-gitea_net}" + if ssh_exec FEDORA "docker network inspect gitea_net" &>/dev/null; then + check_done "Docker network gitea_net exists" + else + check_todo "Docker network gitea_net — would create" + fi + + # docker-compose.yml + FEDORA_COMPOSE="${FEDORA_COMPOSE_DIR:-}/gitea" + if ssh_exec FEDORA "test -f '${FEDORA_COMPOSE}/docker-compose.yml'" 2>/dev/null; then + check_done "docker-compose.yml deployed at ${FEDORA_COMPOSE}" + else + check_todo "docker-compose.yml — would deploy to ${FEDORA_COMPOSE}" + fi + + # app.ini + FEDORA_DATA="${FEDORA_GITEA_DATA_PATH:-}" + if ssh_exec FEDORA "test -f '${FEDORA_DATA}/config/app.ini'" 2>/dev/null; then + check_done "app.ini deployed" + else + check_todo "app.ini — would deploy" + fi + + # Gitea container running + healthy + FEDORA_CONTAINER=$(ssh_exec FEDORA "docker ps --filter name='^/gitea$' --format '{{.Status}}'" 2>/dev/null || true) + if [[ "$FEDORA_CONTAINER" == *"Up"* ]]; then + if [[ "$FEDORA_CONTAINER" == *"healthy"* ]]; then + check_done "Gitea container running (healthy)" + else + check_done "Gitea container running (${FEDORA_CONTAINER})" + fi + else + check_todo "Gitea container — would start" + fi + + # Gitea HTTP responds + if [[ -n "${GITEA_BACKUP_INTERNAL_URL:-}" ]]; then + BACKUP_VERSION=$(curl -sf "${GITEA_BACKUP_INTERNAL_URL}/api/v1/version" 2>/dev/null || echo "") + if [[ -n "$BACKUP_VERSION" ]]; then + BACKUP_VER=$(printf '%s' "$BACKUP_VERSION" | jq -r '.version // ""' 2>/dev/null) + check_done "Gitea backup API responds (v${BACKUP_VER})" + GITEA_BACKUP_API_OK=true + else + check_todo "Gitea backup API — not responding" + fi + fi + + # Backup API token + if $GITEA_BACKUP_API_OK && [[ -n "${GITEA_BACKUP_ADMIN_TOKEN:-}" ]]; then + BACKUP_TOKEN_RESP=$(curl -sf -H "Authorization: token ${GITEA_BACKUP_ADMIN_TOKEN}" \ + "${GITEA_BACKUP_INTERNAL_URL}/api/v1/user" 2>/dev/null || echo "") + if [[ -n "$BACKUP_TOKEN_RESP" ]]; then + check_done "Backup API token valid" + else + check_error "Backup API token — rejected by Gitea" + fi + elif $GITEA_BACKUP_API_OK; then + check_todo "Backup API token — would generate" + fi + + # Cross-host connectivity: Fedora → Unraid + if $FEDORA_SSH_OK && [[ -n "${UNRAID_GITEA_IP:-}" ]]; then + if ssh_exec FEDORA "curl -sf -o /dev/null http://${UNRAID_GITEA_IP}:3000/api/v1/version" 2>/dev/null; then + check_done "Fedora can reach Unraid Gitea (${UNRAID_GITEA_IP}:3000)" + else + if $GITEA_API_OK; then + check_error "Fedora cannot reach Unraid Gitea at ${UNRAID_GITEA_IP}:3000" + else + check_todo "Fedora → Unraid connectivity (Unraid Gitea not running)" + fi + fi + fi +fi + +# --------------------------------------------------------------------------- +# Phase 3: Runners +# --------------------------------------------------------------------------- +section_header 3 "Phase 3: Runners" + +RUNNERS_CONF="$(_project_root)/runners.conf" +if [[ -f "$RUNNERS_CONF" ]]; then + RUNNER_COUNT=$(ini_list_sections "$RUNNERS_CONF" | wc -l | xargs) + check_done "runners.conf exists (${RUNNER_COUNT} runner(s) defined)" +else + check_todo "runners.conf — not found" +fi + +# Registration token +if [[ -n "${GITEA_RUNNER_REGISTRATION_TOKEN:-}" ]]; then + check_done "Runner registration token in .env" +else + if $GITEA_API_OK; then + check_todo "Runner registration token — would fetch from Gitea" + else + check_todo "Runner registration token — Gitea not available to fetch" + fi +fi + +# Check each runner's status via Gitea admin API +if $GITEA_API_OK && [[ -n "${GITEA_ADMIN_TOKEN:-}" ]] && [[ -f "$RUNNERS_CONF" ]]; then + # Get all registered runners from Gitea + API_RUNNERS=$(gitea_api GET "/admin/runners" 2>/dev/null || echo "[]") + + while IFS= read -r runner_name; do + [[ -z "$runner_name" ]] && continue + RUNNER_HOST=$(ini_get "$RUNNERS_CONF" "$runner_name" "host" "") + + # Search for this runner in API response + RUNNER_STATUS=$(printf '%s' "$API_RUNNERS" | jq -r --arg n "$runner_name" \ + '.[] | select(.name == $n) | .status // "unknown"' 2>/dev/null || echo "") + + if [[ -z "$RUNNER_STATUS" ]]; then + # Try matching by label substring if exact name match fails + check_todo "Runner '${runner_name}' (${RUNNER_HOST:-local}) — not registered" + elif [[ "$RUNNER_STATUS" == "online" ]]; then + check_done "Runner '${runner_name}' (${RUNNER_HOST:-local}) — online" + else + check_error "Runner '${runner_name}' (${RUNNER_HOST:-local}) — ${RUNNER_STATUS}" + fi + done < <(ini_list_sections "$RUNNERS_CONF") +fi + +# --------------------------------------------------------------------------- +# Phase 4: Migrate Repos +# --------------------------------------------------------------------------- +section_header 4 "Phase 4: Migrate Repos" + +read -ra REPOS <<< "${REPO_NAMES:-}" + +if [[ ${#REPOS[@]} -eq 0 ]]; then + check_error "REPO_NAMES is empty — no repos to migrate" +else + for repo in "${REPOS[@]}"; do + # GitHub source accessible + if $GITHUB_API_OK; then + GH_REPO_RESP=$(curl -sf -H "Authorization: token ${GITHUB_TOKEN}" \ + "https://api.github.com/repos/${GITHUB_USERNAME}/${repo}" 2>/dev/null || echo "") + if [[ -n "$GH_REPO_RESP" ]]; then + check_done "GitHub source: ${GITHUB_USERNAME}/${repo} accessible" + else + check_error "GitHub source: ${GITHUB_USERNAME}/${repo} — not found or no access" + fi + fi + + # Gitea primary + if $GITEA_API_OK && [[ -n "${GITEA_ADMIN_TOKEN:-}" ]]; then + GITEA_REPO_RESP=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}" 2>/dev/null || echo "") + if [[ -n "$GITEA_REPO_RESP" ]]; then + check_done "Gitea primary: ${GITEA_ORG_NAME}/${repo} exists" + else + check_todo "Gitea primary: ${GITEA_ORG_NAME}/${repo} — would migrate" + fi + fi + + # Fedora mirror + if $GITEA_BACKUP_API_OK && [[ -n "${GITEA_BACKUP_ADMIN_TOKEN:-}" ]]; then + BACKUP_REPO_RESP=$(gitea_backup_api GET "/repos/${GITEA_ADMIN_USER}/${repo}" 2>/dev/null || echo "") + if [[ -n "$BACKUP_REPO_RESP" ]]; then + IS_MIRROR=$(printf '%s' "$BACKUP_REPO_RESP" | jq -r '.mirror // false' 2>/dev/null) + if [[ "$IS_MIRROR" == "true" ]]; then + check_done "Fedora mirror: ${GITEA_ADMIN_USER}/${repo} (pull mirror active)" + else + check_done "Fedora mirror: ${GITEA_ADMIN_USER}/${repo} exists (not a mirror)" + fi + else + check_todo "Fedora mirror: ${GITEA_ADMIN_USER}/${repo} — would create" + fi + fi + done +fi + +# --------------------------------------------------------------------------- +# Phase 5: Migrate Pipelines +# --------------------------------------------------------------------------- +section_header 5 "Phase 5: Migrate Pipelines" + +if $GITEA_API_OK && [[ -n "${GITEA_ADMIN_TOKEN:-}" ]]; then + for repo in "${REPOS[@]}"; do + WORKFLOWS_RESP=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/contents/.gitea/workflows" 2>/dev/null || echo "") + if [[ -n "$WORKFLOWS_RESP" ]] && [[ "$WORKFLOWS_RESP" != "null" ]]; then + WORKFLOW_COUNT=$(printf '%s' "$WORKFLOWS_RESP" | jq 'length' 2>/dev/null || echo 0) + check_done "${repo}: .gitea/workflows/ exists (${WORKFLOW_COUNT} file(s))" + else + # Check if source repo has GitHub Actions workflows + if $GITHUB_API_OK; then + GH_WORKFLOWS=$(curl -sf -H "Authorization: token ${GITHUB_TOKEN}" \ + "https://api.github.com/repos/${GITHUB_USERNAME}/${repo}/contents/.github/workflows" 2>/dev/null || echo "") + if [[ -n "$GH_WORKFLOWS" ]] && [[ "$GH_WORKFLOWS" != *"Not Found"* ]]; then + check_todo "${repo}: has GitHub workflows — would migrate" + else + check_done "${repo}: no workflows in source — nothing to migrate" + fi + else + check_todo "${repo}: .gitea/workflows/ — would check" + fi + fi + done +else + check_todo "Pipeline checks — Gitea API not available" +fi + +# --------------------------------------------------------------------------- +# Phase 6: GitHub Mirrors +# --------------------------------------------------------------------------- +section_header 6 "Phase 6: GitHub Mirrors" + +if $GITEA_API_OK && [[ -n "${GITEA_ADMIN_TOKEN:-}" ]]; then + for repo in "${REPOS[@]}"; do + # Push mirror configured + MIRROR_RESP=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/push_mirrors" 2>/dev/null || echo "[]") + MIRROR_COUNT=$(printf '%s' "$MIRROR_RESP" | jq 'length' 2>/dev/null || echo 0) + if [[ "$MIRROR_COUNT" -gt 0 ]]; then + check_done "${repo}: push mirror configured" + else + check_todo "${repo}: push mirror — would configure" + fi + + # GitHub Actions disabled + if $GITHUB_API_OK; then + ACTIONS_RESP=$(curl -sf -H "Authorization: token ${GITHUB_TOKEN}" \ + "https://api.github.com/repos/${GITHUB_USERNAME}/${repo}/actions/permissions" 2>/dev/null || echo "") + if [[ -n "$ACTIONS_RESP" ]]; then + ACTIONS_ENABLED=$(printf '%s' "$ACTIONS_RESP" | jq -r '.enabled // true' 2>/dev/null) + if [[ "$ACTIONS_ENABLED" == "false" ]]; then + check_done "${repo}: GitHub Actions disabled" + else + check_todo "${repo}: GitHub Actions — would disable" + fi + fi + fi + done +else + check_todo "Mirror checks — Gitea API not available" +fi + +# --------------------------------------------------------------------------- +# Phase 7: Branch Protection +# --------------------------------------------------------------------------- +section_header 7 "Phase 7: Branch Protection" + +BRANCH="${PROTECTED_BRANCH:-main}" + +if $GITEA_API_OK && [[ -n "${GITEA_ADMIN_TOKEN:-}" ]]; then + for repo in "${REPOS[@]}"; do + BP_RESP=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/branch_protections/${BRANCH}" 2>/dev/null || echo "") + if [[ -n "$BP_RESP" ]]; then + APPROVALS=$(printf '%s' "$BP_RESP" | jq -r '.required_approvals // 0' 2>/dev/null) + check_done "${repo}: branch protection on '${BRANCH}' (${APPROVALS} approval(s) required)" + else + check_todo "${repo}: branch protection on '${BRANCH}' — would create" + fi + done +else + check_todo "Branch protection checks — Gitea API not available" +fi + +# --------------------------------------------------------------------------- +# Phase 8: Cutover (HTTPS + GitHub mirror marking) +# --------------------------------------------------------------------------- +section_header 8 "Phase 8: Cutover" + +# DNS resolution +if command -v dig &>/dev/null; then + DNS_RESULT=$(dig +short "${GITEA_DOMAIN:-}" 2>/dev/null | head -1) +elif command -v host &>/dev/null; then + DNS_RESULT=$(host "${GITEA_DOMAIN:-}" 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+' | head -1) +else + DNS_RESULT="" +fi + +if [[ -n "$DNS_RESULT" ]]; then + check_done "DNS: ${GITEA_DOMAIN} resolves to ${DNS_RESULT}" +else + check_todo "DNS: ${GITEA_DOMAIN} — does not resolve" +fi + +# Caddy container +if $UNRAID_DOCKER_OK; then + CADDY_STATUS=$(ssh_exec UNRAID "docker ps --filter name=caddy --format '{{.Status}}'" 2>/dev/null || true) + if [[ "$CADDY_STATUS" == *"Up"* ]]; then + check_done "Caddy container running" + else + check_todo "Caddy container — would start" + fi +fi + +# HTTPS end-to-end +HTTPS_RESP=$(curl -sf -o /dev/null -w "%{http_code}" "https://${GITEA_DOMAIN:-}/api/v1/version" 2>/dev/null || echo "000") +if [[ "$HTTPS_RESP" == "200" ]]; then + check_done "HTTPS end-to-end: https://${GITEA_DOMAIN} works" +else + check_todo "HTTPS: https://${GITEA_DOMAIN} — not responding (HTTP ${HTTPS_RESP})" +fi + +# TLS certificate validity +TLS_INFO=$(curl -vI "https://${GITEA_DOMAIN:-}/" 2>&1 || true) +if printf '%s' "$TLS_INFO" | grep -q "SSL certificate verify ok" 2>/dev/null; then + check_done "TLS certificate valid" +elif printf '%s' "$TLS_INFO" | grep -q "SSL certificate" 2>/dev/null; then + check_error "TLS certificate — verification failed" +else + if [[ "$HTTPS_RESP" == "200" ]]; then + check_done "TLS certificate present (HTTPS working)" + else + check_todo "TLS certificate — HTTPS not active yet" + fi +fi + +# Cloudflare token (if applicable) +if [[ "${TLS_MODE:-}" == "cloudflare" ]] && [[ -n "${CLOUDFLARE_API_TOKEN:-}" ]]; then + CF_VERIFY=$(curl -sf -H "Authorization: Bearer ${CLOUDFLARE_API_TOKEN}" \ + "https://api.cloudflare.com/client/v4/user/tokens/verify" 2>/dev/null || echo "") + if printf '%s' "$CF_VERIFY" | jq -e '.success == true' &>/dev/null; then + check_done "Cloudflare API token valid" + else + check_error "Cloudflare API token — verification failed" + fi +elif [[ "${TLS_MODE:-}" == "cloudflare" ]]; then + check_error "Cloudflare API token — CLOUDFLARE_API_TOKEN not set" +fi + +# GitHub repos marked as mirror +if $GITHUB_API_OK; then + for repo in "${REPOS[@]}"; do + GH_DESC=$(curl -sf -H "Authorization: token ${GITHUB_TOKEN}" \ + "https://api.github.com/repos/${GITHUB_USERNAME}/${repo}" 2>/dev/null \ + | jq -r '.description // ""' 2>/dev/null || echo "") + if [[ "$GH_DESC" == "[MIRROR]"* ]]; then + check_done "${repo}: GitHub description marked as [MIRROR]" + else + check_todo "${repo}: GitHub description — would mark as [MIRROR]" + fi + done +fi + +# --------------------------------------------------------------------------- +# Phase 9: Security +# --------------------------------------------------------------------------- +section_header 9 "Phase 9: Security" + +if $GITEA_API_OK && [[ -n "${GITEA_ADMIN_TOKEN:-}" ]]; then + for repo in "${REPOS[@]}"; do + SEC_RESP=$(gitea_api GET "/repos/${GITEA_ORG_NAME}/${repo}/contents/.gitea/workflows/security-scan.yml" 2>/dev/null || echo "") + if [[ -n "$SEC_RESP" ]] && [[ "$SEC_RESP" != "null" ]]; then + check_done "${repo}: security-scan.yml exists" + else + check_todo "${repo}: security-scan.yml — would deploy" + fi + done +else + check_todo "Security checks — Gitea API not available" +fi + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +PHASE_NAMES=( + "Connectivity" + "Phase 1: Gitea on Unraid" + "Phase 2: Gitea on Fedora" + "Phase 3: Runners" + "Phase 4: Migrate Repos" + "Phase 5: Migrate Pipelines" + "Phase 6: GitHub Mirrors" + "Phase 7: Branch Protection" + "Phase 8: Cutover" + "Phase 9: Security" +) + +printf '\n%b=== Summary ===%b\n' "$_C_BOLD" "$_C_RESET" >&2 +printf ' %s error(s), %s todo(s), %s done\n\n' "$TOTAL_ERROR" "$TOTAL_TODO" "$TOTAL_DONE" >&2 + +for i in $(seq 0 9); do + DONE="${PHASE_DONE[$i]}" + TODO="${PHASE_TODO[$i]}" + ERRORS="${PHASE_ERROR[$i]}" + TOTAL="${PHASE_TOTAL[$i]}" + NAME="${PHASE_NAMES[$i]}" + + if [[ "$TOTAL" -eq 0 ]]; then + continue + fi + + if [[ "$ERRORS" -gt 0 ]]; then + printf ' %b%-30s%b %s/%s done, %s error(s)\n' "$_C_RED" "$NAME" "$_C_RESET" "$DONE" "$TOTAL" "$ERRORS" >&2 + elif [[ "$TODO" -gt 0 ]]; then + printf ' %b%-30s%b %s/%s done, %s todo\n' "$_C_YELLOW" "$NAME" "$_C_RESET" "$DONE" "$TOTAL" "$TODO" >&2 + else + printf ' %b%-30s%b %s/%s done\n' "$_C_GREEN" "$NAME" "$_C_RESET" "$DONE" "$TOTAL" >&2 + fi +done + +printf '\n' >&2 + +if [[ "$TOTAL_ERROR" -gt 0 ]]; then + log_error "${TOTAL_ERROR} error(s) found — see above for details" + exit 1 +else + log_success "No errors found" + exit 0 +fi