Files
HiveGrid-Wireframes/hivegrid-cloud/scripts/drill-migration-rollback.sh
T

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"