130 lines
4.4 KiB
Bash
Executable File
130 lines
4.4 KiB
Bash
Executable File
#!/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"
|