deploy: prod-only pipeline with Drizzle migrations; dev refresh + preview
- push to main builds, backs up prod (pg_dump 17), runs db-migrate + seeds, preflights and deploys; rollback = workflow_dispatch with rollback_tag - staging workflow, promote step and migrate-*.ts / old SQL removed - DB_URL = app role, DB_MIGRATE_URL = owner role - scripts/db-refresh-dev.sh (prod dump -> local dev DB), scripts/preview.sh <branch>
This commit is contained in:
84
scripts/db-refresh-dev.sh
Executable file
84
scripts/db-refresh-dev.sh
Executable file
@@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env bash
|
||||
# Refresh the local development database from a fresh production dump.
|
||||
#
|
||||
# Prod is the source of truth for data; dev is a disposable copy of it.
|
||||
#
|
||||
# Required env (put them in .env.dev-refresh or export them; never commit real values):
|
||||
# PROD_DB_URL read access to prod, e.g. postgresql://personal:...@127.0.0.1:5440/personal
|
||||
# DEV_ADMIN_URL superuser on the dev cluster, maintenance DB, e.g. postgresql://postgres:...@127.0.0.1:5432/postgres
|
||||
# Optional:
|
||||
# DEV_DB_NAME target database (default personal_dev)
|
||||
# PG_DOCKER=1 use the postgres:17-alpine image instead of local pg_dump/pg_restore
|
||||
#
|
||||
# What it does: dump prod (custom format) -> drop + recreate DEV_DB_NAME -> create
|
||||
# missing roles (NOLOGIN) -> restore with ownership/grants intact -> clear sessions
|
||||
# and verification tokens so prod logins can't be reused in dev. It never writes to prod.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
if [[ -f .env.dev-refresh ]]; then set -a; . ./.env.dev-refresh; set +a; fi
|
||||
|
||||
: "${PROD_DB_URL:?set PROD_DB_URL (source, read-only use)}"
|
||||
: "${DEV_ADMIN_URL:?set DEV_ADMIN_URL (superuser URL to the dev cluster maintenance DB)}"
|
||||
DEV_DB_NAME="${DEV_DB_NAME:-personal_dev}"
|
||||
|
||||
if [[ ! "$DEV_DB_NAME" =~ ^[a-z_][a-z0-9_]*$ ]]; then
|
||||
echo "DEV_DB_NAME must be a plain lowercase identifier" >&2
|
||||
exit 1
|
||||
fi
|
||||
case "$DEV_DB_NAME" in
|
||||
personal | personal_prod | postgres | template0 | template1)
|
||||
echo "Refusing to overwrite '$DEV_DB_NAME'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
# host:port/db of a URL (no credentials), for the "same database" guard.
|
||||
endpoint() { bun -e 'const u=new URL(process.argv[1]); console.log(`${u.hostname}:${u.port||5432}${u.pathname}`)' "$1"; }
|
||||
with_db() { bun -e 'const u=new URL(process.argv[1]); u.pathname="/"+process.argv[2]; console.log(u.toString())' "$1" "$2"; }
|
||||
|
||||
prod_endpoint="$(endpoint "$PROD_DB_URL")"
|
||||
dev_url="$(with_db "$DEV_ADMIN_URL" "$DEV_DB_NAME")"
|
||||
if [[ "$(endpoint "$dev_url")" == "$prod_endpoint" ]]; then
|
||||
echo "Refusing: target $DEV_DB_NAME is the prod source database ($prod_endpoint)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${PG_DOCKER:-0}" == 1 ]]; then
|
||||
pg() { docker run --rm -i --network host -v "$work:$work" postgres:17-alpine "$@"; }
|
||||
else
|
||||
pg() { "$@"; }
|
||||
fi
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
dump="$work/prod.dump"
|
||||
|
||||
echo "Dumping prod ($prod_endpoint)..."
|
||||
pg pg_dump --format=custom --no-password --file="$dump" "$PROD_DB_URL"
|
||||
|
||||
echo "Recreating $DEV_DB_NAME..."
|
||||
pg psql -v ON_ERROR_STOP=1 -q "$DEV_ADMIN_URL" <<SQL
|
||||
SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DEV_DB_NAME' AND pid <> pg_backend_pid();
|
||||
DROP DATABASE IF EXISTS "$DEV_DB_NAME";
|
||||
DO \$\$
|
||||
DECLARE r text;
|
||||
BEGIN
|
||||
FOREACH r IN ARRAY ARRAY['personal','personal_app','personal_ddl','personal_ro'] LOOP
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = r) THEN
|
||||
EXECUTE format('CREATE ROLE %I NOLOGIN', r);
|
||||
RAISE NOTICE 'created role % (NOLOGIN; give it a password + LOGIN for dev use)', r;
|
||||
END IF;
|
||||
END LOOP;
|
||||
END \$\$;
|
||||
CREATE DATABASE "$DEV_DB_NAME" OWNER personal;
|
||||
SQL
|
||||
|
||||
echo "Restoring into $DEV_DB_NAME..."
|
||||
pg pg_restore --no-password --exit-on-error --dbname="$dev_url" "$dump"
|
||||
|
||||
echo "Clearing sessions and verification tokens..."
|
||||
pg psql -v ON_ERROR_STOP=1 -q "$dev_url" -c 'TRUNCATE auth.session, auth.verification'
|
||||
|
||||
pg psql -qAt "$dev_url" -c "SELECT 'drizzle migrations recorded: ' || count(*) FROM drizzle.__drizzle_migrations"
|
||||
echo "Done. Apply pending migrations from your branch with: bun run db:migrate"
|
||||
Reference in New Issue
Block a user