Skip to content

Continuous integration

The verify pipeline lives in .github/workflows/ci.yml. It runs the same gate a developer runs locally before a PR (see the Pre-PR gate in development.md), split across parallel jobs so wall-clock is the slowest leg rather than the sum of every step.

When it runs

on:
  push: { branches: [main, develop] }
  pull_request: { branches: [main, develop] }

develop is the working branch and main is the release branch; both are gated, on direct pushes and on PRs targeting them. A concurrency group keyed on the ref cancels an in-progress run when a newer commit lands on the same branch, so only the tip is ever verified.

The jobs

All jobs install with pnpm install --frozen-lockfile on the Node version in .nvmrc, then run in parallel:

JobWhat it runsNotes
lint-typecheckpnpm lint, pnpm typecheckThe lightest leg; usually finishes first.
repo-checkscodegen drift (gen:schema/gen:events/gen:routers --check), check:tiers, check:modules, check:i18n, check:mcpCross-package invariants. Isolated so a stale generated file can't masquerade as a lint/type failure.
migration-driftcheck:migrationsReplays every committed migration into a throwaway postgres:16 service and diffs the result against schema.prisma. Catches a modeled change shipped without a migration, or a hand-edited migration.
unit-testspnpm test:coverageUploads each package's coverage/unit/ as an artifact.
integration-testspnpm test:integration:coverageThe slow leg (~3–4 min); each package boots a Postgres testcontainer in globalSetup. Uploads coverage/integration/.
coverage-uploadpnpm coverage:merge → Codecovneeds: [unit-tests, integration-tests]. Fuses the two coverage sets per package, enforces the per-package floors (below), then uploads to Codecov.

The codegen-drift check compares the committed generated files to a fresh re-render EOL-insensitively (a Windows-committed CRLF artifact vs the Linux runner's LF render is not real drift); pnpm gen:schema also normalizes source line endings to LF on read so its output is byte-identical across platforms.

The coverage-floor gate

coverage-upload fails the build if any package drops below its floor, so a regression in a tested package breaks CI rather than silently eroding coverage. The floors live in tools/merge-coverage.ts as THRESHOLDS, one entry per package { lines, statements, functions, branches }.

Two rules the gate enforces:

  • Every package that emits coverage MUST have an entry. A package that produces a coverage report but has no floor fails the gate ("produced coverage but has no THRESHOLDS entry") ; this is how a newly-added module can't ship un-gated. A package with no test:coverage script produces no report and is listed as "skipped" (e.g. pure libraries covered only via their consumers).
  • Floors are a regression guard, not a brag. Set each metric a few points below the measured baseline (round down to the nearest 5 %). Ratchet a floor up when new tests justify it; only lower one as a conscious decision, with a comment saying why.

When you add a module with tests, add its floor in the same PR. For a module whose real coverage comes mostly from an integration suite (a router / webhook), a floor set off the unit-only number is a safe lower bound (merged ≥ unit); ratchet it up once a merged CI run publishes the real number.

End-to-end tests are manual

The Playwright e2e suite (.github/workflows/e2e.yml) is workflow_dispatch only : it boots a full Supabase + api + web stack and costs ~20 min, too much to pay on every push for what is primarily an auth/admin regression net. Run it from the Actions tab (Actions → E2E → Run workflow → pick a branch) when touching those surfaces; developers run pnpm test:e2e locally otherwise.

Other workflows

Alongside the verify pipeline, security + quality workflows run on push/PR:

  • gitleaks : secret scanning (allowlist in .gitleaks.toml).
  • codeql : static analysis.
  • dependency-review : flags risky dependency changes on PRs.
  • lighthouse : web performance/accessibility budget.

Relationship to the local gate

CI is the enforcement copy of the local pre-PR gate:

pnpm gen && pnpm typecheck && pnpm lint && pnpm test && \
  pnpm check:tiers && pnpm check:modules && pnpm check:i18n && pnpm check:mcp

Run that (plus pnpm test:integration and pnpm coverage:merge if you touched tested code) before pushing, and CI should be green on the first try. The one gap the local gate can't fully reproduce is the merged coverage number (unit + integration), which only coverage-upload computes.