diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index ecc28310..3d59d498 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -385,20 +385,29 @@ jobs: REMOTE_DB_NAME=$(ssh root@alpha.mintel.me "grep -h '^PAYLOAD_DB_NAME=' $SITE_DIR/.env* 2>/dev/null | tail -1 | cut -d= -f2" || echo "payload") REMOTE_DB_USER="${REMOTE_DB_USER:-payload}" REMOTE_DB_NAME="${REMOTE_DB_NAME:-payload}" - ssh root@alpha.mintel.me "docker exec $DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME -c \" - DO \\\$\\\$ BEGIN - DELETE FROM payload_migrations WHERE batch = -1; - INSERT INTO payload_migrations (name, batch) - SELECT name, batch FROM (VALUES - ('20260223_195005_products_collection', 1), - ('20260223_195151_remove_sku_unique', 2), - ('20260225_003500_add_pages_collection', 3) - ) AS v(name, batch) - WHERE NOT EXISTS (SELECT 1 FROM payload_migrations pm WHERE pm.name = v.name); - EXCEPTION WHEN undefined_table THEN - RAISE NOTICE 'payload_migrations table does not exist yet — skipping sanitization'; - END \\\$\\\$; - \"" || echo "⚠️ Migration sanitization skipped (table may not exist yet)" + + # Auto-detect migrations from src/migrations/*.ts + BATCH=1 + VALUES="" + for f in $(ls src/migrations/*.ts 2>/dev/null | sort); do + NAME=$(basename "$f" .ts) + [ -n "$VALUES" ] && VALUES="$VALUES," + VALUES="$VALUES ('$NAME', $BATCH)" + ((BATCH++)) + done + + if [ -n "$VALUES" ]; then + ssh root@alpha.mintel.me "docker exec $DB_CONTAINER psql -U $REMOTE_DB_USER -d $REMOTE_DB_NAME -c \" + DO \\\$\\\$ BEGIN + DELETE FROM payload_migrations WHERE batch = -1; + INSERT INTO payload_migrations (name, batch) + SELECT name, batch FROM (VALUES $VALUES) AS v(name, batch) + WHERE NOT EXISTS (SELECT 1 FROM payload_migrations pm WHERE pm.name = v.name); + EXCEPTION WHEN undefined_table THEN + RAISE NOTICE 'payload_migrations table does not exist yet — skipping sanitization'; + END \\\$\\\$; + \"" || echo "⚠️ Migration sanitization skipped (table may not exist yet)" + fi # Restart app to pick up clean migration state APP_CONTAINER="${{ needs.prepare.outputs.project_name }}-klz-app-1" @@ -474,6 +483,26 @@ jobs: [ -f /usr/bin/chromium ] && ln -sf /usr/bin/chromium /usr/bin/chromium-browser # ── Critical Smoke Tests (MUST pass) ────────────────────────────────── + - name: 🏥 CMS Deep Health Check + env: + DEPLOY_URL: ${{ needs.prepare.outputs.next_public_url }} + GK_PASS: ${{ secrets.GATEKEEPER_PASSWORD || 'klz2026' }} + run: | + echo "Waiting 10s for app to fully start..." + sleep 10 + echo "Checking basic health..." + curl -sf "$DEPLOY_URL/health" || { echo "❌ Basic health check failed"; exit 1; } + echo "✅ Basic health OK" + echo "Checking CMS DB connectivity..." + RESPONSE=$(curl -sf "$DEPLOY_URL/api/health/cms?gk_bypass=$GK_PASS" 2>&1) || { + echo "❌ CMS health check failed!" + echo "$RESPONSE" + echo "" + echo "This usually means Payload CMS migrations failed or DB tables are missing." + echo "Check: docker logs \$APP_CONTAINER | grep -i error" + exit 1 + } + echo "✅ CMS health: $RESPONSE" - name: 🚀 OG Image Check if: always() && steps.deps.outcome == 'success' env: diff --git a/app/api/health/cms/route.ts b/app/api/health/cms/route.ts index af30e2e0..c247e36a 100644 --- a/app/api/health/cms/route.ts +++ b/app/api/health/cms/route.ts @@ -1,9 +1,41 @@ import { NextResponse } from 'next/server'; +import { getPayload } from 'payload'; +import configPromise from '@payload-config'; export const dynamic = 'force-dynamic'; +/** + * Deep CMS Health Check + * Validates that Payload CMS can actually query the database. + * Used by post-deploy smoke tests to catch migration/schema issues. + */ export async function GET() { - // Payload is embedded within the Next.js app, so if this route responds, the CMS is up. - // Further DB health checks can be implemented via Payload Local API later. - return NextResponse.json({ status: 'ok', message: 'Payload CMS is embedded.' }, { status: 200 }); + const checks: Record = {}; + + try { + const payload = await getPayload({ config: configPromise }); + checks.init = 'ok'; + + // Verify each collection can be queried (catches missing locale tables, broken migrations) + const collections = ['posts', 'products', 'pages', 'media'] as const; + for (const collection of collections) { + try { + await payload.find({ collection, limit: 1, locale: 'en' }); + checks[collection] = 'ok'; + } catch (e: any) { + checks[collection] = `error: ${e.message?.substring(0, 100)}`; + } + } + + const hasErrors = Object.values(checks).some(v => v.startsWith('error')); + return NextResponse.json( + { status: hasErrors ? 'degraded' : 'ok', checks }, + { status: hasErrors ? 503 : 200 }, + ); + } catch (e: any) { + return NextResponse.json( + { status: 'error', message: e.message?.substring(0, 200), checks }, + { status: 503 }, + ); + } } diff --git a/scripts/cms-sync.sh b/scripts/cms-sync.sh index eaf918bf..db7c2eaf 100755 --- a/scripts/cms-sync.sh +++ b/scripts/cms-sync.sh @@ -53,13 +53,17 @@ TIMESTAMP=$(date +"%Y%m%d_%H%M%S") REMOTE_DB_USER="" REMOTE_DB_NAME="" -# Migration names to insert after restore (keeps Payload from prompting) -MIGRATIONS=( - "20260223_195005_products_collection:1" - "20260223_195151_remove_sku_unique:2" - "20260225_003500_add_pages_collection:3" - "20260225_175000_native_localization:4" -) +# Auto-detect migrations from src/migrations/*.ts (no manual maintenance needed) +MIGRATIONS=() +BATCH=1 +for migration_file in $(ls src/migrations/*.ts 2>/dev/null | sort); do + name=$(basename "$migration_file" .ts) + MIGRATIONS+=("$name:$BATCH") + ((BATCH++)) +done +if [ ${#MIGRATIONS[@]} -eq 0 ]; then + echo "⚠️ No migration files found in src/migrations/" +fi # ── Resolve target environment ───────────────────────────────────────────── resolve_target() { @@ -159,6 +163,29 @@ backup_remote_db() { REMOTE_BACKUP_FILE="$file" } +# ── Pre-flight: Verify remote containers exist ───────────────────────────── +check_remote_containers() { + echo "🔍 Checking $TARGET containers..." + local missing=0 + if ! ssh "$SSH_HOST" "docker ps -q -f name=$REMOTE_DB_CONTAINER" | grep -q .; then + echo "❌ Database container '$REMOTE_DB_CONTAINER' not found on $SSH_HOST" + echo " → Deploy $TARGET first: git push to trigger pipeline, or run:" + echo " ssh $SSH_HOST \"cd $REMOTE_SITE_DIR && docker compose -p $REMOTE_PROJECT --env-file .env.\$TARGET up -d\"" + missing=1 + fi + if ! ssh "$SSH_HOST" "docker ps -q -f name=$REMOTE_APP_CONTAINER" | grep -q .; then + echo "❌ App container '$REMOTE_APP_CONTAINER' not found on $SSH_HOST" + missing=1 + fi + if [ $missing -eq 1 ]; then + echo "" + echo "💡 The $TARGET environment hasn't been deployed yet." + echo " Push to the '$TARGET' branch or run the pipeline first." + exit 1 + fi + echo "✅ All $TARGET containers running." +} + # ── PUSH: local → remote ────────────────────────────────────────────────── do_push() { echo "" @@ -172,8 +199,9 @@ do_push() { echo "" [[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; } - # 0. Ensure local DB is running + # 0. Ensure local DB is running & remote containers exist ensure_local_db + check_remote_containers # 1. Safety backup of remote backup_remote_db @@ -227,8 +255,9 @@ do_pull() { echo "" [[ ! $REPLY =~ ^[Yy]$ ]] && { echo "Cancelled."; exit 0; } - # 0. Ensure local DB is running + # 0. Ensure local DB is running & remote containers exist ensure_local_db + check_remote_containers # 1. Safety backup of local backup_local_db