#!/usr/bin/env bash # Forward + rollback migration drill against an ephemeral PostgreSQL # container. Validates that: # 1. All committed migrations apply cleanly from an empty database # 2. The seed runs idempotently against the resulting schema # 3. Running the entire forward chain a second time is a no-op # (Prisma marks every migration as already applied) # # This is a production-rehearsal: catches migrations that fail under a # fresh DB (drift between dev shadow and production state) and asserts the # seed-after-migrate contract that prod deploys rely on. # # Usage: # ./scripts/drill-migration-rollback.sh # ./scripts/drill-migration-rollback.sh --no-seed # skip seed step # ./scripts/drill-migration-rollback.sh --keep-container # leave PG running # # Requires: docker, pnpm. Aborts with a documentation pointer if either # is missing. set -euo pipefail DRILL_NAME="boilerplate-rollback-drill-$$" DRILL_PORT="${DRILL_PG_PORT:-55842}" DRILL_USER="drill" DRILL_PASSWORD="drill" DRILL_DB="drill" DRILL_DATABASE_URL="postgresql://${DRILL_USER}:${DRILL_PASSWORD}@127.0.0.1:${DRILL_PORT}/${DRILL_DB}?schema=public" KEEP_CONTAINER=0 RUN_SEED=1 PG_IMAGE="${DRILL_PG_IMAGE:-postgres:16-alpine}" for arg in "$@"; do case "$arg" in --no-seed) RUN_SEED=0 ;; --keep-container) KEEP_CONTAINER=1 ;; --help|-h) sed -n '2,/^$/p' "$0" | sed 's/^# \?//' exit 0 ;; *) echo "Unknown arg: $arg" >&2; exit 64 ;; esac done require_cmd() { if ! command -v "$1" >/dev/null 2>&1; then echo "MISSING $1" >&2 echo "" >&2 echo "Migration-rollback drill requires '$1' to be available locally." >&2 echo "See docs/operations/database-rollback.md for the manual procedure." >&2 exit 127 fi } require_cmd docker require_cmd pnpm cleanup() { local exit_code=$? if [ "$KEEP_CONTAINER" = "0" ]; then docker rm -f "$DRILL_NAME" >/dev/null 2>&1 || true else echo "" echo "KEEP container '$DRILL_NAME' is still running on port $DRILL_PORT (DATABASE_URL=$DRILL_DATABASE_URL)" >&2 fi exit "$exit_code" } trap cleanup EXIT echo "PHASE 0/4 starting ephemeral postgres ($PG_IMAGE on port $DRILL_PORT)" docker run --rm -d \ --name "$DRILL_NAME" \ -p "${DRILL_PORT}:5432" \ -e POSTGRES_USER="$DRILL_USER" \ -e POSTGRES_PASSWORD="$DRILL_PASSWORD" \ -e POSTGRES_DB="$DRILL_DB" \ "$PG_IMAGE" >/dev/null deadline=$(( $(date +%s) + 30 )) until docker exec "$DRILL_NAME" pg_isready -U "$DRILL_USER" -d "$DRILL_DB" >/dev/null 2>&1; do if [ "$(date +%s)" -gt "$deadline" ]; then echo "FAIL postgres did not become ready within 30s" >&2 exit 1 fi sleep 1 done echo "PASS postgres ready" echo "PHASE 1/4 applying forward migration chain" DATABASE_URL="$DRILL_DATABASE_URL" pnpm --filter @toolctrl/db prisma:generate >/dev/null DATABASE_URL="$DRILL_DATABASE_URL" pnpm exec prisma migrate deploy --schema prisma/schema/schema.prisma forward_count=$(DATABASE_URL="$DRILL_DATABASE_URL" pnpm exec prisma migrate status --schema prisma/schema/schema.prisma 2>&1 | grep -c "Following migration have been applied" || true) echo "PASS forward migration chain applied" if [ "$RUN_SEED" = "1" ]; then echo "PHASE 2/4 running production seed (idempotency probe 1/2)" DATABASE_URL="$DRILL_DATABASE_URL" \ SEED_ADMIN_EMAIL="drill-admin@example.com" \ SEED_ADMIN_PASSWORD="drill-bootstrap-password-do-not-reuse" \ SEED_DEMO_DATA="false" \ NODE_ENV=production \ pnpm --filter @toolctrl/db seed echo "PASS seed run 1" echo "PHASE 3/4 re-running seed (idempotency probe 2/2)" DATABASE_URL="$DRILL_DATABASE_URL" \ SEED_ADMIN_EMAIL="drill-admin@example.com" \ SEED_ADMIN_PASSWORD="drill-bootstrap-password-do-not-reuse" \ SEED_DEMO_DATA="false" \ NODE_ENV=production \ pnpm --filter @toolctrl/db seed echo "PASS seed run 2 (idempotent)" else echo "SKIP seed (--no-seed flag)" fi echo "PHASE 4/4 re-applying migrate deploy (chain idempotency)" output=$(DATABASE_URL="$DRILL_DATABASE_URL" pnpm exec prisma migrate deploy --schema prisma/schema/schema.prisma 2>&1) echo "$output" | tail -5 if echo "$output" | grep -qiE "no pending migrations|already in sync|database schema is up to date"; then echo "PASS migrate deploy is a no-op on already-migrated schema" else echo "FAIL second migrate deploy reported changes — chain is not idempotent" >&2 exit 1 fi echo "" echo "DRILL OK — migration chain + seed survive a fresh-database deployment rehearsal"