fix: enhance entrypoint script to handle Prisma migration failures with auto-resolution for specific errors
This commit is contained in:
@@ -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')"
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user