From 109224af5d66ec3c35b6cdb4c22c6f98a48b0168 Mon Sep 17 00:00:00 2001 From: Tianyi Cui <53024+tianyicui@users.noreply.github.com> Date: Wed, 22 Jul 2026 00:31:25 +0800 Subject: [PATCH] ci: quarter cold-cache lint lanes --- .../2026-07-06-parallel-github-ci-gates.md | 2 +- .github/workflows/ci.yml | 72 ++++++++++++++----- scripts/lint-shards.spec.ts | 42 +++++++---- scripts/lint-shards.ts | 24 ++++--- 4 files changed, 102 insertions(+), 38 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 184f400a83..aeab7cbfbb 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,7 +14,7 @@ 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 A-M and N-Z package-source and package-test lanes plus a repository complement; 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. +[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 A-C, D-M, N-S, and T-Z package-source and package-test lanes plus a repository complement; 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 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, refreshing APT indexes and retrying if the hosted image's initial package install fails, 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. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a280b7316a..79dcfcffcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,26 +67,46 @@ jobs: static_shard: site-build - lane: typecheck command: pnpm run typecheck - - lane: lint-package-sources-a-m + - lane: lint-package-sources-a-c command: pnpm run check:ci:lint gate_concurrency: '1' eslint_cache: '1' - lint_shard: package-sources-a-m - - lane: lint-package-sources-n-z + lint_shard: package-sources-a-c + - lane: lint-package-sources-d-m command: pnpm run check:ci:lint gate_concurrency: '1' eslint_cache: '1' - lint_shard: package-sources-n-z - - lane: lint-package-tests-a-m + lint_shard: package-sources-d-m + - lane: lint-package-sources-n-s command: pnpm run check:ci:lint gate_concurrency: '1' eslint_cache: '1' - lint_shard: package-tests-a-m - - lane: lint-package-tests-n-z + lint_shard: package-sources-n-s + - lane: lint-package-sources-t-z command: pnpm run check:ci:lint gate_concurrency: '1' eslint_cache: '1' - lint_shard: package-tests-n-z + lint_shard: package-sources-t-z + - lane: lint-package-tests-a-c + command: pnpm run check:ci:lint + gate_concurrency: '1' + eslint_cache: '1' + lint_shard: package-tests-a-c + - lane: lint-package-tests-d-m + command: pnpm run check:ci:lint + gate_concurrency: '1' + eslint_cache: '1' + lint_shard: package-tests-d-m + - lane: lint-package-tests-n-s + command: pnpm run check:ci:lint + gate_concurrency: '1' + eslint_cache: '1' + lint_shard: package-tests-n-s + - lane: lint-package-tests-t-z + command: pnpm run check:ci:lint + gate_concurrency: '1' + eslint_cache: '1' + lint_shard: package-tests-t-z - lane: lint-repository command: pnpm run check:ci:lint gate_concurrency: '1' @@ -422,26 +442,46 @@ jobs: command: pnpm run check:ci:static gate_concurrency: '4' static_shard: 'doc-types,api-contracts' - - lane: lint-package-sources-a-m + - lane: lint-package-sources-a-c command: pnpm run check:ci:lint gate_concurrency: '1' eslint_cache: '1' - lint_shard: package-sources-a-m - - lane: lint-package-sources-n-z + lint_shard: package-sources-a-c + - lane: lint-package-sources-d-m command: pnpm run check:ci:lint gate_concurrency: '1' eslint_cache: '1' - lint_shard: package-sources-n-z - - lane: lint-package-tests-a-m + lint_shard: package-sources-d-m + - lane: lint-package-sources-n-s command: pnpm run check:ci:lint gate_concurrency: '1' eslint_cache: '1' - lint_shard: package-tests-a-m - - lane: lint-package-tests-n-z + lint_shard: package-sources-n-s + - lane: lint-package-sources-t-z command: pnpm run check:ci:lint gate_concurrency: '1' eslint_cache: '1' - lint_shard: package-tests-n-z + lint_shard: package-sources-t-z + - lane: lint-package-tests-a-c + command: pnpm run check:ci:lint + gate_concurrency: '1' + eslint_cache: '1' + lint_shard: package-tests-a-c + - lane: lint-package-tests-d-m + command: pnpm run check:ci:lint + gate_concurrency: '1' + eslint_cache: '1' + lint_shard: package-tests-d-m + - lane: lint-package-tests-n-s + command: pnpm run check:ci:lint + gate_concurrency: '1' + eslint_cache: '1' + lint_shard: package-tests-n-s + - lane: lint-package-tests-t-z + command: pnpm run check:ci:lint + gate_concurrency: '1' + eslint_cache: '1' + lint_shard: package-tests-t-z - lane: lint-repository command: pnpm run check:ci:lint gate_concurrency: '1' diff --git a/scripts/lint-shards.spec.ts b/scripts/lint-shards.spec.ts index 51ac21e0bd..540c4b88c4 100644 --- a/scripts/lint-shards.spec.ts +++ b/scripts/lint-shards.spec.ts @@ -11,21 +11,37 @@ describe('lint gate shards', () => { expect(selectLintShard('')).toEqual({ eslintTargets: ['.'], includeDuplication: true }) }) - it('partitions package sources and tests into alphabetic halves plus their repository complement', () => { - expect(selectLintShard('package-sources-a-m')).toEqual({ - eslintTargets: ['packages/[a-m]*/*/src/**/*.ts'], + it('partitions package sources and tests into alphabetic ranges plus their repository complement', () => { + expect(selectLintShard('package-sources-a-c')).toEqual({ + eslintTargets: ['packages/[a-c]*/*/src/**/*.ts'], includeDuplication: false, }) - expect(selectLintShard('package-sources-n-z')).toEqual({ - eslintTargets: ['packages/[n-z]*/*/src/**/*.ts'], + expect(selectLintShard('package-sources-d-m')).toEqual({ + eslintTargets: ['packages/[d-m]*/*/src/**/*.ts'], includeDuplication: false, }) - expect(selectLintShard('package-tests-a-m')).toEqual({ - eslintTargets: ['packages/[a-m]*/*/tests/**/*.ts'], + expect(selectLintShard('package-sources-n-s')).toEqual({ + eslintTargets: ['packages/[n-s]*/*/src/**/*.ts'], includeDuplication: false, }) - expect(selectLintShard('package-tests-n-z')).toEqual({ - eslintTargets: ['packages/[n-z]*/*/tests/**/*.ts'], + expect(selectLintShard('package-sources-t-z')).toEqual({ + eslintTargets: ['packages/[t-z]*/*/src/**/*.ts'], + includeDuplication: false, + }) + expect(selectLintShard('package-tests-a-c')).toEqual({ + eslintTargets: ['packages/[a-c]*/*/tests/**/*.ts'], + includeDuplication: false, + }) + expect(selectLintShard('package-tests-d-m')).toEqual({ + eslintTargets: ['packages/[d-m]*/*/tests/**/*.ts'], + includeDuplication: false, + }) + expect(selectLintShard('package-tests-n-s')).toEqual({ + eslintTargets: ['packages/[n-s]*/*/tests/**/*.ts'], + includeDuplication: false, + }) + expect(selectLintShard('package-tests-t-z')).toEqual({ + eslintTargets: ['packages/[t-z]*/*/tests/**/*.ts'], includeDuplication: false, }) expect(selectLintShard('repository')).toEqual({ @@ -40,15 +56,15 @@ describe('lint gate shards', () => { }) }) - it('assigns every package group to one alphabetic half', () => { + it('assigns every package group to one alphabetic range', () => { const groups = readdirSync(packagesRoot, { withFileTypes: true }) .filter(entry => entry.isDirectory()) .map(entry => entry.name) .sort() - const firstHalf = groups.filter(group => /^[a-m]/u.test(group)) - const secondHalf = groups.filter(group => /^[n-z]/u.test(group)) + const ranges = [/^[a-c]/u, /^[d-m]/u, /^[n-s]/u, /^[t-z]/u] + const assignments = ranges.flatMap(range => groups.filter(group => range.test(group))).sort() - expect([...firstHalf, ...secondHalf].sort()).toEqual(groups) + expect(assignments).toEqual(groups) }) it('rejects an unknown lane', () => { diff --git a/scripts/lint-shards.ts b/scripts/lint-shards.ts index a48e9ce257..e9a12c3e80 100644 --- a/scripts/lint-shards.ts +++ b/scripts/lint-shards.ts @@ -19,14 +19,22 @@ export function selectLintShard(name?: string): LintSelection { case undefined: case '': return { eslintTargets: ['.'], includeDuplication: true } - case 'package-sources-a-m': - return { eslintTargets: ['packages/[a-m]*/*/src/**/*.ts'], includeDuplication: false } - case 'package-sources-n-z': - return { eslintTargets: ['packages/[n-z]*/*/src/**/*.ts'], includeDuplication: false } - case 'package-tests-a-m': - return { eslintTargets: ['packages/[a-m]*/*/tests/**/*.ts'], includeDuplication: false } - case 'package-tests-n-z': - return { eslintTargets: ['packages/[n-z]*/*/tests/**/*.ts'], includeDuplication: false } + case 'package-sources-a-c': + return { eslintTargets: ['packages/[a-c]*/*/src/**/*.ts'], includeDuplication: false } + case 'package-sources-d-m': + return { eslintTargets: ['packages/[d-m]*/*/src/**/*.ts'], includeDuplication: false } + case 'package-sources-n-s': + return { eslintTargets: ['packages/[n-s]*/*/src/**/*.ts'], includeDuplication: false } + case 'package-sources-t-z': + return { eslintTargets: ['packages/[t-z]*/*/src/**/*.ts'], includeDuplication: false } + case 'package-tests-a-c': + return { eslintTargets: ['packages/[a-c]*/*/tests/**/*.ts'], includeDuplication: false } + case 'package-tests-d-m': + return { eslintTargets: ['packages/[d-m]*/*/tests/**/*.ts'], includeDuplication: false } + case 'package-tests-n-s': + return { eslintTargets: ['packages/[n-s]*/*/tests/**/*.ts'], includeDuplication: false } + case 'package-tests-t-z': + return { eslintTargets: ['packages/[t-z]*/*/tests/**/*.ts'], includeDuplication: false } case 'repository': return { eslintTargets: [