diff --git a/.github/workflows/migrate-staging.yml b/.github/workflows/migrate-staging.yml index 85644276..d7ac79ed 100644 --- a/.github/workflows/migrate-staging.yml +++ b/.github/workflows/migrate-staging.yml @@ -12,6 +12,26 @@ # separate `production` branch promotion, and auto-applying irreversible DDL to # prod on merge is a different risk decision. This runner has no down # migrations. +# +# THE SECRET MUST BE THE SESSION-MODE POOLER URI, NOT THE DIRECT ONE. +# `db..supabase.co` publishes only an AAAA record, and GitHub-hosted +# runners have no outbound IPv6 — a direct string fails with "Network is +# unreachable" / "server closed the connection unexpectedly" before it ever +# authenticates. (Same wall on a home network without a global IPv6 address; +# it is why staging migrations had to be applied by hand.) +# +# Use the pooler host on port 5432 — SESSION mode. Not 6543 (transaction mode), +# which drops the session-level behaviour psycopg and DDL rely on. Session mode +# behaves like a direct connection. Note the pooler also changes the username to +# `postgres.`. +# +# Take the host from the dashboard's Connect panel rather than assembling it: +# projects are assigned to NUMBERED pooler clusters (`aws-0-`, `aws-1-`, ...) +# and the number is NOT derivable from the region — staging and production are +# both us-west-2 yet sit on different clusters. A wrong prefix fails with +# "Tenant or user not found", which is at least distinguishable from a bad +# password. `backend/scripts/pooler_url.py` builds the URI from an env file so +# the password is never copied by hand. name: Migrate (staging) on: @@ -56,7 +76,7 @@ jobs: SUPABASE_DB_URL: ${{ secrets.STAGING_SUPABASE_DB_URL }} run: | if [ -z "${SUPABASE_DB_URL}" ]; then - echo "::notice::STAGING_SUPABASE_DB_URL is not set — skipping. Add the secret (the DIRECT connection string, port 5432, not the pooler) to enable." + echo "::notice::STAGING_SUPABASE_DB_URL is not set — skipping. Add the secret to enable: the SESSION-mode pooler URI on port 5432, user postgres.. Take the host from the dashboard's Connect panel — projects are assigned to NUMBERED pooler clusters (aws-0-, aws-1-, ...) and the number is not derivable from the region, so do not assume aws-0. 'python scripts/pooler_url.py .env.staging --raw' builds the URI from the env file. Not the direct db..supabase.co string — that is IPv6-only and unreachable from GitHub runners — and not port 6543, which is transaction mode." echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi diff --git a/CLAUDE.md b/CLAUDE.md index 833fc93e..1807f71d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,14 +45,18 @@ python -m pytest tests/ -q # backend test suite Database (run from `backend/`; migrations are raw DDL, never dashboard SQL): ``` -python -m db.migrate # apply pending migrations (needs SUPABASE_DB_URL = direct conn string) +python -m db.migrate # apply pending migrations (SUPABASE_DB_URL = SESSION-mode pooler URI, port 5432) python -m db.migrate --baseline # record migrations as applied without running them python -m db.seed_staging # idempotent fake demo dataset on the new schema ``` The `db/` scripts read `.env` by default; for staging/prod ops run them under `dotenv -f .env.staging run -- python -m db.