diff --git a/.github/workflows/migrations.yml b/.github/workflows/migrations.yml new file mode 100644 index 0000000000..e80985f2ed --- /dev/null +++ b/.github/workflows/migrations.yml @@ -0,0 +1,71 @@ +name: Migrations + +# Replaces the "Supabase Preview" check, which never worked: it fired +# only on migration changes and then either 404'd in four seconds +# ("Postgres config not found") or hung forever waiting on a branch +# action, because two stale dashboard integrations were pointed at this +# repo and neither could provision. It never applied a migration, so +# every `supabase/migrations/*.sql` file in this repo has shipped +# unexecuted by CI. +# +# This job does the thing that check only claimed to: boot a clean +# Postgres and replay every migration, in order, from nothing. It needs +# no secrets and no Supabase account — the CLI runs the database in a +# container on the runner. + +on: + pull_request: + # `supabase/**` rather than `supabase/migrations/**` so a change to + # config.toml re-runs the check too, and this workflow is listed so + # that edits to the check are validated by the check. + # + # NOTE: if this is ever made a REQUIRED status check, drop these + # filters. GitHub leaves a required check that never ran stuck as + # "expected", which blocks every PR that doesn't touch SQL. + paths: + - 'supabase/**' + - '.github/workflows/migrations.yml' + push: + branches: [main] + paths: + - 'supabase/**' + - '.github/workflows/migrations.yml' + +concurrency: + group: migrations-${{ github.ref }} + cancel-in-progress: true + +jobs: + apply: + name: Apply to a clean database + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install the Supabase CLI + uses: supabase/setup-cli@v1 + with: + # Pinned rather than `latest`: this check is infrastructure, + # and a CLI release changing behaviour should be a deliberate + # bump in a PR, not a Tuesday where main goes red. + version: 2.113.0 + + - name: Start Postgres + run: supabase db start + + # `db start` already applies the migrations on a cold volume, but + # only on a cold volume — it is a no-op against a database that + # already exists. `db reset` drops and rebuilds unconditionally, + # so this step, not the one above, is the guarantee that every + # migration ran in filename order against nothing. It fails on the + # first statement Postgres rejects. --no-seed because there is no + # seed script and this is about schema, not data. + - name: Replay every migration from scratch + run: supabase db reset --local --no-seed + + # A migration can apply "successfully" and still do nothing — + # every DDL statement here is IF NOT EXISTS-guarded, which turns a + # typo'd object name into a silent no-op. Assert the outcome, not + # just the absence of an error. + - name: Verify the resulting schema + run: supabase db query --local --file supabase/ci/verify-schema.sql diff --git a/supabase/ci/verify-schema.sql b/supabase/ci/verify-schema.sql new file mode 100644 index 0000000000..d3c5b23928 --- /dev/null +++ b/supabase/ci/verify-schema.sql @@ -0,0 +1,62 @@ +-- Post-migration assertions for the CI job in +-- `.github/workflows/migrations.yml`. +-- +-- `supabase db reset` already fails on any statement Postgres rejects, +-- so this is not about syntax. It's about the quieter failure: a +-- migration that applies cleanly and does nothing. Every DDL statement +-- in this repo is guarded with IF NOT EXISTS / ON CONFLICT so the files +-- can be re-run safely, and that same guard turns a typo'd object name +-- into a silent no-op with a green checkmark. +-- +-- Keep this thin. It is a smoke test for "did the migrations actually +-- build the schema", not a spec of it — asserting every column here +-- would just be the migrations restated in a second place, drifting. +DO $$ +BEGIN + -- The core tables, from 001. + IF to_regclass('public.messages') IS NULL THEN + RAISE EXCEPTION 'public.messages is missing — migrations did not apply'; + END IF; + IF to_regclass('public.whatsapp_config') IS NULL THEN + RAISE EXCEPTION 'public.whatsapp_config is missing — migrations did not apply'; + END IF; + + -- Supabase provides the storage schema; migrations 016/020/023 write + -- to it. If it is absent the bucket migrations silently accomplish + -- nothing, which is precisely the case a plain "no errors" run hides. + IF to_regclass('storage.buckets') IS NULL THEN + RAISE EXCEPTION + 'storage.buckets is missing — the storage schema was not available when the bucket migrations ran'; + END IF; + + -- Buckets are UPSERTed, so their absence means the INSERT never ran. + IF NOT EXISTS (SELECT 1 FROM storage.buckets WHERE id = 'chat-media') THEN + RAISE EXCEPTION 'the chat-media bucket row was not created (migration 023)'; + END IF; + IF NOT EXISTS (SELECT 1 FROM storage.buckets WHERE id = 'flow-media') THEN + RAISE EXCEPTION 'the flow-media bucket row was not created (migration 016)'; + END IF; + + -- Account scoping (017) is load-bearing for every RLS policy. + IF to_regclass('public.accounts') IS NULL THEN + RAISE EXCEPTION 'public.accounts is missing — migration 017 did not apply'; + END IF; + + RAISE NOTICE 'schema verification passed'; +END +$$; + +-- Two things this file has already been burned by, both verified in CI +-- rather than assumed: +-- +-- 1. It must contain EXACTLY ONE statement. `supabase db query --file` +-- sends the whole file as a prepared statement, and a second +-- top-level statement fails with the distinctly unhelpful "cannot +-- insert multiple commands into a prepared statement" (commit +-- f91a6c8). Add assertions INSIDE the DO block above; do not append +-- a second one. +-- +-- 2. A RAISE in here really does fail the job. A deliberately false +-- assertion (commit 42c7db0, run 31579334056) surfaced as +-- `failed to execute query: error: ...` and exited 1. This is not a +-- decorative green tick. diff --git a/supabase/config.toml b/supabase/config.toml new file mode 100644 index 0000000000..9f66337d37 --- /dev/null +++ b/supabase/config.toml @@ -0,0 +1,24 @@ +# Supabase CLI configuration. +# +# This file exists for ONE reason: the CI job in +# `.github/workflows/migrations.yml` needs a project directory the CLI +# recognises before it can boot a throwaway Postgres and replay +# `supabase/migrations/` against it. +# +# It is NOT a mirror of the hosted project's settings. Auth providers, +# storage limits, SMTP, rate limits and the rest are configured in the +# Supabase dashboard and are deliberately left out here rather than +# duplicated into a second source of truth that would quietly drift. +# Run `supabase init` if you ever want the CLI's full annotated +# reference version of this file. +project_id = "wacrm" + +[db] +port = 54322 +shadow_port = 54320 +# Keep this aligned with the hosted project's Postgres version +# (Dashboard → Settings → Infrastructure). A mismatch means CI can +# green-light SQL the production database would reject, which is the +# one failure mode this whole check exists to prevent. 17 is the CLI's +# current default, not a reading of the live project. +major_version = 17