From 3d0a1d5e4469be97265a42b8c0c3ca09ebf9e143 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Apr 2026 22:14:01 +0000 Subject: [PATCH] Fix six code review issues in sync-prod-db.sh - Add upfront check for pg_dump/pg_restore/psql availability (#5) - Fix password masking to use sed instead of broken bash glob (#6) - Replace DROP/CREATE DATABASE with pg_restore --clean --if-exists, which works on managed cloud databases without superuser access (#2) - Remove dead ADMIN_DB assignment that was immediately overwritten (#3) - Handle pg_restore exit code 1 (non-fatal warnings) gracefully instead of aborting via set -e (#1) - Count webhook URLs before the UPDATE for an accurate cleared count (#4) https://claude.ai/code/session_01EiYh5ZiuWAnpo1PND45Yi1 --- scripts/sync-prod-db.sh | 78 ++++++++++++++++++++++------------------- 1 file changed, 42 insertions(+), 36 deletions(-) diff --git a/scripts/sync-prod-db.sh b/scripts/sync-prod-db.sh index 84968f5..7af1ddf 100755 --- a/scripts/sync-prod-db.sh +++ b/scripts/sync-prod-db.sh @@ -25,6 +25,11 @@ warn() { echo -e "${YELLOW}$*${RESET}"; } success() { echo -e "${GREEN}$*${RESET}"; } error() { echo -e "${RED}ERROR: $*${RESET}" >&2; exit 1; } +# ── Check required tools (fix #5) ──────────────────────────────────────────── +for cmd in pg_dump pg_restore psql; do + command -v "$cmd" &>/dev/null || error "'$cmd' not found. Install PostgreSQL client tools (e.g. brew install libpq)." +done + # ── Load .env ───────────────────────────────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="$SCRIPT_DIR/../.env" @@ -50,12 +55,18 @@ if [[ "$PROD_DB" == "$DEV_DB" ]]; then error "PROD_DATABASE_URL and DATABASE_URL are the same. Aborting to prevent data loss." fi +# ── Mask password for display (fix #6) ─────────────────────────────────────── +# Replaces only the password portion (between : and @) using sed, not bash globs. +mask_url() { + echo "$1" | sed 's|\(://[^:]*\):[^@]*@|\1:***@|' +} + # ── Confirmation prompt ─────────────────────────────────────────────────────── echo "" warn "⚠️ WARNING: This will OVERWRITE your development database with production data." echo "" -echo -e " Source (prod): ${BOLD}${PROD_DB//:*@/:***@}${RESET}" # mask password -echo -e " Target (dev): ${BOLD}${DEV_DB//:*@/:***@}${RESET}" +echo -e " Source (prod): ${BOLD}$(mask_url "$PROD_DB")${RESET}" +echo -e " Target (dev): ${BOLD}$(mask_url "$DEV_DB")${RESET}" echo "" warn "After restore, the script will:" echo " • NULL out all Discord webhook URLs (prevents notification leaks)" @@ -74,7 +85,7 @@ DUMP_FILE=$(mktemp /tmp/brackt_prod_dump_XXXXXX.dump) trap 'rm -f "$DUMP_FILE"' EXIT info "" -info "1/4 Dumping production database…" +info "1/3 Dumping production database…" pg_dump \ --no-owner \ --no-acl \ @@ -83,48 +94,43 @@ pg_dump \ --file="$DUMP_FILE" success " Dump complete: $(du -h "$DUMP_FILE" | cut -f1)" -# ── Drop & recreate dev DB ──────────────────────────────────────────────────── +# ── Restore (fix #1, #2, #3) ───────────────────────────────────────────────── +# Uses --clean --if-exists to drop and recreate all objects within the existing +# database, without needing DROP DATABASE / CREATE DATABASE permissions. +# This works on managed cloud databases (Railway, Render, Neon, Supabase, etc.). +# +# pg_restore exits with code 1 for non-fatal warnings (e.g. "role X does not +# exist" with --no-owner), so we capture the exit code and only abort on +# unexpected failures, not routine restore warnings. info "" -info "2/4 Recreating development database…" - -# Extract the database name from the URL (last path component) -DEV_DB_NAME="${DEV_DB##*/}" -DEV_DB_NAME="${DEV_DB_NAME%%\?*}" # strip any query params - -# Build a connection string to the postgres maintenance DB on the same host -# by replacing the DB name with "postgres" -ADMIN_DB="${DEV_DB%/$DEV_DB_NAME*}/postgres${DEV_DB#*/$DEV_DB_NAME}" -# Simpler: just swap the last path segment -ADMIN_DB=$(echo "$DEV_DB" | sed "s|/$DEV_DB_NAME\$|/postgres|;s|/$DEV_DB_NAME?|/postgres?|") - -# Terminate existing connections, then drop and recreate -psql "$ADMIN_DB" \ - -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '$DEV_DB_NAME' AND pid <> pg_backend_pid();" \ - -c "DROP DATABASE IF EXISTS \"$DEV_DB_NAME\";" \ - -c "CREATE DATABASE \"$DEV_DB_NAME\";" \ - --quiet -success " Database '$DEV_DB_NAME' recreated." - -# ── Restore ─────────────────────────────────────────────────────────────────── -info "" -info "3/4 Restoring production dump to development database…" +info "2/3 Restoring production dump to development database…" +RESTORE_EXIT=0 pg_restore \ --no-owner \ --no-acl \ + --clean \ + --if-exists \ --dbname="$DEV_DB" \ - "$DUMP_FILE" -success " Restore complete." + "$DUMP_FILE" || RESTORE_EXIT=$? -# ── Sanitize ────────────────────────────────────────────────────────────────── +if [[ $RESTORE_EXIT -eq 0 ]]; then + success " Restore complete." +elif [[ $RESTORE_EXIT -eq 1 ]]; then + warn " Restore completed with warnings (non-fatal). Continuing…" +else + error "pg_restore failed with exit code $RESTORE_EXIT. Aborting." +fi + +# ── Sanitize (fix #4) ──────────────────────────────────────────────────────── info "" -info "4/4 Sanitizing development database…" +info "3/3 Sanitizing development database…" SANITIZE_RESULT=$(psql "$DEV_DB" --tuples-only --no-align << 'SQL' BEGIN; --- 1. Disable Discord webhook notifications for all leagues +-- 1. Count leagues with webhooks before clearing (accurate count) +SELECT 'discord_webhooks_nulled:' || COUNT(*) FROM leagues WHERE discord_webhook_url IS NOT NULL; UPDATE leagues SET discord_webhook_url = NULL; -SELECT 'discord_webhooks_nulled:' || COUNT(*) FROM leagues; -- 2. Anonymize PII for non-admin users only -- (admin accounts are untouched so admin pages remain accessible) @@ -149,9 +155,9 @@ ADMIN_COUNT=$(echo "$SANITIZE_RESULT" | grep 'admin_users_kept:' | cut echo "" success "✓ Sync complete. Sanitization summary:" -echo " • Discord webhook URLs nulled: ${LEAGUES_COUNT:-?} leagues" -echo " • Non-admin users anonymized: ${ANON_COUNT:-?} users" -echo " • Admin accounts preserved: ${ADMIN_COUNT:-?} users" +echo " • Discord webhook URLs cleared: ${LEAGUES_COUNT:-?} leagues" +echo " • Non-admin users anonymized: ${ANON_COUNT:-?} users" +echo " • Admin accounts preserved: ${ADMIN_COUNT:-?} users" echo "" warn "Reminder: ensure your .env does NOT contain:" echo " • RESEND_API_KEY (email sending)"