From 710f062586ab12b7c077d4d0be7df2abb2f646e9 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Tue, 21 Jul 2026 23:56:53 +0800 Subject: [PATCH] ci: add snapshot lane headroom --- .../2026-07-06-parallel-github-ci-gates.md | 4 +-- .github/workflows/ci.yml | 29 +++++++++++++++---- scripts/snapshot-shards.spec.ts | 13 +++++++-- scripts/snapshot-shards.ts | 4 +-- 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md b/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md index 0e2fbf76ac..35de6cca40 100644 --- a/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md +++ b/.agents/notes/implemented/process/2026-07-06-parallel-github-ci-gates.md @@ -14,9 +14,9 @@ The artifact boundary remains load-bearing. `publint`, `verify-node-next-types`, [CI](../../../../.github/workflows/ci.yml) treats one minute for non-Windows jobs and three minutes for Windows jobs as observed performance targets, not cancellation deadlines. Hosted-runner variance should leave complete timing evidence and useful failure logs instead of cancelling an otherwise-correct gate. The [serial cross-platform CI reference](2026-07-21-serial-cross-platform-ci-reference.md) independently runs the complete unsharded primary Node aggregate on Linux, macOS, and Windows so the optimized lane inventory is not its own completeness oracle. -[scripts/run-gates.ts](../../../../scripts/run-gates.ts) remains the common bounded scheduler, but GitHub supplies explicit shard names for the expensive gate families. [scripts/static-shards.ts](../../../../scripts/static-shards.ts) partitions static gates into foundation, documentation-type, API-contract, catalog, prose, documentation-projection, and documentation-build lanes and rejects a missing or duplicate gate assignment. Lint uses disjoint package-source, package-test, and repository-complement lanes; the complement still starts from `.` so a new top-level lint target cannot disappear between shards, and it owns the single cross-file duplication run. [scripts/coverage-shards.ts](../../../../scripts/coverage-shards.ts) assigns every workspace package to exactly one source-coverage lane; its test expands the live package tree, so a new package makes CI red until it has an owner. Each coverage lane includes only its owned source files, repeats the exhaustive companion topology test, and runs without a preceding build because the complete coverage suite passes from a tree with every generated `lib/` removed. +[scripts/run-gates.ts](../../../../scripts/run-gates.ts) remains the common bounded scheduler, but GitHub supplies explicit shard names for the expensive gate families. [scripts/static-shards.ts](../../../../scripts/static-shards.ts) partitions static gates into foundation, documentation-type, API-contract, catalog, prose, documentation-projection, and documentation-build lanes and rejects a missing or duplicate gate assignment. Lint uses disjoint package-source, package-test, and repository-complement lanes; the complement still starts from `.` so a new top-level lint target cannot disappear between shards, and it owns the single cross-file duplication run. [scripts/coverage-shards.ts](../../../../scripts/coverage-shards.ts) assigns every workspace package to exactly one source-coverage lane; its test expands the live package tree, so a new package makes CI red until it has an owner. Directory filters retain a trailing separator because Vitest positional filters match substrings and would otherwise admit prefix-named sibling packages. Each coverage lane includes only its owned source files, repeats the exhaustive companion topology test, and runs without a preceding build because the complete coverage suite passes from a tree with every generated `lib/` removed. -Snapshot replay uses two explicit multi-file lanes and six scenario partitions of the large ACP file. [scripts/snapshot-shards.ts](../../../../scripts/snapshot-shards.ts) owns that inventory, and its test discovers every file admitted by the snapshot config so a new file cannot land outside CI. Each snapshot job builds the shipped runtime while its Linux runner installs bubblewrap from the hosted image's existing package index, then runs only its assigned replay surface. CI explicitly retains the suite's bounded concurrency of five subprocesses instead of clamping it to the runner's two logical CPUs, because replay spends most of its time waiting on child protocol I/O. Fixture guards still inspect the complete ACP scenario table in every partition. Static, lint, coverage, and snapshot sharding changes only GitHub scheduling: the ordinary local package scripts still run their complete suites. +Snapshot replay uses two explicit multi-file lanes and eight scenario partitions of the large ACP file. [scripts/snapshot-shards.ts](../../../../scripts/snapshot-shards.ts) owns that inventory, and its test discovers every file admitted by the snapshot config so a new file cannot land outside CI. Each snapshot job installs dependencies while its Linux runner installs bubblewrap, then builds the shipped runtime and runs only its assigned replay surface. CI explicitly retains the suite's bounded concurrency of five subprocesses instead of clamping it to the runner's two logical CPUs, because replay spends most of its time waiting on child protocol I/O. Fixture guards still inspect the complete ACP scenario table in every partition. Static, lint, coverage, and snapshot sharding changes only GitHub scheduling: the ordinary local package scripts still run their complete suites. Cold standalone documentation typechecking rebuilds the complete project-reference graph, so a dedicated documentation-type lane builds once and checks Markdown blocks against those declarations. The Linux documentation lane uses VitePress's MPA build to retain page rendering and dead-link validation within the observed non-Windows target; separate blocking Windows build and production-site lanes preserve the emitted-package and shipped-site checks without putting both critical paths in one job. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5b9d6e76f..eb946fe117 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -190,6 +190,18 @@ jobs: snapshot_lane: acp-6 snapshot_max_concurrency: '5' snapshot_prebuilt: '1' + - lane: snapshot-acp-7 + command: pnpm run check:ci:snapshot + gate_concurrency: '1' + snapshot_lane: acp-7 + snapshot_max_concurrency: '5' + snapshot_prebuilt: '1' + - lane: snapshot-acp-8 + command: pnpm run check:ci:snapshot + gate_concurrency: '1' + snapshot_lane: acp-8 + snapshot_max_concurrency: '5' + snapshot_prebuilt: '1' - lane: artifacts-metadata command: pnpm run check:ci:artifacts gate_concurrency: '3' @@ -220,6 +232,7 @@ jobs: ${{ runner.os }}-node-${{ env.PRIMARY_NODE_VERSION }}-pnpm- - name: Install (immutable) + if: ${{ ! startsWith(matrix.lane, 'snapshot-') }} run: pnpm install --frozen-lockfile # The snapshot lanes REPLAY the sandbox example's recorded scenarios, @@ -227,22 +240,28 @@ jobs: # no bubblewrap preinstalled and no built Landlock launcher, so without # this the confined executions fail closed (SANDBOX_UNAVAILABLE). Same # install as sandbox.yml's bwrap leg (incl. the Ubuntu 24.04 AppArmor - # userns knob). Building does not depend on bubblewrap, so overlap them. - - name: Prepare built snapshot runtime and bubblewrap + # userns knob). Bubblewrap preparation is independent of dependency + # installation and the build, so it runs beside both. + - name: Install and prepare built snapshot runtime and bubblewrap if: startsWith(matrix.lane, 'snapshot-') run: | - pnpm run build & - build_pid=$! + pnpm install --frozen-lockfile & + install_pid=$! ( sudo apt-get install -yq --no-install-recommends bubblewrap sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 \ || echo "apparmor userns knob absent — the functional probe decides" ) & sandbox_pid=$! + install_status=0 + wait "$install_pid" || install_status=$? build_status=0 + if (( install_status == 0 )); then + pnpm run build || build_status=$? + fi sandbox_status=0 - wait "$build_pid" || build_status=$? wait "$sandbox_pid" || sandbox_status=$? + if (( install_status != 0 )); then exit "$install_status"; fi if (( build_status != 0 )); then exit "$build_status"; fi exit "$sandbox_status" diff --git a/scripts/snapshot-shards.spec.ts b/scripts/snapshot-shards.spec.ts index 7cc37fb7bd..1b8db7dc39 100644 --- a/scripts/snapshot-shards.spec.ts +++ b/scripts/snapshot-shards.spec.ts @@ -26,10 +26,19 @@ describe('snapshot lanes', () => { expect(new Set(ordinary).size).toBe(ordinary.length) expect(acp.map(lane => lane.files)).toEqual(Array.from( - { length: 6 }, + { length: 8 }, () => ['examples/acp-agent/tests/acp.snapshot.ts'], )) - expect(acp.map(lane => lane.scenarioShard)).toEqual(['1/6', '2/6', '3/6', '4/6', '5/6', '6/6']) + expect(acp.map(lane => lane.scenarioShard)).toEqual([ + '1/8', + '2/8', + '3/8', + '4/8', + '5/8', + '6/8', + '7/8', + '8/8', + ]) expect([...ordinary, 'examples/acp-agent/tests/acp.snapshot.ts'].sort()).toEqual(discovered) }) diff --git a/scripts/snapshot-shards.ts b/scripts/snapshot-shards.ts index fae8010261..5439be94fb 100644 --- a/scripts/snapshot-shards.ts +++ b/scripts/snapshot-shards.ts @@ -28,10 +28,10 @@ export const snapshotLanes: readonly SnapshotLane[] = [ 'examples/headless-agent/tests/headless.snapshot.ts', ], }, - ...Array.from({ length: 6 }, (_, offset) => ({ + ...Array.from({ length: 8 }, (_, offset) => ({ name: `acp-${offset + 1}`, files: ['examples/acp-agent/tests/acp.snapshot.ts'], - scenarioShard: `${offset + 1}/6`, + scenarioShard: `${offset + 1}/8`, })), ]