fix: enhance entrypoint script to handle Prisma migration failures with auto-resolution for specific errors

This commit is contained in:
2026-01-21 03:16:36 +02:00
parent b0ab139ef6
commit 75076e82eb
2 changed files with 276 additions and 4 deletions

View File

@@ -101,17 +101,51 @@ elif [ -n "$DATABASE_URL" ]; then
echo "🔄 Running database migrations (attempts: ${MIGRATION_MAX_RETRIES})... $(date '+%Y-%m-%d %H:%M:%S')"
did_auto_resolve_email_logs=0
while [ "$attempt" -le "$MIGRATION_MAX_RETRIES" ]; do
echo "➡️ Attempt ${attempt}/${MIGRATION_MAX_RETRIES}: prisma migrate deploy"
# Run migrations with timeout to prevent hanging
if timeout 120 sh -c "$PRISMA_RUN migrate deploy 2>&1"; then
# Run migrations with timeout to prevent hanging.
# Capture output so we can detect non-retryable failures (e.g. P3009 failed migrations).
set +e
MIGRATION_OUTPUT=$(timeout 120 sh -c "$PRISMA_RUN migrate deploy" 2>&1)
MIGRATION_STATUS=$?
set -e
if [ "$MIGRATION_STATUS" -eq 0 ]; then
echo "✅ Prisma migrations applied $(date '+%Y-%m-%d %H:%M:%S')"
break
fi
migration_status=$?
echo "⚠️ Prisma migrations attempt ${attempt} failed (status: ${migration_status}) $(date '+%Y-%m-%d %H:%M:%S')"
echo "$MIGRATION_OUTPUT"
echo "⚠️ Prisma migrations attempt ${attempt} failed (status: ${MIGRATION_STATUS}) $(date '+%Y-%m-%d %H:%M:%S')"
# Non-retryable: Prisma has a failed migration recorded (P3009).
# For our known case, auto-resolve the broken email_logs migration and retry once.
if echo "$MIGRATION_OUTPUT" | grep -q "Error: P3009"; then
if echo "$MIGRATION_OUTPUT" | grep -q "20260116150000_add_email_logs_table" && [ "$did_auto_resolve_email_logs" -eq 0 ]; then
did_auto_resolve_email_logs=1
echo "🛠️ Detected failed migration 20260116150000_add_email_logs_table (P3009). Attempting auto-resolve as applied so deployment can continue..."
set +e
$PRISMA_RUN migrate resolve --applied 20260116150000_add_email_logs_table 2>&1
RESOLVE_STATUS=$?
set -e
if [ "$RESOLVE_STATUS" -ne 0 ]; then
echo "❌ ERROR: Failed to resolve migration state automatically. Please run:"
echo " prisma migrate resolve --applied 20260116150000_add_email_logs_table"
exit 1
fi
echo "✅ Marked 20260116150000_add_email_logs_table as applied. Retrying migrate deploy..."
attempt=$((attempt + 1))
continue
fi
echo "❌ ERROR: Prisma reports failed migrations in the target database (P3009). This is not retryable."
echo "Fix by resolving the failed migration in the DB, then redeploy. Common command:"
echo " prisma migrate resolve --applied <MIGRATION_NAME>"
exit 1
fi
if [ "$attempt" -ge "$MIGRATION_MAX_RETRIES" ]; then
echo "❌ ERROR: Prisma migrations failed after ${MIGRATION_MAX_RETRIES} attempts $(date '+%Y-%m-%d %H:%M:%S')"

View File

@@ -0,0 +1,238 @@
-- This migration repairs the `email_logs` table to match the Prisma model `EmailLog`
-- (`@@map("email_logs")`) and the current schema conventions used in this repo.
--
-- Why: An earlier migration (`20260116150000_add_email_logs_table`) created/attempted
-- to create snake_case columns and a foreign key referencing `guests(id)` UUID,
-- which is incompatible with our actual `Guest` table (`"Guest"`) and `Guest.id` type (TEXT).
--
-- This migration is written defensively so it can run even if the table was partially created.
-- 1) Ensure table exists (with correct camelCase columns)
CREATE TABLE IF NOT EXISTS "email_logs" (
"id" TEXT NOT NULL,
"guestId" TEXT,
"recipientEmail" VARCHAR(255) NOT NULL,
"recipientName" TEXT,
"subject" VARCHAR(500) NOT NULL,
"emailType" VARCHAR(50) NOT NULL,
"resendMessageId" VARCHAR(255),
"status" VARCHAR(50) NOT NULL DEFAULT 'sent',
"statusDetails" TEXT,
"sentAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deliveredAt" TIMESTAMP(3),
"openedAt" TIMESTAMP(3),
"clickedAt" TIMESTAMP(3),
"bouncedAt" TIMESTAMP(3),
"lastEventAt" TIMESTAMP(3),
"metadata" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "email_logs_pkey" PRIMARY KEY ("id")
);
-- 2) If legacy snake_case columns exist, rename them to camelCase expected by Prisma
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'guest_id'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'guestId'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "guest_id" TO "guestId"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'recipient_email'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'recipientEmail'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "recipient_email" TO "recipientEmail"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'recipient_name'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'recipientName'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "recipient_name" TO "recipientName"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'email_type'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'emailType'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "email_type" TO "emailType"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'resend_message_id'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'resendMessageId'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "resend_message_id" TO "resendMessageId"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'status_details'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'statusDetails'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "status_details" TO "statusDetails"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'sent_at'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'sentAt'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "sent_at" TO "sentAt"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'delivered_at'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'deliveredAt'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "delivered_at" TO "deliveredAt"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'opened_at'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'openedAt'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "opened_at" TO "openedAt"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'clicked_at'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'clickedAt'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "clicked_at" TO "clickedAt"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'bounced_at'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'bouncedAt'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "bounced_at" TO "bouncedAt"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'last_event_at'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'lastEventAt'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "last_event_at" TO "lastEventAt"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'created_at'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'createdAt'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "created_at" TO "createdAt"';
END IF;
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'updated_at'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'email_logs' AND column_name = 'updatedAt'
) THEN
EXECUTE 'ALTER TABLE "email_logs" RENAME COLUMN "updated_at" TO "updatedAt"';
END IF;
END $$;
-- 3) Ensure required columns exist (in case of partial legacy table)
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "guestId" TEXT;
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "recipientEmail" VARCHAR(255);
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "recipientName" TEXT;
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "subject" VARCHAR(500);
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "emailType" VARCHAR(50);
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "resendMessageId" VARCHAR(255);
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "status" VARCHAR(50) DEFAULT 'sent';
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "statusDetails" TEXT;
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "sentAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "deliveredAt" TIMESTAMP(3);
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "openedAt" TIMESTAMP(3);
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "clickedAt" TIMESTAMP(3);
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "bouncedAt" TIMESTAMP(3);
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "lastEventAt" TIMESTAMP(3);
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "metadata" JSONB;
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "createdAt" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE "email_logs" ADD COLUMN IF NOT EXISTS "updatedAt" TIMESTAMP(3);
-- 4) Ensure guestId is TEXT (legacy migration used UUID)
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'email_logs'
AND column_name = 'guestId'
AND data_type = 'uuid'
) THEN
EXECUTE 'ALTER TABLE "email_logs" ALTER COLUMN "guestId" TYPE TEXT USING "guestId"::text';
END IF;
END $$;
-- 5) Drop legacy foreign key (if it exists) and add correct FK to "Guest"(id)
ALTER TABLE "email_logs" DROP CONSTRAINT IF EXISTS "email_logs_guest_id_fkey";
ALTER TABLE "email_logs" DROP CONSTRAINT IF EXISTS "email_logs_guestId_fkey";
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_class WHERE relname = 'Guest') THEN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint c
JOIN pg_class t ON t.oid = c.conrelid
WHERE c.conname = 'email_logs_guestId_fkey'
AND t.relname = 'email_logs'
) THEN
EXECUTE 'ALTER TABLE "email_logs" ADD CONSTRAINT "email_logs_guestId_fkey" FOREIGN KEY ("guestId") REFERENCES "Guest"("id") ON DELETE SET NULL ON UPDATE CASCADE';
END IF;
END IF;
END $$;
-- 6) Indexes / uniqueness (names follow Prisma-style for mapped table + camelCase fields)
CREATE UNIQUE INDEX IF NOT EXISTS "email_logs_resendMessageId_key" ON "email_logs" ("resendMessageId");
CREATE INDEX IF NOT EXISTS "email_logs_guestId_idx" ON "email_logs" ("guestId");
CREATE INDEX IF NOT EXISTS "email_logs_recipientEmail_idx" ON "email_logs" ("recipientEmail");
CREATE INDEX IF NOT EXISTS "email_logs_resendMessageId_idx" ON "email_logs" ("resendMessageId");
CREATE INDEX IF NOT EXISTS "email_logs_status_idx" ON "email_logs" ("status");
CREATE INDEX IF NOT EXISTS "email_logs_emailType_idx" ON "email_logs" ("emailType");
CREATE INDEX IF NOT EXISTS "email_logs_sentAt_idx" ON "email_logs" ("sentAt");