Wave 1 — stop the bleeding: three CRITICALs, the cost cap, and ADR-0073/0074 #756
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Relavium CI — the green gate every push and PR must pass (Phase 0 · 0.G). | |
| # | |
| # Runs lint + typecheck + test across every workspace in dependency order with the | |
| # Turborepo cache, plus the formatting check and the no-vendor-type-across-the-seam fence. | |
| # | |
| # Branch protection (set in the GitHub repo settings, not here): `ci` and `coverage` are the REQUIRED | |
| # status checks to merge into `main`. Every other job self-labels `(advisory)` in its `name:`, which is | |
| # what shows in the PR checks list — `floor-check`, `peer-dep-gate` and `windows-concurrency`. | |
| # | |
| # `coverage` enforces the testing.md >=90% line+branch floor for `@relavium/llm` and `@relavium/mcp` only. | |
| # `packages/core` is measured and printed by the same run but does not fail it: its branch margin is +0.83 | |
| # (90.83 measured 2026-07-29) and Phase 2.5.5 Waves 1-3 edit `core` heavily, so blocking merges on a | |
| # sub-1-point margin would red-CI real work for no defect. That is a scoped, dated ruling with a promotion | |
| # trigger — Wave 3's test-coverage items — not an open-ended exemption. A local `pnpm coverage` still | |
| # enforces all three, so the floor never silently relaxes for a developer. | |
| # | |
| # Caching: the always-on layer is the GitHub Actions `.turbo` cache (restored/saved below), | |
| # which makes a no-change re-run a Turborepo cache hit — the M0 "demonstrably hitting" | |
| # criterion. The Vercel-style Turborepo REMOTE cache (cross-runner sharing) is opt-in: add | |
| # repo secrets `TURBO_TOKEN` and `TURBO_TEAM` and Turbo picks them up automatically below. | |
| # | |
| # Third-party actions are pinned to a full commit SHA (the `# vX.Y.Z` comment tracks the | |
| # human-readable release) so a moved tag can't inject unreviewed code; bumps are deliberate. | |
| # | |
| # Install-script posture: `pnpm install` runs WITHOUT `--ignore-scripts` deliberately. Supply-chain risk is | |
| # handled more precisely one level up, by `pnpm.onlyBuiltDependencies` in the root package.json, which | |
| # allowlists the ONLY two packages permitted to run lifecycle scripts (`better-sqlite3` for its native | |
| # prebuild, `esbuild` for its platform binary). Every other transitive dependency is already blocked. | |
| # Passing `--ignore-scripts` here would break both of those and gain nothing — it is a weaker, blunter form | |
| # of a control the repo already applies. | |
| name: CI | |
| on: | |
| push: | |
| branches: [main, development] | |
| pull_request: | |
| # Cancel superseded runs so CI tracks the latest push — but never cancel a run on `main` (its | |
| # history is the merge record and must complete). The group key: a PR run is keyed by its PR | |
| # number (`github.event.number` — so two PRs open from the SAME head branch stay independent and | |
| # can't cancel each other), a push run by its short branch (`ref_name`, e.g. `development`). | |
| # `event_name` keeps a push and its same-SHA PR in separate groups (otherwise one run cancels the | |
| # other and a cancelled required check can block merge): `ci-CI-push-development` vs | |
| # `ci-CI-pull_request-10`. | |
| concurrency: | |
| group: ci-${{ github.workflow }}-${{ github.event_name }}-${{ github.event.number || github.ref_name }} | |
| cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} | |
| permissions: | |
| contents: read | |
| env: | |
| # Picked up by Turbo for remote caching when the secrets are configured; harmless if empty. | |
| TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} | |
| TURBO_TEAM: ${{ secrets.TURBO_TEAM }} | |
| jobs: | |
| ci: | |
| name: lint · typecheck · test | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 # a hung step fails fast instead of inheriting the 360-min default | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| persist-credentials: false | |
| - name: Set up pnpm | |
| # pnpm version comes from the root package.json `packageManager`. | |
| uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 | |
| - name: Set up Node | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Install (frozen lockfile) | |
| run: pnpm install --frozen-lockfile | |
| - name: Restore Turborepo cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: .turbo | |
| key: turbo-${{ runner.os }}-${{ github.sha }} | |
| restore-keys: | | |
| turbo-${{ runner.os }}- | |
| # The required gate: lint, typecheck, and test every workspace, then build. | |
| - name: Lint, typecheck, test | |
| run: pnpm turbo run lint typecheck test | |
| # Typecheck the root/tooling *.config.ts that sit outside every package's tsc program. | |
| - name: Typecheck tooling configs | |
| run: pnpm typecheck:tools | |
| # Not redundant with typecheck: `build` runs each package's emit (tsconfig.build.json | |
| # → dist), which `typecheck` (tsconfig.json --noEmit) does not — it catches | |
| # emit-only failures and proves the published artifact compiles. | |
| - name: Build | |
| run: pnpm turbo run build | |
| # Drift gate (2.L · ADR-0051): the published CLI bundle inlines the @relavium/* engine and externalizes | |
| # every third-party dep — its external import closure must equal the declared `dependencies`, or a global | |
| # `npm i -g relavium` would fail to resolve a missing dep. Needs the build above. | |
| - name: CLI bundle closure matches its declared dependencies | |
| run: pnpm lint:bundle-closure | |
| # Drift gate: the committed migration must match src/schema.ts. If a schema change | |
| # (or an upstream @relavium/shared enum change the CHECKs derive from) wasn't | |
| # regenerated via `pnpm --filter @relavium/db db:generate`, regenerating here produces | |
| # an uncommitted diff and fails — turning a silent stale-migration into a red check. | |
| - name: DB migration is in sync with the schema | |
| run: | | |
| pnpm --filter @relavium/db db:generate | |
| if [ -n "$(git status --porcelain packages/db/drizzle)" ]; then | |
| echo "::error::The committed @relavium/db migration is out of sync with src/schema.ts (or an upstream @relavium/shared enum the CHECKs derive from). Regenerate and commit: pnpm --filter @relavium/db db:generate" | |
| git status --porcelain packages/db/drizzle | |
| exit 1 | |
| fi | |
| # Formatting is run, not argued (code-style-typescript.md). Via turbo so it caches. | |
| - name: Format check | |
| run: pnpm turbo run format:check | |
| # Prove the no-vendor-type-across-the-@relavium/llm-seam fence is live (0.F). | |
| - name: Seam fence is enforced | |
| run: pnpm lint:fence-check | |
| # The size half of the fence: engine packages' runtime dependency graphs must stay | |
| # within their reviewed allowlists (a new engine dep = an ADR + a deliberate edit to | |
| # tools/engine-deps/check.mjs in the same change). | |
| - name: Engine dependency allowlist | |
| run: pnpm lint:engine-deps | |
| # `tools/` is real code that gates real things (the seam fence, the bundle-closure guard, the | |
| # models.dev sync). The root `ci` script has always linted it; this job never did, so a lint error in | |
| # a guard script could merge while `pnpm run ci` was red locally (#312). | |
| # NB: it is `pnpm run ci`, never `pnpm ci` — pnpm reserves `ci` as a builtin and answers | |
| # `ERR_PNPM_CI_NOT_IMPLEMENTED`, so the script is unreachable by the name everyone types. | |
| - name: Lint the tooling scripts | |
| run: pnpm lint:tools | |
| # RUN the artifact this job just built, through the SAME `pnpm smoke:cli` script the root `ci` script | |
| # calls — a check that exists in only one of the two is exactly the #312 divergence this change closes. | |
| # Until now nothing in the required gate executed | |
| # `apps/cli/dist/index.js` — only the advisory Windows leg and the tag-gated release smoke did (#294) — | |
| # so a bundle that builds but cannot boot merged green. The `run --json` leg additionally proves the | |
| # migrations resolve beside the bundle, which is the failure `apps/cli/drizzle/**` becoming a declared | |
| # turbo output (#315) exists to prevent: a cache-hit replay used to leave `dist/` fresh next to a | |
| # missing `drizzle/`, crashing on first DB touch with nothing red anywhere. | |
| - name: Smoke the compiled binary | |
| run: pnpm smoke:cli | |
| # Supported-floor gate (ADR-0067). TWO floors, deliberately different: | |
| # * the PUBLISHED floor is `apps/cli` `engines.node` = `>=22` — the max constraint in the CLI's RUNTIME | |
| # dependency closure (`ink@7` / `cli-truncate` / `slice-ansi` → `>=22`); nothing runtime needs more. | |
| # * the DEV-INSTALL floor is the root `engines.node` = `>=22.13.0` — forced ONLY by devDependencies | |
| # (`vite` via vitest → `>=22.12.0`; `eslint-visitor-keys` via eslint → `^22.13.0`). `.npmrc` sets | |
| # `engine-strict=true`, so `pnpm install` on the whole workspace HARD-FAILS below it. | |
| # A workspace install therefore cannot run at 22.0.0, so this leg pins the LOWEST Node the repo installs | |
| # on (22.13.0). It still exercises the published 22 line end-to-end and proves better-sqlite3 resolves its | |
| # Node-22 prebuild (ABI 127 — one binary for ALL of 22.x, so 22.13.0 loads the same artifact 22.0.0 would; | |
| # no node-gyp C++ source build). What it does NOT prove is that no RUNTIME dep requires >22.0.0 — that is | |
| # a property of the prod closure, verified at the ADR (see ADR-0067's amendment note) rather than here. | |
| # A SEPARATE job (the required check stays the ubuntu `ci` job on Node 24); promote once confirmed stable. | |
| floor-check: | |
| name: node 22-line floor (22.13.0) · typecheck · test · build (advisory) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| # Fully isolate this job from the shared Turbo remote cache. Turbo's task hash does NOT include the | |
| # runtime Node version, so floor-check (Node 22) and the required `ci` job (Node 24) share a hash — | |
| # either could replay the other's cached result and mask a version-specific failure. `--force` (below) | |
| # stops floor-check READING the shared cache; blanking the inherited workflow-level tokens here stops it | |
| # WRITING into it, so the required `ci` job can never replay a Node-22 result as Node-24-proven. | |
| env: | |
| TURBO_TOKEN: '' | |
| TURBO_TEAM: '' | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 | |
| - name: Set up Node (the lowest 22.x the workspace installs on) | |
| uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version: 22.13.0 | |
| cache: pnpm | |
| - name: Install (frozen lockfile — better-sqlite3 must resolve a prebuild, not source-build) | |
| run: pnpm install --frozen-lockfile | |
| - name: Typecheck, test, build on the exact floor | |
| # `--force` = no cache READ (genuine Node-22 run, not a Node-24 replay); the job-level blanked | |
| # TURBO_TOKEN/TURBO_TEAM above = no cache WRITE (so `ci` never replays this Node-22 result). Both are | |
| # needed — see the job-level comment. | |
| run: pnpm turbo run typecheck test build --force | |
| # CI-only strict peer-dependency gate (.npmrc keeps this OFF for local installs so fresh | |
| # checkouts resolve while the surface packages' peers are not all in the tree yet). Catch | |
| # peer drift here without breaking local dev. | |
| peer-dep-gate: | |
| name: strict peer-dependency check (advisory) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Install with strict peers | |
| run: pnpm install --frozen-lockfile --config.strict-peer-dependencies=true | |
| # Engine coverage floor (testing.md >=90% line+branch, exit criterion #5). A REQUIRED check named | |
| # `engine coverage floor (llm, mcp)` — kept as a SEPARATE job from `ci` so a coverage regression is | |
| # legible on its own line rather than buried in a 12-step job. It is a repo-ROOT vitest run, which is what | |
| # makes the root-relative per-glob thresholds authoritative (vitest.config.ts). `packages/core` is measured | |
| # but not enforced here; see the header for the ruling and its promotion trigger. | |
| coverage: | |
| name: engine coverage floor (llm, mcp) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Install (frozen lockfile) | |
| run: pnpm install --frozen-lockfile | |
| # Share the same Turborepo local cache the `ci` job writes, so the build below is a warm cache hit | |
| # on a same-SHA / recent-ancestor run rather than a cold rebuild. | |
| - name: Restore Turborepo cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: .turbo | |
| key: turbo-${{ runner.os }}-${{ github.sha }} | |
| restore-keys: | | |
| turbo-${{ runner.os }}- | |
| # `pnpm coverage` is a repo-ROOT vitest run (not a turbo task), so it does NOT get the `test` | |
| # task's `^build` — yet the `@relavium/*` package `exports` resolve only to `dist`. Build first so a | |
| # fresh checkout can resolve the cross-package entries (a per-package test gets dist via turbo `^build`; | |
| # each package still covers its OWN src via relative imports, so the floor stays src-accurate). | |
| - name: Build workspaces (so coverage resolves the @relavium/* package entries) | |
| run: pnpm turbo run build | |
| # ENFORCED subset only: `llm` and `mcp` fail the build below 90% line+branch. `core` is measured and | |
| # printed by the same run but does NOT fail it — its branch margin is +0.83 today and Phase 2.5.5 | |
| # Waves 1-3 edit it heavily, so blocking on a sub-1-point margin would red-CI real work for no | |
| # defect (maintainer ruling 2026-07-29). A bare `pnpm coverage` still enforces all three locally. | |
| # Promote `core` here once Wave 3's test-coverage items land. | |
| - name: Engine coverage floor (>=90% line+branch — llm, mcp enforced; core measured) | |
| run: pnpm coverage:enforced | |
| # Cross-OS concurrency + headless gate (2.5.I S6). The DB write-path hardening (BEGIN IMMEDIATE + the | |
| # SQLITE_BUSY retry's Atomics.wait sleep + WAL locking) and the two-process concurrency e2e (a child spawn + | |
| # a file:// import of the built @relavium/db) are the parts most likely to behave differently on Windows — | |
| # ci.yml ran ubuntu-only (release.yml already smokes the built binary cross-OS, but never the test suite). | |
| # Runs the @relavium/db concurrency suite (the real Windows native-addon + WAL exercise) + the CLI | |
| # concurrency/perf harness there, plus a headless no-TTY smoke. A SEPARATE, advisory job (the required check | |
| # stays the ubuntu `ci` job) — promote to required in branch protection once confirmed stable. POSIX 0600/0700 | |
| # perm assertions are NOT exercised here (a documented Windows no-op — ADR-0050); nor is ink's raw-mode code, | |
| # which is TTY-gated (the driver-selection gate picks the plain driver without a TTY — see the smoke below). | |
| windows-concurrency: | |
| name: windows · db concurrency + headless smoke (advisory) | |
| runs-on: windows-latest | |
| timeout-minutes: 25 | |
| steps: | |
| - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| with: | |
| persist-credentials: false | |
| - uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v4.4.0 | |
| - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: pnpm | |
| - name: Install (frozen lockfile) | |
| run: pnpm install --frozen-lockfile | |
| - name: Restore Turborepo cache | |
| uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 | |
| with: | |
| path: .turbo | |
| key: turbo-${{ runner.os }}-${{ github.sha }} | |
| restore-keys: | | |
| turbo-${{ runner.os }}- | |
| # The two-process concurrency e2e spawns children that import the BUILT @relavium/db (by file:// URL), | |
| # so the dist must exist before the tests run. | |
| - name: Build | |
| run: pnpm turbo run build | |
| - name: DB concurrency suite (BEGIN IMMEDIATE + SQLITE_BUSY retry + WAL behave identically on Windows) | |
| run: pnpm --filter @relavium/db test | |
| - name: CLI concurrency + query-shape harness (cross-process spawn + EXPLAIN plans on Windows) | |
| run: pnpm --filter relavium exec vitest run concurrency perf-budget session-chain | |
| - name: Headless no-TTY smoke (the bundle loads; a piped chat picks the plain driver and exits cleanly) | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| node apps/cli/dist/index.js --help >/dev/null # the tsup bundle loads + --help renders | |
| # `chat` is TTY-gated on STDOUT (selectChatDriver / io.stdoutIsTty): with stdout captured (non-TTY) | |
| # here, the driver-selection gate picks the PLAIN driver (ink/setRawMode is never entered), and the | |
| # plain driver must exit cleanly on EOF — never crash. A regression routing to ink without a TTY would | |
| # throw "Raw mode is not supported…", which this catches. | |
| out="$(printf '' | node apps/cli/dist/index.js chat 2>&1 || true)" | |
| if printf '%s' "$out" | grep -qiE 'setRawMode|raw mode'; then | |
| echo "::error::chat entered raw mode without a TTY (the driver-selection gate regressed)"; exit 1 | |
| fi | |
| echo "✓ windows headless no-TTY smoke passed" |