#!/bin/sh set -eu MAX_MIGRATION_RETRIES="${MIGRATION_MAX_RETRIES:-6}" MIGRATION_RETRY_DELAY_SECONDS="${MIGRATION_RETRY_DELAY_SECONDS:-2}" ATTEMPT=1 # Auto-resolve any previously failed migrations so deploy can proceed. # This handles the case where a migration failed mid-flight and was then # fixed in a subsequent deploy — without this, Prisma refuses to run # anything else (P3009). # # We query `_prisma_migrations` directly rather than parsing the output of # `prisma migrate status`, because that output's wording has shifted between # Prisma versions and any drift means failed migrations slip through and # the container crash-loops. Truth lives in the table: a row with # `finished_at IS NULL AND rolled_back_at IS NULL` is an unresolved failure. echo "==> Checking for failed migrations..." RESOLVE_ATTEMPTS=0 while [ "$RESOLVE_ATTEMPTS" -lt 5 ]; do FAILED=$(node -e " const { PrismaClient } = require('@prisma/client'); const p = new PrismaClient(); p.\$queryRaw\` SELECT migration_name FROM _prisma_migrations WHERE finished_at IS NULL AND rolled_back_at IS NULL ORDER BY started_at ASC LIMIT 1 \`.then(r => { console.log(r[0]?.migration_name || ''); p.\$disconnect(); }) .catch(() => { console.log(''); p.\$disconnect(); }); " 2>/dev/null || echo "") if [ -z "$FAILED" ]; then break fi echo "==> Found failed migration: $FAILED — marking as rolled back..." npx prisma migrate resolve --rolled-back "$FAILED" || { echo "WARNING: prisma migrate resolve failed for $FAILED" break } RESOLVE_ATTEMPTS=$((RESOLVE_ATTEMPTS + 1)) done echo "==> Running database migrations (with retry)..." until npx prisma migrate deploy; do if [ "$ATTEMPT" -ge "$MAX_MIGRATION_RETRIES" ]; then echo "ERROR: Migration failed after ${MAX_MIGRATION_RETRIES} attempts." exit 1 fi echo "Migration attempt ${ATTEMPT} failed. Retrying in ${MIGRATION_RETRY_DELAY_SECONDS}s..." ATTEMPT=$((ATTEMPT + 1)) sleep "$MIGRATION_RETRY_DELAY_SECONDS" done echo "==> Generating Prisma client..." npx prisma generate # Auto-seed on first startup: check if Users table is empty USER_COUNT=$(node -e " const { PrismaClient } = require('@prisma/client'); const p = new PrismaClient(); p.user.count().then(c => { console.log(c); p.\$disconnect(); }).catch(() => { console.log('0'); p.\$disconnect(); }); " 2>/dev/null || echo "0") if [ "$USER_COUNT" = "0" ]; then echo "==> Empty database detected — running seed..." npx prisma db seed || echo "WARNING: Seed script failed." else echo "==> Database already seeded ($USER_COUNT users found), skipping seed." fi # Always sync notification email settings (upsert — safe for existing data) echo "==> Syncing notification email settings..." npx tsx prisma/seed-notification-settings.ts || echo "WARNING: Notification settings sync failed." # Sync team lead links only if there are unlinked submitters UNLINKED_COUNT=$(node -e " const { PrismaClient } = require('@prisma/client'); const p = new PrismaClient(); p.\$queryRaw\` SELECT COUNT(*)::int AS c FROM \"Project\" p WHERE p.\"submittedByUserId\" IS NOT NULL AND NOT EXISTS ( SELECT 1 FROM \"TeamMember\" tm WHERE tm.\"projectId\" = p.id AND tm.\"userId\" = p.\"submittedByUserId\" ) \`.then(r => { console.log(r[0].c); p.\$disconnect(); }).catch(() => { console.log('0'); p.\$disconnect(); }); " 2>/dev/null || echo "0") if [ "$UNLINKED_COUNT" != "0" ]; then echo "==> Syncing ${UNLINKED_COUNT} unlinked team lead links..." npx tsx prisma/seed-team-leads.ts || echo "WARNING: Team lead sync failed." else echo "==> Team lead links already synced, skipping." fi echo "==> Starting application..." # Graceful shutdown: forward SIGTERM/SIGINT to the Node process # so in-flight requests can complete before the container exits. shutdown() { echo "==> Received shutdown signal, stopping gracefully..." kill -TERM "$NODE_PID" 2>/dev/null wait "$NODE_PID" exit $? } trap shutdown TERM INT node server.js & NODE_PID=$! wait "$NODE_PID"