name: pg-real tests on: [pull_request] # Neither job writes anything back: they read the repo, stand up a throwaway # Postgres, and run tests. Without this block both inherit the repository's # default token permissions, which are broader than that. permissions: contents: read concurrency: group: pg-real-${{ github.ref }} cancel-in-progress: true jobs: coverage-gate: # Enforces the database.md rule: a migration touching a trigger/RPC/RLS/ # DEFERRABLE must come with a *.pg.test.ts change. Previously instruction- # only. Escape hatch: `-- pg-test: covered-by ` / `-- pg-test: skip # ()` comments inside the migration. runs-on: ubuntu-latest steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # Full history so the merge-base with the PR base branch exists. fetch-depth: 0 persist-credentials: false - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: 20 - name: Require pg-real coverage for trigger/RPC/RLS migrations env: PG_GATE_BASE: origin/${{ github.base_ref }} run: node scripts/check-pg-test-coverage.mjs # Proves that an EXISTING database survives the PR's migrations, which the # pg-real job below cannot: it applies every migration to an empty database, # so a NOT NULL, a CHECK, a unique index or a backfill passes trivially # against zero rows and can still fail (or silently corrupt) on production. # # Shape: schema at the merge-base -> seed real rows -> apply ONLY the new # migrations -> assert the rows are intact. A PR that adds no migration skips # straight past the apply step and costs one cheap no-op run. pg-upgrade: runs-on: ubuntu-latest services: postgres: image: supabase/postgres:15.8.1.060 env: POSTGRES_PASSWORD: postgres ports: - 5432:5432 options: >- --health-cmd "pg_isready -U postgres" --health-interval 5s --health-timeout 5s --health-retries 20 env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres PGPASSWORD: postgres steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: # Full history so the merge-base with the PR base branch exists. fetch-depth: 0 persist-credentials: false - name: Install psql client run: sudo apt-get update && sudo apt-get install -y --no-install-recommends postgresql-client - name: Identify the migrations this PR adds id: newmig env: BASE_REF: origin/${{ github.base_ref }} run: | set -euo pipefail MERGE_BASE=$(git merge-base "$BASE_REF" HEAD) echo "merge_base=$MERGE_BASE" >> "$GITHUB_OUTPUT" echo "Merge base: $MERGE_BASE" # Migrations present at HEAD but not at the merge base. Uses the git # tree, not the filesystem, so a rebase or a merge commit cannot make # an already-shipped migration look new. git ls-tree -r --name-only HEAD -- supabase/migrations \ | grep '\.sql$' | sort > /tmp/head-migrations.txt git ls-tree -r --name-only "$MERGE_BASE" -- supabase/migrations \ | grep '\.sql$' | sort > /tmp/base-migrations.txt comm -23 /tmp/head-migrations.txt /tmp/base-migrations.txt > /tmp/new-migrations.txt COUNT=$(wc -l < /tmp/new-migrations.txt | tr -d ' ') echo "count=$COUNT" >> "$GITHUB_OUTPUT" echo "New migrations ($COUNT):" cat /tmp/new-migrations.txt - name: Bootstrap storage schema if: steps.newmig.outputs.count != '0' run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -f tests/pg/bootstrap.sql - name: Apply the schema as it stands at the merge base if: steps.newmig.outputs.count != '0' env: MERGE_BASE: ${{ steps.newmig.outputs.merge_base }} run: | set -euo pipefail # Read each migration out of the merge-base tree rather than the # working tree: a PR that EDITS a shipped migration (forbidden, but # this job must not be the thing that hides it) still gets the # original applied here, so the edit shows up as a failure below. while read -r f; do echo "Applying (base) $f" git show "$MERGE_BASE:$f" | psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q done < /tmp/base-migrations.txt - name: Seed a real company with posted verifikat if: steps.newmig.outputs.count != '0' run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -f tests/pg/upgrade/seed.sql - name: Apply ONLY the new migrations, against existing data if: steps.newmig.outputs.count != '0' run: | set -euo pipefail while read -r f; do echo "Applying (new) $f" psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -f "$f" done < /tmp/new-migrations.txt - name: Assert the existing data survived if: steps.newmig.outputs.count != '0' run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -f tests/pg/upgrade/assert.sql - name: No migrations in this PR if: steps.newmig.outputs.count == '0' run: echo "This PR adds no migration; nothing to upgrade-test." pg-real: runs-on: ubuntu-latest services: postgres: # Supabase image ships the auth schema, auth.uid(), and the extensions # (uuid-ossp, pg_cron, btree_gist, vector) this repo's migrations need. # Plain postgres:15 would require manual bootstrap SQL. image: supabase/postgres:15.8.1.060 env: POSTGRES_PASSWORD: postgres ports: - 5432:5432 options: >- --health-cmd "pg_isready -U postgres" --health-interval 5s --health-timeout 5s --health-retries 20 env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres PGPASSWORD: postgres steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false # Deliberately no `cache: npm` here. This workflow runs on pull_request # only, so the cache is never written on main, and GitHub scopes caches # by ref: every PR uploaded its own 284 MB copy under an identical key # that no other PR could ever restore. Fourteen dead copies (4 GB, 40% of # the repo quota) accumulated in a single day. If this is ever worth # caching again, it has to be actions/cache/save on main plus # actions/cache/restore here, which is the only shape that gets hits. - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: 20 - run: npm ci - name: Install psql client run: sudo apt-get update && sudo apt-get install -y --no-install-recommends postgresql-client - name: Bootstrap storage schema # The supabase/postgres image ships a partial storage schema; the rest # is provisioned by the storage-api service at runtime, which we do # not run in CI. This aligns the schema with what migrations expect. run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -f tests/pg/bootstrap.sql - name: Apply migrations run: | set -euo pipefail shopt -s nullglob files=(supabase/migrations/*.sql) if [ ${#files[@]} -eq 0 ]; then echo "No migration files found" exit 1 fi for f in "${files[@]}"; do echo "Applying $f" psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -f "$f" done - run: npm run test:pg tool-pg: # MCP tools driven through a REAL supabase-js client against a REAL # PostgREST. This is NOT a duplicate of the pg-real job: that one holds a # `pg` Pool and writes SQL, which cannot see the half of a tool that # PostgREST resolves (the .select() column strings, the resource embeds, # the or=(...) grammar). Before this job, all 100 files in # extensions/general/mcp-server/__tests__ faked supabase and nothing in CI # exercised that surface. runs-on: ubuntu-latest services: postgres: image: supabase/postgres:15.8.1.060 env: POSTGRES_PASSWORD: postgres ports: - 5432:5432 options: >- --health-cmd "pg_isready -U postgres" --health-interval 5s --health-timeout 5s --health-retries 20 env: DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres TOOL_PG_DATABASE_URL: postgresql://postgres:postgres@localhost:5432/postgres TOOL_PG_REST_URL: http://127.0.0.1:3000 PGPASSWORD: postgres steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: persist-credentials: false - uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 with: node-version: 20 - run: npm ci - name: Install psql client run: sudo apt-get update && sudo apt-get install -y --no-install-recommends postgresql-client - name: Bootstrap storage schema run: psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -f tests/pg/bootstrap.sql - name: Default privileges for the supabase roles # Without these PostgREST answers every request with 42501, because the # grants have to exist before the migrations create ~400 tables. run: | psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -c " GRANT USAGE ON SCHEMA public TO postgres, anon, authenticated, service_role; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO postgres, anon, authenticated, service_role; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON ROUTINES TO postgres, anon, authenticated, service_role; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO postgres, anon, authenticated, service_role; " - name: Apply migrations run: | set -euo pipefail shopt -s nullglob files=(supabase/migrations/*.sql) if [ ${#files[@]} -eq 0 ]; then echo "No migration files found"; exit 1; fi for f in "${files[@]}"; do psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -f "$f" done - name: Grant on everything the migrations created run: | psql "$DATABASE_URL" -v ON_ERROR_STOP=1 -q -c " GRANT ALL ON ALL TABLES IN SCHEMA public TO postgres, anon, authenticated, service_role; GRANT ALL ON ALL ROUTINES IN SCHEMA public TO postgres, anon, authenticated, service_role; GRANT ALL ON ALL SEQUENCES IN SCHEMA public TO postgres, anon, authenticated, service_role; " - name: Start PostgREST # Deliberately `docker run --network host` rather than a service # container. Service containers on a non-containerized job are reachable # from the runner on localhost, but NOT from each other by name, and # PostgREST has to reach Postgres. Host networking sidesteps that. run: | docker run -d --name postgrest --network host \ -e PGRST_DB_URI="postgres://postgres:postgres@127.0.0.1:5432/postgres" \ -e PGRST_DB_SCHEMAS=public \ -e PGRST_DB_ANON_ROLE=anon \ -e PGRST_JWT_SECRET="super-secret-jwt-token-with-at-least-32-characters-long" \ -e PGRST_DB_MAX_ROWS=100000 \ -e PGRST_SERVER_PORT=3000 \ postgrest/postgrest:v12.2.3 for _ in $(seq 1 60); do if curl -sf -o /dev/null "http://127.0.0.1:3000/" ; then break; fi sleep 1 done curl -sf -o /dev/null "http://127.0.0.1:3000/" || (docker logs postgrest && exit 1) - run: npm run test:tools