Skip to content

CI/CD

All GitHub Actions workflows live in .github/workflows/. There are three:

File Trigger Purpose
ci.yml Pull requests + pushes to main, demo Migration guard, lint, type-check ratchet, unit tests, build, bundle budgets, DB tests, edge-function deno check, Playwright e2e
release.yml workflow_run when CI completes on main Run semantic-release to tag and changelog
supabase-backup.yml Daily cron at 02:00 UTC + workflow_dispatch Dump Postgres schema + data (see backup-restore.md)

CI (ci.yml)

Jobs:

Job Runs when What Typical time
ci always migration guard, lint, type-check ratchet, unit tests, build, bundle budgets ~5–7 min
changes always dorny/paths-filter — decides whether db-tests / deno-check run ~10 s
db-tests supabase/migrations/**, supabase/tests/**, supabase/config.toml, scripts/db-test.sh or ci.yml changed supabase start (Postgres only, all migrations applied) → scripts/db-test.sh ~3–4 min
deno-check supabase/functions/**, scripts/deno-check-functions.sh or ci.yml changed deno check every edge function ~1–2 min
e2e after ci, not for Dependabot Playwright —

See PLT-020 for why these are the merge gates.

Steps in the ci job

  1. Checkout
  2. Setup Node.js 24 with npm cache
  3. node scripts/check-migrations.mjs — fails on duplicate migration versions / bad filenames (runs before npm ci, ~0.1 s)
  4. npm ci
  5. npm run lint
  6. npm run typecheck:ratchet — fails on type errors not in scripts/typecheck-baseline.txt
  7. npm run test:run (Vitest, non-watch mode)
  8. npm run build — production Vite build, needs all VITE_* secrets in the environment
  9. node scripts/check-bundle-budgets.mjs — see bundle-budgets.md
  10. Upload dist/ as an artifact (3-day retention)

A failure at any step fails the PR check.

db-tests job

Starts only the Postgres container with supabase start -x <every other service> — the CLI applies every migration while starting — then runs scripts/db-test.sh against 127.0.0.1:54322. A test file passes only if psql exits 0 and prints its ALL … PASSED line. Reproduce locally: testing.md → Database tests.

deno-check job

denoland/setup-deno@v2 (latest 2.x) then scripts/deno-check-functions.sh, which checks each supabase/functions/*/index.ts separately so one broken function doesn't hide the rest.

Steps in the e2e job

Runs after ci succeeds. Installs Chromium, runs npm run e2e (Playwright), uploads the HTML report on failure.

Tip

If you are a Dependabot dep-bump PR author, the e2e job is deliberately skipped (the bot doesn't have access to VITE_SUPABASE_URL etc.). The ci job alone is authoritative for dep bumps.

Reproducing a failing check locally

# match CI exactly
nvm use 24                        # or fnm use 24
npm ci
node scripts/check-migrations.mjs
npm run lint
npm run typecheck:ratchet
npm run test:run
npm run build
node scripts/check-bundle-budgets.mjs
scripts/db-test.sh                  # needs local Supabase, see testing.md
scripts/deno-check-functions.sh

For the e2e job:

npx playwright install --with-deps chromium
npm run e2e
# open the last run's report:
npm run e2e:report

Common gotchas:

  • Build fails locally with "missing env": copy env.example to .env.local and fill in the VITE_* values. These must be present at build time (Vite inlines them).
  • Bundle-budget failure locally but green on CI (or vice versa): you forgot npm run build before running the budget script. The script reads dist/assets/ and exits early if the directory is missing.
  • Type-check ratchet fails with "NEW type error(s)": fix the listed errors. npm run build does not type-check (Vite strips types with SWC), so the ratchet is the only gate. Line numbers are ignored when comparing, but editing an error's message (e.g. renaming a type it mentions) makes it "new" — regenerate the baseline with npm run typecheck:ratchet -- --update only in the PR that genuinely changed it, and never to add unrelated errors.
  • Ratchet prints "FIXED" errors: not a failure; run npm run typecheck:ratchet -- --update and commit the smaller baseline.
  • Migration guard fails with "version … is used by 2 files": two branches picked the same timestamp. Rename the migration that is not yet applied to production to a new timestamp (see commit 70c8db1).

Release (release.yml)

Fires only after a successful CI run on main (on: workflow_run: workflows: ["CI"], branches: [main]). If CI fails, no release happens.

The job checks out full history (for tag detection), installs deps, and runs:

npx semantic-release

Configuration lives in .releaserc.json. Summary of behaviour:

  • Branches: only main produces releases.
  • Tag format: v${version}.
  • Version bumps (Angular commit-message convention):
Commit type Bump
feat: minor
fix: patch
perf: patch
revert: patch
refactor: patch
Footer BREAKING CHANGE: anywhere major
docs:, style:, test:, chore:, ci: no release
  • Changelog: CHANGELOG.md is regenerated and committed back to main with the commit message chore(release): <version> [skip ci] so the release commit does not re-trigger CI.
  • GitHub Release: a release is published with the generated notes.
  • npm: npmPublish: false — this is a private app, no package is pushed.

Warning

Only the commit subject line is parsed for the release type. A commit subject of chore: small tweak that quietly includes a breaking refactor will not trigger a major bump. Put BREAKING CHANGE: <explanation> in the commit footer (or use feat!: / fix!: shorthand) if the behaviour actually changed.

What happens when release fails

  1. Check Actions → Release for the failed run.
  2. Common cause: no releasable commits since the last tag (every commit on main was docs:, chore:, etc.). This is not an error — semantic-release logs "There are no relevant changes, so no new version is released" and exits 0.
  3. If the job genuinely errored (auth, tag conflict), re-trigger with the Actions → Release → Run workflow button, or push an empty commit to main (git commit --allow-empty -m "chore: retry release") to drive another CI → Release cycle.

Supabase Backup (supabase-backup.yml)

Covered in backup-restore.md. Summary: daily cron dumps schema-only and data-only SQL, gzips, checksums, uploads as a 30-day artifact, optionally rotates to S3.

Adding a new workflow

  1. Place the YAML in .github/workflows/.
  2. Pin action versions by major tag (actions/checkout@v6, actions/setup-node@v6) — matches the convention in existing workflows.
  3. Secrets go through ${{ secrets.NAME }}. Never hardcode or echo them into logs.
  4. If the workflow needs Node, use Node 24 with cache: 'npm'.
  5. Add a row to the table at the top of this doc.