CI/CD
All GitHub Actions workflows live in .github/workflows/. There are three:
| File | Trigger | Purpose |
|---|---|---|
ci.yml |
Pull requests + pushes to main, demo |
Lint, unit tests, build, bundle budgets, 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)
Runs two jobs: ci (lint + test + build + bundle-size) and e2e (Playwright, skipped for Dependabot because it can't see repo secrets).
Steps in the ci job
- Checkout
- Setup Node.js 24 with npm cache
npm cinpm run lintnpm 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.
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
npm run lint
npm run test:run
npm run build
node scripts/check-bundle-budgets.mjs
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. - Typecheck drift: CI doesn't run a standalone
tsc --noEmit, butnpm run buildinvokes the TypeScript compiler through Vite. Runnpx tsc --noEmitlocally if you want the faster, type-only check (CLAUDE.mdrequires this to be clean before PR review).
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.