130 lines
5.1 KiB
Bash
Executable File
130 lines
5.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Migrations<->schema parity gate. Fails when the Prisma schema contains
|
|
# objects that no committed migration creates (the AD-21 footgun: feature
|
|
# branches use `prisma db push`, so a forgotten merge migration only surfaces
|
|
# as P2021 at runtime on deploy-managed databases — see
|
|
# the consolidated Prisma baseline).
|
|
#
|
|
# Replays the full migration history into an ephemeral PostgreSQL container
|
|
# (shadow DB) and diffs the result against prisma/schema. Any non-empty diff
|
|
# means "schema changed but no migration was written" and blocks the PR.
|
|
#
|
|
# Usage:
|
|
# ./scripts/verify-migrations-schema-parity.sh
|
|
#
|
|
# Requires: docker, pnpm.
|
|
|
|
set -euo pipefail
|
|
|
|
PARITY_NAME="hivegrid-migrations-parity-$$"
|
|
PARITY_USER="parity"
|
|
PARITY_PASSWORD="parity"
|
|
PARITY_DB="parity"
|
|
PARITY_MIGRATOR_PASSWORD="migrator_parity_0123456789abcdef0123"
|
|
PARITY_APP_PASSWORD="app_parity_0123456789abcdef012345678"
|
|
PARITY_PORTAL_PASSWORD="portal_parity_0123456789abcdef01234"
|
|
PARITY_WORKER_PASSWORD="worker_parity_0123456789abcdef01234"
|
|
PARITY_AUTHENTIK_JIT_PASSWORD="authentik_jit_parity_0123456789abcdef"
|
|
PG_IMAGE="${PARITY_PG_IMAGE:-postgres:16-alpine}"
|
|
|
|
require_cmd() {
|
|
if ! command -v "$1" >/dev/null 2>&1; then
|
|
echo "MISSING $1" >&2
|
|
echo "The migrations/schema parity gate requires '$1' locally." >&2
|
|
exit 127
|
|
fi
|
|
}
|
|
|
|
require_cmd docker
|
|
require_cmd node
|
|
require_cmd pnpm
|
|
|
|
node scripts/verify-prisma-baseline.mjs
|
|
|
|
cleanup() {
|
|
docker rm -f "$PARITY_NAME" >/dev/null 2>&1 || true
|
|
}
|
|
trap cleanup EXIT
|
|
|
|
docker run -d --name "$PARITY_NAME" -p 127.0.0.1::5432 \
|
|
-e POSTGRES_USER="$PARITY_USER" \
|
|
-e POSTGRES_PASSWORD="$PARITY_PASSWORD" \
|
|
-e POSTGRES_DB="$PARITY_DB" \
|
|
"$PG_IMAGE" >/dev/null
|
|
|
|
PARITY_PORT="$(docker port "$PARITY_NAME" 5432/tcp | awk -F: 'NR == 1 { print $NF }')"
|
|
SHADOW_ADMIN_URL="postgresql://${PARITY_USER}:${PARITY_PASSWORD}@127.0.0.1:${PARITY_PORT}/${PARITY_DB}?schema=public"
|
|
SHADOW_MIGRATOR_URL="postgresql://hivegrid_migrator:${PARITY_MIGRATOR_PASSWORD}@127.0.0.1:${PARITY_PORT}/${PARITY_DB}?schema=public"
|
|
|
|
# Probe over TCP, not the unix socket. On first boot the postgres image starts a
|
|
# temporary server that listens on the socket ONLY (initdb + init scripts), then
|
|
# shuts it down before starting the real one. A socket-based pg_isready reports
|
|
# ready during that window, so the very next psql can hit
|
|
# "FATAL: the database system is shutting down". The temp server sets
|
|
# listen_addresses='', so a TCP probe cannot succeed too early.
|
|
#
|
|
# The window is deliberately generous. First boot is initdb plus a full
|
|
# "syncing data to disk", and on a CI runner that is building images in
|
|
# parallel that I/O can stall for far longer than the container needs when it
|
|
# runs alone. A 30s window missed a real, healthy boot by 150ms once; the cost
|
|
# of waiting longer is nothing (the loop exits as soon as the probe answers),
|
|
# the cost of being too tight is a red pipeline that says "not ready" about a
|
|
# database that was fine.
|
|
PARITY_READY_ATTEMPTS=90
|
|
for attempt in $(seq 1 "$PARITY_READY_ATTEMPTS"); do
|
|
if docker exec "$PARITY_NAME" pg_isready -h 127.0.0.1 -U "$PARITY_USER" -d "$PARITY_DB" >/dev/null 2>&1; then
|
|
break
|
|
fi
|
|
if [[ "$attempt" == "$PARITY_READY_ATTEMPTS" ]]; then
|
|
echo "Parity gate Postgres did not become ready after ${PARITY_READY_ATTEMPTS}s" >&2
|
|
docker logs "$PARITY_NAME" >&2 || true
|
|
exit 1
|
|
fi
|
|
sleep 1
|
|
done
|
|
|
|
# The RLS cutover deliberately requires externally provisioned roles and a
|
|
# non-superuser migrator. Rehearse that same prerequisite in the disposable
|
|
# database instead of weakening the migration for Prisma's shadow replay.
|
|
HIVEGRID_RLS_PROVISION_DATABASE_URL="$SHADOW_ADMIN_URL" \
|
|
HIVEGRID_RLS_MIGRATOR_PASSWORD="$PARITY_MIGRATOR_PASSWORD" \
|
|
HIVEGRID_RLS_APP_PASSWORD="$PARITY_APP_PASSWORD" \
|
|
HIVEGRID_RLS_PORTAL_PASSWORD="$PARITY_PORTAL_PASSWORD" \
|
|
HIVEGRID_RLS_WORKER_PASSWORD="$PARITY_WORKER_PASSWORD" \
|
|
HIVEGRID_RLS_AUTHENTIK_JIT_PASSWORD="$PARITY_AUTHENTIK_JIT_PASSWORD" \
|
|
node scripts/provision-rls-roles.mjs >/dev/null
|
|
|
|
# prisma.config.ts resolves the datasource at load time. Use the exact
|
|
# restricted migrator for both datasource and shadow replay so the parity gate
|
|
# exercises the same ownership and role contract as deployment.
|
|
set +e
|
|
DATABASE_URL="$SHADOW_MIGRATOR_URL" DIRECT_URL="$SHADOW_MIGRATOR_URL" SHADOW_DATABASE_URL="$SHADOW_MIGRATOR_URL" \
|
|
pnpm exec prisma migrate diff \
|
|
--from-migrations prisma/migrations \
|
|
--to-schema prisma/schema \
|
|
--exit-code \
|
|
--config prisma.config.ts
|
|
DIFF_EXIT=$?
|
|
set -e
|
|
|
|
if [[ "$DIFF_EXIT" == "0" ]]; then
|
|
echo "OK migrations and prisma/schema are in parity"
|
|
exit 0
|
|
fi
|
|
|
|
if [[ "$DIFF_EXIT" == "2" ]]; then
|
|
echo "" >&2
|
|
echo "FAIL prisma/schema contains changes no migration covers (AD-21: write the merge migration)." >&2
|
|
echo "Drift script:" >&2
|
|
DATABASE_URL="$SHADOW_MIGRATOR_URL" DIRECT_URL="$SHADOW_MIGRATOR_URL" SHADOW_DATABASE_URL="$SHADOW_MIGRATOR_URL" \
|
|
pnpm exec prisma migrate diff \
|
|
--from-migrations prisma/migrations \
|
|
--to-schema prisma/schema \
|
|
--script \
|
|
--config prisma.config.ts >&2 || true
|
|
exit 2
|
|
fi
|
|
|
|
echo "FAIL prisma migrate diff errored (exit ${DIFF_EXIT})" >&2
|
|
exit "$DIFF_EXIT"
|