ci: split remaining one-minute lanes
This commit is contained in:
@@ -10,6 +10,7 @@ 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 =
|
||||
@@ -307,19 +308,19 @@ function coverageGate(): Gate {
|
||||
// plugins via real exports) — CI and pre-push already build, so they exercise what ships rather
|
||||
// than the tsx/source path dev uses. It therefore waits on `build`.
|
||||
function snapshotGate(needs: string[] = ['build']): Gate {
|
||||
const shard = process.env.DSH_SNAPSHOT_SHARD
|
||||
if (shard !== undefined && shard !== '' && !/^\d+\/\d+$/.test(shard)) {
|
||||
throw new Error(`run-gates: DSH_SNAPSHOT_SHARD must be INDEX/TOTAL, got ${JSON.stringify(shard)}.`)
|
||||
}
|
||||
const lane = selectSnapshotLane(process.env.DSH_SNAPSHOT_LANE)
|
||||
return pnpmExec('snapshot', [
|
||||
'vitest',
|
||||
'run',
|
||||
'--config',
|
||||
'vitest.snapshot.config.ts',
|
||||
...(shard === undefined || shard === '' ? [] : [`--shard=${shard}`]),
|
||||
...lane.files,
|
||||
], {
|
||||
label: 'test:snapshot',
|
||||
env: { DSH_EXAMPLE_MODE: 'lib' },
|
||||
env: {
|
||||
DSH_EXAMPLE_MODE: 'lib',
|
||||
...lane.scenarioShard === undefined ? {} : { DSH_SNAPSHOT_SCENARIO_SHARD: lane.scenarioShard },
|
||||
},
|
||||
...needs.length === 0 ? {} : { needs },
|
||||
})
|
||||
}
|
||||
@@ -392,8 +393,11 @@ function docSyncLeafGates(options: {
|
||||
pnpmScript('translation-prompt', 'verify-translation-prompt', { label: 'translation prompt' }),
|
||||
pnpmScript('translation-pairing', 'verify-translation-pairing', { label: 'translation pairing' }),
|
||||
pnpmScript('doc-budgets', 'verify-doc-budgets', { label: 'doc budgets' }),
|
||||
// Keep the VitePress build in this single gate because projection rewrites website/.generated.
|
||||
pnpmScript('docs-site', 'docs:check', { label: 'documentation site' }),
|
||||
pnpmExec('docs-site-projection', ['vitest', 'run', 'scripts/project-doc-site.spec.ts'], {
|
||||
label: 'documentation projection',
|
||||
}),
|
||||
// Keep the VitePress build itself in one gate because projection rewrites website/.generated.
|
||||
pnpmScript('docs-site-build', 'docs:build', { label: 'documentation build' }),
|
||||
pnpmScript('package-readme-limitations', 'verify-package-readme-limitations', { label: 'package README limitations' }),
|
||||
]
|
||||
}
|
||||
|
||||
45
scripts/snapshot-shards.spec.ts
Normal file
45
scripts/snapshot-shards.spec.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
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: 4 },
|
||||
() => ['examples/acp-agent/tests/acp.snapshot.ts'],
|
||||
))
|
||||
expect(acp.map(lane => lane.scenarioShard)).toEqual(['1/4', '2/4', '3/4', '4/4'])
|
||||
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')
|
||||
})
|
||||
})
|
||||
54
scripts/snapshot-shards.ts
Normal file
54
scripts/snapshot-shards.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
/** 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/ui/tui/tests/tui.snapshot.ts',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'demos',
|
||||
files: [
|
||||
'examples/tui-agent/tests/tui.snapshot.ts',
|
||||
'packages/sdk/create-sdk/tests/create.snapshot.ts',
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'agents',
|
||||
files: [
|
||||
'examples/acp-agent/tests/goal.snapshot.ts',
|
||||
'examples/headless-agent/tests/headless.snapshot.ts',
|
||||
],
|
||||
},
|
||||
...Array.from({ length: 4 }, (_, offset) => ({
|
||||
name: `acp-${offset + 1}`,
|
||||
files: ['examples/acp-agent/tests/acp.snapshot.ts'],
|
||||
scenarioShard: `${offset + 1}/4`,
|
||||
})),
|
||||
]
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -46,7 +46,8 @@ export const staticShards = [
|
||||
'package-readme-limitations',
|
||||
],
|
||||
},
|
||||
{ name: 'site', gateIds: ['docs-site'] },
|
||||
{ name: 'site-projection', gateIds: ['docs-site-projection'] },
|
||||
{ name: 'site-build', gateIds: ['docs-site-build'] },
|
||||
] as const satisfies readonly StaticShard[]
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user