Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/verify-migrations-schema-parity.sh
T

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"