Skip to content

Commit b509ed3

Browse files
Merge pull request #1028 from corbitsdev/cl-7912-shard-the-ci-test-matrix-toward-30-90s-wall-clock
Shard the CI test matrix into four time-balanced slices
2 parents f4256a4 + 90453f6 commit b509ed3

4 files changed

Lines changed: 622 additions & 39 deletions

File tree

.github/workflows/ci.yml

Lines changed: 78 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ on:
55
branches: [main]
66
pull_request:
77
workflow_dispatch:
8+
# Nightly pollution-detector run (see test-one-process below).
9+
schedule:
10+
- cron: "17 7 * * *"
811

912
concurrency:
1013
group: ci-${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }}
@@ -78,31 +81,29 @@ jobs:
7881
run: bun run build
7982

8083
# The suite is sharded so the slowest slice, not the whole suite, sets the
81-
# wall clock. The old ./src leg (387 files, ~88s local) is split into three
82-
# path-disjoint shards measured at ~30s (src-a: tui, 146 files), ~46s
83-
# (src-b: agent + subagent, 90 files), and ~45s (src-c: everything else in
84-
# src, 153 files); the ./tests leg stays whole because ./evals ./scripts
85-
# finish in ~1s and a leg of their own would be all setup overhead.
86-
# Every shard still goes through check:projects-dir-guard: the
87-
# guard forwards these path filters to the suite it wraps, and the union of
88-
# the shards' filters is exactly ./src ./tests ./evals ./scripts, so the gate covers
89-
# the same tests as before, all of them sandboxed.
84+
# wall clock. Four time-balanced shards split the same
85+
# ./src ./tests ./evals ./scripts union via bun's --shard=k/4, balanced by
86+
# the checked-in per-file durations in scripts/ci-timings.json (--timings).
87+
# Every shard still goes through check:projects-dir-guard: the guard
88+
# forwards the path union plus the shard flags to the suite it wraps, so the
89+
# gate covers the same tests as before, all of them sandboxed.
90+
#
91+
# Timings refresh policy: regenerate scripts/ci-timings.json by running the
92+
# full union locally with --update-timings (same seeded flags as `test`):
93+
# bun run check:projects-dir-guard ./src ./tests ./evals ./scripts \
94+
# --timings=./scripts/ci-timings.json --update-timings
95+
# Regen when the slowest shard's Test step skews more than ~20% above a
96+
# quarter of the one-process suite time (shards drifting apart means the
97+
# timings no longer describe the suite), or proactively whenever slow files
98+
# land. A scheduled refresh artifact is a future option, not current setup.
9099
test:
91100
runs-on: ubuntu-latest
92101
strategy:
93-
# A red shard must not cancel the others; every result is the signal.
102+
# A red shard must not cancel the others; all results are the signal.
94103
fail-fast: false
95104
matrix:
96-
shard:
97-
- name: src-a
98-
paths: ./src/tui
99-
- name: src-b
100-
paths: ./src/agent ./src/subagent
101-
- name: src-c
102-
paths: ./src/auth ./src/changelog ./src/config ./src/cost ./src/crash ./src/exec ./src/logging ./src/mcp ./src/perf ./src/permission ./src/plugins ./src/provider ./src/session ./src/shell ./src/telemetry ./src/tools ./src/trust ./src/upgrade ./src/util ./src/web ./src/config.test.ts ./src/context-compactor.test.ts ./src/director.test.ts ./src/inference-abort.test.ts ./src/inference-error-message.test.ts ./src/inference-gateway-error.test.ts ./src/list-dir.test.ts ./src/pricing-fetcher.test.ts ./src/pricing-metadata.test.ts ./src/profiles.test.ts ./src/prompts.test.ts ./src/renderer.test.ts ./src/settings.test.ts ./src/state.test.ts
103-
- name: tests-evals-and-scripts
104-
paths: ./tests ./evals ./scripts
105-
name: test (${{ matrix.shard.name }})
105+
shard: ["1/4", "2/4", "3/4", "4/4"]
106+
name: test (${{ matrix.shard }})
106107
steps:
107108
- name: Checkout
108109
uses: actions/checkout@v4
@@ -131,19 +132,67 @@ jobs:
131132
- name: Install dependencies
132133
run: bun install --frozen-lockfile
133134

134-
# The same script the local `bun run check` gate runs, with the shard's
135-
# path filters forwarded through the guard to the suite. The guard
136-
# routes a filtered run through test:paths, which carries the same
137-
# seeded flags as the `test` script; bun test filters are additive, so
138-
# appending filters to `bun run test` could not narrow it. Randomized
139-
# order catches tests that only pass in the default file order (shared
140-
# module-level state, an unrestored global mock, a leaked env var).
135+
# The same script the local `bun run check` gate runs, with the full
136+
# path union plus the shard's --shard/--timings flags forwarded through
137+
# the guard to the suite. The guard routes a filtered run through
138+
# test:paths, which carries the same seeded flags as the `test` script;
139+
# bun test filters are additive, so appending filters to `bun run test`
140+
# could not narrow it. --shard splits by file (balanced by --timings),
141+
# so every shard covers the same union and the four shards together
142+
# cover the whole suite. Randomized order catches tests that only pass
143+
# in the default file order (shared module-level state, an unrestored
144+
# global mock, a leaked env var).
141145
# The seed stays 424242 in every shard rather than varying per shard:
142146
# the shards already run disjoint file sets, and a fixed seed keeps
143147
# any failure reproducible locally with the same
144-
# `bun run test:paths <paths>`.
148+
# `bun run test:paths <paths> --shard=k/4
149+
# --timings=./scripts/ci-timings.json`.
145150
- name: Test
146-
run: bun run check:projects-dir-guard ${{ matrix.shard.paths }}
151+
run: bun run check:projects-dir-guard ./src ./tests ./evals ./scripts --shard=${{ matrix.shard }} --timings=./scripts/ci-timings.json
152+
153+
# Cross-shard pollution detector: the shards above split the path union,
154+
# but the union is not the isolation domain — a mock.module leak across
155+
# files fails in the one-process suite yet passes when the files land in
156+
# different shards (CL-6967). This job reruns the whole union in one
157+
# process with no filters, exactly like the local `bun run check` gate.
158+
# Nightly (plus manual workflow_dispatch), not per-PR: the one-process
159+
# suite takes ~2 minutes on CI and would put that back on the PR wall
160+
# clock this sharding removes. Non-blocking (continue-on-error) so a slow
161+
# or flaky full run cannot hold the gate; a real pollution failure still
162+
# shows up red for triage.
163+
test-one-process:
164+
name: test (one-process pollution detector)
165+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
166+
continue-on-error: true
167+
runs-on: ubuntu-latest
168+
steps:
169+
- name: Checkout
170+
uses: actions/checkout@v4
171+
172+
- name: Setup Node
173+
uses: actions/setup-node@v4
174+
with:
175+
node-version: "24"
176+
177+
- name: Setup Bun
178+
uses: oven-sh/setup-bun@v2
179+
with:
180+
bun-version: "1.3.14"
181+
182+
- name: Install ripgrep
183+
run: sudo apt-get install -y ripgrep
184+
185+
- name: Cache dependencies
186+
uses: actions/cache@v4
187+
with:
188+
path: node_modules
189+
key: bun-${{ hashFiles('bun.lock') }}
190+
191+
- name: Install dependencies
192+
run: bun install --frozen-lockfile
193+
194+
- name: Test
195+
run: bun run check:projects-dir-guard
147196

148197
# protect-main still requires the pre-restructure check names. These jobs
149198
# exist only to publish those contexts after the real work succeeds.

CONTRIBUTING.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,15 @@ bun run build
3030
bun run test
3131
```
3232

33-
These match the local development loop. CI shards the same path union via
34-
`test:paths` rather than running the one-process `bun run test` suite.
33+
These match the local development loop. CI splits the same path union into
34+
four time-balanced `--shard=k/4` slices via `test:paths` (balanced by the
35+
checked-in per-file durations in `scripts/ci-timings.json`) rather than
36+
running the one-process `bun run test` suite. Regenerate that file with
37+
`bun run check:projects-dir-guard ./src ./tests ./evals ./scripts
38+
--timings=./scripts/ci-timings.json --update-timings` when the slowest
39+
shard skews more than ~20% above a quarter of the one-process suite time,
40+
or proactively whenever slow files land — see `.github/workflows/ci.yml`
41+
for the full policy.
3542
Run `bun run check`
3643
(lint, typecheck, build, and the guarded test suite) before opening a PR —
3744
`bun run test` alone skips the projects-dir sandbox guard, which only runs

0 commit comments

Comments
 (0)