ci: consolidate primary checks on one larger runner
This commit is contained in:
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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`),
|
||||
]
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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)}.`)
|
||||
}
|
||||
}
|
||||
@@ -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'],
|
||||
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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
|
||||
}
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user