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
- Checkout
- Setup Node.js 24 with npm cache
node scripts/check-migrations.mjs— fails on duplicate migration versions / bad filenames (runs beforenpm ci, ~0.1 s)npm cinpm run lintnpm run typecheck:ratchet— fails on type errors not inscripts/typecheck-baseline.txtnpm run test:run(Vitest, non-watch mode)npm run build— production Vite build, needs allVITE_*secrets in the environmentnode scripts/check-bundle-budgets.mjs— seebundle-budgets.md- 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.exampleto.env.localand fill in theVITE_*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 buildbefore running the budget script. The script readsdist/assets/and exits early if the directory is missing. - Type-check ratchet fails with "NEW type error(s)": fix the listed errors.
npm run builddoes 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 withnpm run typecheck:ratchet -- --updateonly 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 -- --updateand 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:
Configuration lives in .releaserc.json. Summary of behaviour:
- Branches: only
mainproduces 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.mdis regenerated and committed back tomainwith the commit messagechore(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
- Check Actions → Release for the failed run.
- Common cause: no releasable commits since the last tag (every commit on
mainwasdocs:,chore:, etc.). This is not an error —semantic-releaselogs "There are no relevant changes, so no new version is released" and exits 0. - 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
- Place the YAML in
.github/workflows/. - Pin action versions by major tag (
actions/checkout@v6,actions/setup-node@v6) — matches the convention in existing workflows. - Secrets go through
${{ secrets.NAME }}. Never hardcode orechothem into logs. - If the workflow needs Node, use Node 24 with
cache: 'npm'. - Add a row to the table at the top of this doc.