Skip to main content

ADR-039 โ€” 4-Lane CI Topology (budget-scoped testing)

Status: Accepted (2026-07-16) Date: 2026-07-16 Related: #4347 (removed PR checks on dev), #3585 (sharded coverage), #1913 (suite sharding) Team guide: docs/guides/ci-cd-4-lanes.md โ€” the short day-to-day version of this ADR

Contextโ€‹

CI ran the full suite (8 API shards with coverage, frontend build, agent tests, live OpenRouter evals, a macOS-native connector job at 10ร— billing) on EVERY push to dev โ€” 172 runs in the week of 2026-07-08 โ‰ˆ 13,000 Linux-min-equivalents. Measured change locality: 38% of merges touch api/, 33% frontend/, 3% agent/, 6% remote-desktop-agent/, 29% none of them. PRs into dev ran no CI at all (#4347), so compile/type/unit breakage landed on dev routinely.

The team is moving to a weekly release cadence (staging cut Monday 06:00 UTC, prod Wednesday 06:00 UTC). Motto: "keep prod stable; stability derives from proper testing on staging". A release-branch model was considered and DEFERRED โ€” the dev โ†’ staging โ†’ main branch flow stays.

Decision โ€” four lanesโ€‹

LaneTriggerRunsRequired?
1 Fast gatePR โ†’ dev (pr-checks.yml)lint, compile, UNIT tests (no containers), migrations check when api/migrations/** or api/src/database/** touchedYES โ€” PR Gate check
2 Post-mergepush โ†’ dev (ci.yml)integration suites, PATH-SCOPED to touched services; unit lane re-run; no coverage; no live evals; macOS connector only on remote-desktop-agent/** changesdev has no protection gate; red = fix forward
3 PromotionPR โ†’ staging/main (ci.yml)full unscoped suite + live evals (advisory) + trufflehog secret scan (+ e2e-smoke.yml, unchanged)YES โ€” CI Success
4 Nightlyschedule (nightly.yml โ†’ workflow_call ci.yml)FULL suite with coverage thresholds on merged report + live evals + full connector runSlack digest on failure

Manual full run any time: gh workflow run ci.yml --ref <ref> [-f coverage=true] [-f live_evals=true].

Mechanicsโ€‹

  • Unit/integration split: api/jest.classify.js classifies specs at config-load time by infrastructure tokens (DataSource, DATABASE_URL, REDIS_URL, ioredis, BullModule.forRoot, .listen(, โ€ฆ) with // @jest-lane unit|integration override markers. Unit lane (jest.config.unit.js) runs parallel workers with NO DATABASE_URL/REDIS_URL โ€” misclassification fails loudly. Integration lane (jest.config.integration.js) keeps --runInBand + service containers. jest.config.shard.js (full suite + coverage) is now nightly-only. Split at introduction: 482 unit / 152 integration of 634.
  • Path scoping on pushes: .github/actions/ci-scope classifies merge-base(before, sha)..sha; unusable SHAs or merge-base failure fail OPEN to full CI. Any .github/ change โ†’ full CI.
  • Coverage ratchet moved to nightly (accepted trade: a coverage regression is flagged next morning, not at merge).
  • Live evals: advisory (continue-on-error, outside ci-success) โ€” run on promotion PRs and nightly, never on pushes.
  • Secret scanning: trufflehog diff scan on promotion PRs (gitleaks was dropped when it went paid). Dependency/CVE scanning is a separate issue.

Consequencesโ€‹

  • ~60โ€“70% billed-minute reduction at current velocity; PR feedback in ~5 min.
  • Integration failures in untouched lanes surface at nightly/promotion, not per-merge (accepted; scoping is conservative and .github changes run full).
  • New spec files with infrastructure usage MUST be detectable by the token list or carry // @jest-lane integration โ€” the fail-loud unit lane enforces this automatically.
  • Branch protection on dev must require the PR Gate check (repo admin, after the first pr-checks.yml run has reported). Read-modify-write the protection settings โ€” the API PATCH replaces the whole required-checks list.