ci: consolidate primary checks on one larger runner

This commit is contained in:
Tianyi Cui
2026-07-22 17:53:01 +08:00
parent 319005d56b
commit 41609c4ce4
24 changed files with 102 additions and 1330 deletions

View File

@@ -1,46 +0,0 @@
import { readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { coverageArgs, coverageShards } from './coverage-shards.ts'
const repositoryRoot = resolve(import.meta.dirname, '..')
describe('coverage shards', () => {
it('assigns every workspace package to exactly one lane', () => {
const packagesRoot = resolve(repositoryRoot, 'packages')
const workspacePackages = readdirSync(packagesRoot, { withFileTypes: true })
.filter(group => group.isDirectory())
.flatMap(group => readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => `${group.name}/${entry.name}`))
.sort()
const assignedPackages = coverageShards.flatMap(shard => shard.packageRoots.flatMap((packageRoot) => {
if (packageRoot.includes('/')) return [packageRoot]
return readdirSync(resolve(packagesRoot, packageRoot), { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => `${packageRoot}/${entry.name}`)
}))
expect([...assignedPackages].sort()).toEqual(workspacePackages)
expect(new Set(assignedPackages).size).toBe(assignedPackages.length)
})
it.each(coverageShards)('selects tests and source includes for $name', (shard) => {
const args = coverageArgs(shard.name)
for (const packageRoot of shard.packageRoots) {
expect(args).toContain(`packages/${packageRoot}/`)
expect(args).toContain(packageRoot.includes('/')
? `--coverage.include=packages/${packageRoot}/src/**/*.ts`
: `--coverage.include=packages/${packageRoot}/*/src/**/*.ts`)
}
if ('extraTestRoots' in shard) {
for (const testRoot of shard.extraTestRoots) expect(args).toContain(`${testRoot}/`)
}
expect(args).toContain('scripts/test-invariants.spec.ts')
expect(new Set(args).size).toBe(args.length)
})
it('rejects an unknown lane', () => {
expect(() => coverageArgs('missing')).toThrow('unknown DSH_COVERAGE_SHARD')
})
})

View File

@@ -1,114 +0,0 @@
/** Coverage shard definitions for the GitHub Actions source-test lanes. */
/** A coverage lane that owns complete package roots and optional cross-package tests. */
export interface CoverageShard {
/** Stable lane identifier passed through `DSH_COVERAGE_SHARD`. */
name: string
/** Group or package paths below `packages/` whose tests and source coverage belong to the lane. */
packageRoots: readonly string[]
/** Additional test roots needed for cross-package behavior or repository scripts. */
extraTestRoots?: readonly string[]
}
/** Exhaustive, non-overlapping ownership of workspace packages in coverage CI. */
export const coverageShards = [
{
name: 'core-loop',
packageRoots: ['core/agent', 'core/agent-loop', 'core/tools'],
},
{
name: 'state-session',
packageRoots: [
'core/session',
'core/scope',
'core/system-prompt',
'context',
'session-persistence',
'session-query',
'support/invariants',
],
extraTestRoots: [
'packages/examples/cli-demo/tests',
'packages/llm/token-meter/tests',
'scripts',
],
},
{
name: 'models',
packageRoots: ['llm', 'compact'],
},
{
name: 'session-title',
packageRoots: ['session-title'],
},
{
name: 'integrations',
packageRoots: ['hooks/hook-protocol', 'lsp', 'mcp', 'hooks/hooks-claude'],
},
{
name: 'sdk-capabilities',
packageRoots: [
'sdk',
'hooks/hooks-codex',
'web',
'skill',
'spill',
'util',
'guard',
'todo',
'timeout',
],
},
{
name: 'interfaces',
packageRoots: ['ui', 'examples', 'goal'],
extraTestRoots: ['examples'],
},
{ name: 'execution', packageRoots: ['fs', 'bash', 'sandbox', 'code-runtime'] },
{
name: 'workflow',
packageRoots: ['workflow/workflow', 'workflow/tool-workflow', 'workflow/tool-ralph'],
},
{
name: 'workflow-worker',
packageRoots: ['workflow/workflow-workerthread'],
},
{ name: 'delegation', packageRoots: ['subagent', 'tasks'] },
{
name: 'repository',
packageRoots: [
'cordis',
'support/acp-snapshot',
'support/agent-loop-testkit',
'support/llm-replay',
'support/loader-smoke',
],
},
] as const satisfies readonly CoverageShard[]
/**
* Build Vitest filters and coverage include globs for one source-test lane.
*
* @param name Stable shard name from {@link coverageShards}.
* @returns Positional test roots followed by per-group coverage include flags.
*/
export function coverageArgs(name: string): string[] {
const shard = coverageShards.find(candidate => candidate.name === name)
if (shard === undefined) {
throw new Error(`run-gates: unknown DSH_COVERAGE_SHARD ${JSON.stringify(name)}.`)
}
// Vitest positional filters are substrings; the trailing separator keeps
// prefix-named sibling packages out of each lane.
const testRoots = new Set([
...shard.packageRoots.map(packageRoot => `packages/${packageRoot}/`),
...('extraTestRoots' in shard ? shard.extraTestRoots.map(testRoot => `${testRoot}/`) : []),
'scripts/test-invariants.spec.ts',
])
return [
...testRoots,
...shard.packageRoots.map(packageRoot => packageRoot.includes('/')
? `--coverage.include=packages/${packageRoot}/src/**/*.ts`
: `--coverage.include=packages/${packageRoot}/*/src/**/*.ts`),
]
}

View File

@@ -1,81 +0,0 @@
import { readdirSync } from 'node:fs'
import { resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import { selectLintShard } from './lint-shards.ts'
const packagesRoot = resolve(import.meta.dirname, '..', 'packages')
describe('lint gate shards', () => {
it('keeps the unsharded local command complete', () => {
expect(selectLintShard()).toEqual({ eslintTargets: ['.'], includeDuplication: true })
expect(selectLintShard('')).toEqual({ eslintTargets: ['.'], includeDuplication: true })
})
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-d-m')).toEqual({
eslintTargets: ['packages/[d-m]*/*/src/**/*.ts'],
includeDuplication: false,
})
expect(selectLintShard('package-sources-n-s')).toEqual({
eslintTargets: ['packages/[n-s]*/*/src/**/*.ts'],
includeDuplication: false,
})
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('package-sources')).toEqual({
eslintTargets: ['packages/*/*/src/**/*.ts'],
includeDuplication: false,
})
expect(selectLintShard('package-tests')).toEqual({
eslintTargets: ['packages/*/*/tests/**/*.ts'],
includeDuplication: false,
})
expect(selectLintShard('repository')).toEqual({
eslintTargets: [
'.',
'--ignore-pattern',
'packages/*/*/src/**',
'--ignore-pattern',
'packages/*/*/tests/**',
],
includeDuplication: true,
})
})
it('assigns every package group once in the Linux topology', () => {
const groups = readdirSync(packagesRoot, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name)
.sort()
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(assignments).toEqual(groups)
})
it('rejects an unknown lane', () => {
expect(() => selectLintShard('missing')).toThrow('unknown DSH_LINT_SHARD')
})
})

View File

@@ -1,56 +0,0 @@
/** Lint-lane selection for GitHub Actions. */
/** One ESLint target set and whether it owns the cross-file duplication gate. */
export interface LintSelection {
/** Shell-free arguments passed to ESLint before its cache options. */
eslintTargets: readonly string[]
/** Whether this lane also runs the repository-wide duplication check. */
includeDuplication: boolean
}
/**
* Select an exhaustive lint partition without changing the ordinary local lint command.
*
* @param name Optional stable shard name from `DSH_LINT_SHARD`.
* @returns ESLint targets and ownership of the duplication gate.
*/
export function selectLintShard(name?: string): LintSelection {
switch (name) {
case undefined:
case '':
return { eslintTargets: ['.'], includeDuplication: true }
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-sources':
return { eslintTargets: ['packages/*/*/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 'package-tests':
return { eslintTargets: ['packages/*/*/tests/**/*.ts'], includeDuplication: false }
case 'repository':
return {
eslintTargets: [
'.',
'--ignore-pattern',
'packages/*/*/src/**',
'--ignore-pattern',
'packages/*/*/tests/**',
],
includeDuplication: true,
}
default:
throw new Error(`run-gates: unknown DSH_LINT_SHARD ${JSON.stringify(name)}.`)
}
}

View File

@@ -8,15 +8,9 @@ import { spawn } from 'node:child_process'
import { availableParallelism } from 'node:os'
import { resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { coverageArgs } from './coverage-shards.ts'
import { selectLintShard } from './lint-shards.ts'
import { selectSnapshotLane } from './snapshot-shards.ts'
import { selectStaticGates } from './static-shards.ts'
type Mode =
| 'ci-primary'
| 'ci-primary-cpu'
| 'ci-primary-large-runner'
| 'ci-static'
| 'ci-lint'
| 'ci-coverage'
@@ -92,8 +86,6 @@ if (results.some(result => result.gate.allowFailure !== true && (result.status =
function parseMode(raw: string | undefined): Mode {
switch (raw) {
case 'ci-primary':
case 'ci-primary-cpu':
case 'ci-primary-large-runner':
case 'ci-static':
case 'ci-lint':
case 'ci-coverage':
@@ -107,7 +99,7 @@ function parseMode(raw: string | undefined): Mode {
return raw
default:
throw new Error(
`run-gates: expected mode ci-primary | ci-primary-cpu | ci-primary-large-runner | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | doc-sync, got ${JSON.stringify(raw)}.`,
`run-gates: expected mode ci-primary | ci-static | ci-lint | ci-coverage | ci-snapshot | ci-artifacts | ci-windows-blocking | ci-windows-complete | ci-windows-observational | node-compat | doc-sync, got ${JSON.stringify(raw)}.`,
)
}
}
@@ -173,25 +165,17 @@ function gatesForMode(selected: Mode): Gate[] {
switch (selected) {
case 'ci-primary':
return ciPrimaryGates()
case 'ci-primary-cpu':
return ciPrimaryCpuGates()
case 'ci-primary-large-runner':
return ciPrimaryLargeRunnerGates()
case 'ci-static':
return ciStaticGates()
case 'ci-lint': {
const selection = selectLintShard(process.env.DSH_LINT_SHARD)
case 'ci-lint':
return [
lintGate(selection.eslintTargets),
...selection.includeDuplication ? [pnpmScript('duplication', 'duplication')] : [],
lintGate(),
pnpmScript('duplication', 'duplication'),
]
}
case 'ci-coverage':
return [coverageGate()]
case 'ci-snapshot':
return flagEnabled('DSH_SNAPSHOT_PREBUILT')
? [snapshotGate([])]
: [pnpmScript('build', 'build'), snapshotGate()]
return [pnpmScript('build', 'build'), snapshotGate()]
case 'ci-artifacts':
return ciArtifactGates()
case 'ci-windows-blocking':
@@ -217,11 +201,12 @@ function ciPrimaryGates(): Gate[] {
lintGate(),
pnpmScript('duplication', 'duplication'),
coverageGate(),
...nodeCompatSmokeGates(),
snapshotGate(),
...docSyncLeafGates(),
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
pnpmScript('knip', 'knip'),
pnpmScript('build', 'build', { needs: ['typecheck'] }),
pnpmScript('build', 'build'),
pnpmScript('publint', 'publint', { needs: ['build'] }),
pnpmScript('node-next-types', 'verify-node-next-types', {
label: 'node-next types',
@@ -232,30 +217,6 @@ function ciPrimaryGates(): Gate[] {
]
}
function ciPrimaryLargeRunnerGates(): Gate[] {
// The CPU lane owns typecheck, coverage, and the build-to-snapshot chain.
// This core lane starts its own build eagerly for the remaining artifact consumers.
return ciPrimaryGates()
.filter(gate => !['coverage', 'docs-site-build', 'snapshot', 'typecheck'].includes(gate.id))
.map((gate) => {
if (gate.id !== 'build') return gate
const eagerBuild = { ...gate }
delete eagerBuild.needs
return eagerBuild
})
}
function ciPrimaryCpuGates(): Gate[] {
// Build and snapshot stay together so the dependent replay consumes this lane's output.
return [
pnpmScript('typecheck', 'typecheck'),
coverageGate(),
pnpmScript('build', 'build'),
snapshotGate(),
...nodeCompatSmokeGates(),
]
}
function nodeCompatGates(): Gate[] {
return [
...flagEnabled('DSH_NODE_COMPAT_SKIP_TYPECHECK') ? [] : [pnpmScript('typecheck', 'typecheck')],
@@ -279,7 +240,7 @@ function nodeCompatSmokeGates(): Gate[] {
}
function ciStaticGates(): Gate[] {
const gates = [
return [
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
pnpmScript('constraints', 'constraints'),
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
@@ -293,12 +254,10 @@ function ciStaticGates(): Gate[] {
pnpmScript('module-graph', 'verify-module-graph', { label: 'module graph' }),
pnpmScript('knip', 'knip'),
]
return selectStaticGates(gates, process.env.DSH_STATIC_SHARD)
}
function ciArtifactGates(): Gate[] {
const shard = process.env.DSH_ARTIFACT_SHARD
const metadataGates = [
return [
pnpmScript('build', 'build'),
pnpmScript('publint', 'publint', { needs: ['build'] }),
pnpmScript('node-next-types', 'verify-node-next-types', {
@@ -306,13 +265,8 @@ function ciArtifactGates(): Gate[] {
needs: ['build'],
}),
builtPackageInvariantsGate(['build']),
builtBinSmokeGate(),
]
if (shard === 'metadata') return metadataGates
if (shard === 'smoke') return [pnpmScript('build', 'build'), builtBinSmokeGate()]
if (shard !== undefined && shard !== '') {
throw new Error(`run-gates: unknown DSH_ARTIFACT_SHARD ${JSON.stringify(shard)}.`)
}
return [...metadataGates, builtBinSmokeGate()]
}
function ciWindowsBlockingGates(): Gate[] {
@@ -396,12 +350,10 @@ function eslintConcurrencyArgs(): string[] {
}
function coverageGate(): Gate {
const shard = process.env.DSH_COVERAGE_SHARD
return pnpmExec('coverage', [
'vitest',
'run',
'--coverage',
...(shard === undefined || shard === '' ? [] : coverageArgs(shard)),
...positiveIntArg('DSH_COVERAGE_MAX_WORKERS', '--maxWorkers'),
], {
label: 'test:coverage',
@@ -409,23 +361,12 @@ function coverageGate(): Gate {
}
// The snapshot suite boots the example bins in `lib` mode (built artifact under plain Node,
// plugins via real exports). CI normally pairs it with `build`, so it exercises what ships rather
// than the tsx/source path dev uses; callers with prebuilt output may omit that dependency.
function snapshotGate(needs: string[] = ['build']): Gate {
const lane = selectSnapshotLane(process.env.DSH_SNAPSHOT_LANE)
return pnpmExec('snapshot', [
'vitest',
'run',
'--config',
'vitest.snapshot.config.ts',
...lane.files,
], {
label: 'test:snapshot',
env: {
DSH_EXAMPLE_MODE: 'lib',
...lane.scenarioShard === undefined ? {} : { DSH_SNAPSHOT_SCENARIO_SHARD: lane.scenarioShard },
},
...needs.length === 0 ? {} : { needs },
// plugins via real exports). CI pairs it with `build`, so it exercises what ships rather than
// the tsx/source path dev uses and therefore waits on `build`.
function snapshotGate(): Gate {
return pnpmScript('snapshot', 'test:snapshot', {
env: { DSH_EXAMPLE_MODE: 'lib' },
needs: ['build'],
})
}
@@ -492,7 +433,7 @@ function docSyncLeafGates(options: {
]
}
function builtBinSmokeGate(shard?: string): Gate {
function builtBinSmokeGate(): Gate {
return pnpmExec('built-bin-smoke', [
'vitest',
'run',
@@ -508,7 +449,6 @@ function builtBinSmokeGate(shard?: string): Gate {
// (the e2e lane runs unbuilt, so these files self-skip there).
'packages/workflow/workflow-workerthread/tests/built-worker.e2e.ts',
'packages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts',
...(shard === undefined ? [] : [`--shard=${shard}`]),
], {
label: 'built-bin smoke',
needs: ['build'],

View File

@@ -1,54 +0,0 @@
import { existsSync, readdirSync } from 'node:fs'
import { join, relative, sep } from 'node:path'
import { describe, expect, it } from 'vitest'
import { selectSnapshotLane, snapshotLanes } from './snapshot-shards.ts'
const root = join(import.meta.dirname, '..')
function snapshotFiles(dir: string): string[] {
if (!existsSync(dir)) return []
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
const path = join(dir, entry.name)
if (entry.isDirectory()) return snapshotFiles(path)
return entry.name.endsWith('.snapshot.ts') ? [relative(root, path).split(sep).join('/')] : []
})
}
describe('snapshot lanes', () => {
it('assigns every configured snapshot file and every ACP scenario shard', () => {
const discovered = [
...snapshotFiles(join(root, 'examples')),
...snapshotFiles(join(root, 'packages/sdk')),
...snapshotFiles(join(root, 'packages/ui/tui')),
].filter(path => !path.includes('/node_modules/') && !path.includes('/lib/')).sort()
const ordinary = snapshotLanes.filter(lane => lane.scenarioShard === undefined).flatMap(lane => lane.files)
const acp = snapshotLanes.filter(lane => lane.scenarioShard !== undefined)
expect(new Set(ordinary).size).toBe(ordinary.length)
expect(acp.map(lane => lane.files)).toEqual(Array.from(
{ length: 8 },
() => ['examples/acp-agent/tests/acp.snapshot.ts'],
))
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)
})
it('keeps ordinary runs complete and selects known lanes', () => {
expect(selectSnapshotLane()).toEqual({ name: 'complete', files: [] })
expect(selectSnapshotLane('')).toEqual({ name: 'complete', files: [] })
for (const lane of snapshotLanes) expect(selectSnapshotLane(lane.name)).toBe(lane)
})
it('rejects an unknown lane', () => {
expect(() => selectSnapshotLane('missing')).toThrow('unknown DSH_SNAPSHOT_LANE')
})
})

View File

@@ -1,49 +0,0 @@
/** Snapshot-lane definitions for GitHub Actions. */
/** One explicit snapshot file lane, optionally split again by ACP scenarios. */
export interface SnapshotLane {
/** Stable lane name passed through `DSH_SNAPSHOT_LANE`. */
name: string
/** Snapshot test files owned by the lane. */
files: readonly string[]
/** Optional one-based ACP scenario partition. */
scenarioShard?: string
}
/** Exhaustive file ownership plus scenario partitions for the large ACP suite. */
export const snapshotLanes: readonly SnapshotLane[] = [
{
name: 'support',
files: [
'packages/sdk/scripts/tests/config.snapshot.ts',
'packages/sdk/create-sdk/tests/create.snapshot.ts',
'packages/ui/tui/tests/tui.snapshot.ts',
],
},
{
name: 'agents',
files: [
'examples/tui-agent/tests/tui.snapshot.ts',
'examples/acp-agent/tests/goal.snapshot.ts',
'examples/headless-agent/tests/headless.snapshot.ts',
],
},
...Array.from({ length: 8 }, (_, offset) => ({
name: `acp-${offset + 1}`,
files: ['examples/acp-agent/tests/acp.snapshot.ts'],
scenarioShard: `${offset + 1}/8`,
})),
]
/**
* Resolve one CI lane while preserving a complete ordinary snapshot run.
*
* @param name Optional stable lane name.
* @returns An empty file list for the full suite, or one explicit CI lane.
*/
export function selectSnapshotLane(name?: string): SnapshotLane {
if (name === undefined || name === '') return { name: 'complete', files: [] }
const lane = snapshotLanes.find(candidate => candidate.name === name)
if (lane === undefined) throw new Error(`run-gates: unknown DSH_SNAPSHOT_LANE ${JSON.stringify(name)}.`)
return lane
}

View File

@@ -1,26 +0,0 @@
import { describe, expect, it } from 'vitest'
import { selectStaticGates, staticShards } from './static-shards.ts'
const completeInventory = staticShards.flatMap(shard => shard.gateIds).map(id => ({ id }))
describe('static gate shards', () => {
it.each(staticShards)('selects only the gates owned by $name', (shard) => {
expect(selectStaticGates(completeInventory, shard.name).map(gate => gate.id)).toEqual(shard.gateIds)
})
it('selects multiple lanes in gate inventory order', () => {
const selectedNames = new Set(['foundation', 'catalogs', 'prose'])
const expected = staticShards
.filter(shard => selectedNames.has(shard.name))
.flatMap(shard => shard.gateIds)
expect(selectStaticGates(completeInventory, 'foundation,catalogs,prose').map(gate => gate.id)).toEqual(expected)
})
it('rejects missing, duplicate, and unknown assignments', () => {
expect(() => selectStaticGates(completeInventory.slice(1))).toThrow('assign every static gate exactly once')
expect(() => selectStaticGates([...completeInventory, completeInventory[0]!])).toThrow('static gate IDs must be unique')
expect(() => selectStaticGates(completeInventory, 'missing')).toThrow('unknown DSH_STATIC_SHARD')
expect(() => selectStaticGates(completeInventory, 'foundation,foundation')).toThrow('nonempty and unique')
expect(() => selectStaticGates(completeInventory, 'foundation,,prose')).toThrow('nonempty and unique')
})
})

View File

@@ -1,86 +0,0 @@
/** Static-gate shard definitions for GitHub Actions. */
/** A static CI lane identified by the gate IDs it owns. */
export interface StaticShard {
/** Stable lane identifier passed through `DSH_STATIC_SHARD`. */
name: string
/** Gate IDs selected from the static gate inventory. */
gateIds: readonly string[]
}
/** Exhaustive, non-overlapping ownership of static CI gates. */
export const staticShards = [
{
name: 'foundation',
gateIds: [
'runtime-closure',
'constraints',
'package-invariants',
'cordis-config',
'module-graph',
'knip',
],
},
{ name: 'doc-types', gateIds: ['build', 'doc-typecheck'] },
{
name: 'api-contracts',
gateIds: ['cordis-api', 'export-jsdoc', 'scoped-events', 'type-equivalence'],
},
{
name: 'catalogs',
gateIds: ['cordis-catalog', 'tool-catalog', 'config-catalog', 'persistence-catalog', 'doc-graphs'],
},
{
name: 'prose',
gateIds: [
'markdown-wrap',
'markdown-links',
'doc-refs',
'package-paths',
'package-readme-model-experience',
'mermaid',
'agent-note-classification',
'agent-note-format',
'translation-prompt',
'translation-pairing',
'doc-budgets',
'package-readme-limitations',
],
},
{ name: 'site-projection', gateIds: ['docs-site-projection'] },
{ name: 'site-build', gateIds: ['docs-site-build'] },
] as const satisfies readonly StaticShard[]
/**
* Validate the complete gate partition and optionally select one lane.
*
* @param gates Complete static gate inventory.
* @param name Optional comma-separated stable shard names.
* @returns All gates when no shard is requested, otherwise the selected lanes in inventory order.
*/
export function selectStaticGates<T extends { id: string }>(gates: readonly T[], name?: string): T[] {
const gateIds = gates.map(gate => gate.id)
const assignedIds = staticShards.flatMap(shard => shard.gateIds)
const uniqueGateIds = new Set<string>(gateIds)
const uniqueAssignedIds = new Set<string>(assignedIds)
if (uniqueGateIds.size !== gateIds.length) throw new Error('run-gates: static gate IDs must be unique.')
if (uniqueAssignedIds.size !== assignedIds.length) throw new Error('run-gates: static shard gate IDs must be unique.')
if (gateIds.length !== assignedIds.length
|| gateIds.some(id => !uniqueAssignedIds.has(id))
|| assignedIds.some(id => !uniqueGateIds.has(id))) {
throw new Error('run-gates: static shards must assign every static gate exactly once.')
}
if (name === undefined || name === '') return [...gates]
const shardNames = name.split(',')
if (shardNames.some(shardName => shardName === '') || new Set(shardNames).size !== shardNames.length) {
throw new Error(`run-gates: DSH_STATIC_SHARD names must be nonempty and unique, got ${JSON.stringify(name)}.`)
}
const selectedShards = shardNames.map((shardName) => {
const shard = staticShards.find(candidate => candidate.name === shardName)
if (shard === undefined) throw new Error(`run-gates: unknown DSH_STATIC_SHARD ${JSON.stringify(shardName)}.`)
return shard
})
const selectedIds = new Set<string>(selectedShards.flatMap(shard => shard.gateIds))
return gates.filter(gate => selectedIds.has(gate.id))
}