mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
ci: enforce bounded build lanes
This commit is contained in:
43
scripts/coverage-shards.spec.ts
Normal file
43
scripts/coverage-shards.spec.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
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`)
|
||||
}
|
||||
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')
|
||||
})
|
||||
})
|
||||
68
scripts/coverage-shards.ts
Normal file
68
scripts/coverage-shards.ts
Normal file
@@ -0,0 +1,68 @@
|
||||
/** 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: 'spine',
|
||||
packageRoots: ['core', 'llm', 'compact', 'context'],
|
||||
extraTestRoots: ['packages/examples/cli-demo/tests'],
|
||||
},
|
||||
{ name: 'sdk', packageRoots: ['sdk'] },
|
||||
{
|
||||
name: 'interfaces',
|
||||
packageRoots: ['ui', 'examples', 'goal'],
|
||||
extraTestRoots: ['examples'],
|
||||
},
|
||||
{ name: 'execution', packageRoots: ['fs', 'bash', 'sandbox', 'code-runtime'] },
|
||||
{ name: 'orchestration', packageRoots: ['workflow', 'subagent', 'tasks'] },
|
||||
{
|
||||
name: 'infrastructure',
|
||||
packageRoots: ['cordis', 'support', 'lsp', 'mcp'],
|
||||
extraTestRoots: ['scripts'],
|
||||
},
|
||||
{
|
||||
name: 'session-state',
|
||||
packageRoots: ['session-persistence', 'session-query'],
|
||||
},
|
||||
{ name: 'hooks-claude', packageRoots: ['hooks/hook-protocol', 'hooks/hooks-claude'] },
|
||||
{ name: 'hooks-codex', packageRoots: ['hooks/hooks-codex'] },
|
||||
{
|
||||
name: 'capabilities',
|
||||
packageRoots: ['web', 'skill', 'spill', 'util', 'guard', 'todo', 'timeout'],
|
||||
},
|
||||
] 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)}.`)
|
||||
}
|
||||
|
||||
const testRoots = new Set([
|
||||
...shard.packageRoots.map(packageRoot => `packages/${packageRoot}`),
|
||||
...('extraTestRoots' in shard ? shard.extraTestRoots : []),
|
||||
'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`),
|
||||
]
|
||||
}
|
||||
61
scripts/publint-all.spec.ts
Normal file
61
scripts/publint-all.spec.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const repositoryRoot = fileURLToPath(new URL('..', import.meta.url))
|
||||
const runner = fileURLToPath(new URL('./publint-all.ts', import.meta.url))
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function fixture(exportPath = './lib/index.js'): string {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-publint-all-'))
|
||||
roots.push(root)
|
||||
const packageDir = join(root, 'packages/core/probe')
|
||||
mkdirSync(join(packageDir, 'lib'), { recursive: true })
|
||||
writeFileSync(join(packageDir, 'package.json'), `${JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-probe',
|
||||
version: '0.0.1',
|
||||
type: 'module',
|
||||
license: 'MIT',
|
||||
engines: { node: '>=22.19' },
|
||||
sideEffects: false,
|
||||
files: ['lib'],
|
||||
exports: { '.': { default: exportPath } },
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(join(packageDir, 'README.md'), '# Probe\n')
|
||||
writeFileSync(join(packageDir, 'lib/index.js'), 'export const probe = true\n')
|
||||
writeFileSync(join(packageDir, 'unpublished.js'), 'export const hidden = true\n')
|
||||
return root
|
||||
}
|
||||
|
||||
function run(root: string) {
|
||||
return spawnSync(process.execPath, [
|
||||
'--import', 'tsx', runner,
|
||||
'--packages-root', root,
|
||||
], {
|
||||
cwd: repositoryRoot,
|
||||
encoding: 'utf8',
|
||||
timeout: 5_000,
|
||||
})
|
||||
}
|
||||
|
||||
describe('publint package runner', () => {
|
||||
it('lints recursively declared files from an in-memory publication view', () => {
|
||||
const result = run(fixture())
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
expect(result.stdout).toContain('linting 1 package(s)')
|
||||
expect(result.stdout).toContain('All good!')
|
||||
})
|
||||
|
||||
it('rejects an export that exists in the workspace but is not published', () => {
|
||||
const result = run(fixture('./unpublished.js'))
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stdout).toContain('unpublished.js')
|
||||
})
|
||||
})
|
||||
@@ -1,46 +1,53 @@
|
||||
import { execFile } from 'node:child_process'
|
||||
import { existsSync, readdirSync } from 'node:fs'
|
||||
/** Run publint over the exact manifest-declared publication view of every package. */
|
||||
|
||||
import {
|
||||
globSync,
|
||||
readFileSync,
|
||||
readdirSync,
|
||||
statSync,
|
||||
} from 'node:fs'
|
||||
import { availableParallelism } from 'node:os'
|
||||
import { resolve } from 'node:path'
|
||||
import { promisify } from 'node:util'
|
||||
import { dirname, relative, resolve, sep } from 'node:path'
|
||||
import { publint, type Message, type PackFile } from 'publint'
|
||||
import { formatMessage } from 'publint/utils'
|
||||
|
||||
const execFileAsync = promisify(execFile)
|
||||
const CONCURRENCY_ENV = 'DSH_PUBLINT_CONCURRENCY'
|
||||
const repositoryRoot = resolve(import.meta.dirname, '..')
|
||||
const options = parseOptions(process.argv.slice(2))
|
||||
const packagesRoot = resolve(options.get('--packages-root') ?? repositoryRoot)
|
||||
|
||||
// Discover harness packages at packages/<group>/<pkg>; group containers,
|
||||
// examples, and private vendored sources are not package targets.
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const packagesRoot = resolve(root, 'packages')
|
||||
interface PackageTarget {
|
||||
path: string
|
||||
directory: string
|
||||
manifest: PackageManifest
|
||||
}
|
||||
|
||||
// Run publint's JS CLI through the current node, not the .bin shim: the
|
||||
// extensionless shim isn't spawnable on Windows (CVE-2024-27980) and the .cmd
|
||||
// variant needs shell:true, which space-joins args UNESCAPED (DEP0190) and
|
||||
// breaks when the repo path contains spaces. The JS entry is identical on every
|
||||
// platform (`bin` is `./src/cli.js` per publint's package.json).
|
||||
const publintCli = resolve(root, 'node_modules/publint/src/cli.js')
|
||||
interface PackageManifest {
|
||||
name?: string
|
||||
files?: unknown
|
||||
}
|
||||
|
||||
type PublintResult =
|
||||
| { path: string; status: 'passed'; stdout: string; stderr: string }
|
||||
| { path: string; status: 'failed'; stdout: string; stderr: string; message: string }
|
||||
| { path: string; status: 'passed'; messages: Message[]; manifest: Record<string, unknown> }
|
||||
| { path: string; status: 'failed'; messages: Message[]; manifest: Record<string, unknown>; failure?: string }
|
||||
|
||||
function workspacePackages(): string[] {
|
||||
return readdirSync(packagesRoot, { withFileTypes: true })
|
||||
.filter(group => group.isDirectory())
|
||||
.flatMap(group =>
|
||||
readdirSync(resolve(packagesRoot, group.name), { withFileTypes: true })
|
||||
.filter(pkg => pkg.isDirectory())
|
||||
.filter(pkg => existsSync(resolve(packagesRoot, group.name, pkg.name, 'package.json')))
|
||||
.map(pkg => `packages/${group.name}/${pkg.name}`),
|
||||
)
|
||||
function workspacePackages(): PackageTarget[] {
|
||||
return globSync('packages/*/*/package.json', { cwd: packagesRoot })
|
||||
.sort()
|
||||
.map((manifestPath) => {
|
||||
const absoluteManifestPath = resolve(packagesRoot, manifestPath)
|
||||
const manifest = JSON.parse(readFileSync(absoluteManifestPath, 'utf8')) as PackageManifest
|
||||
return { path: dirname(manifestPath), directory: dirname(absoluteManifestPath), manifest }
|
||||
})
|
||||
}
|
||||
|
||||
function publintConcurrency(total: number): number {
|
||||
if (total === 0) return 0
|
||||
|
||||
const raw = process.env[CONCURRENCY_ENV]
|
||||
if (raw !== undefined) {
|
||||
if (raw !== undefined && raw !== '') {
|
||||
const parsed = Number.parseInt(raw, 10)
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1) {
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
|
||||
throw new Error(`publint-all: ${CONCURRENCY_ENV} must be a positive integer, got ${JSON.stringify(raw)}.`)
|
||||
}
|
||||
return Math.min(total, parsed)
|
||||
@@ -49,57 +56,106 @@ function publintConcurrency(total: number): number {
|
||||
return Math.min(total, availableParallelism())
|
||||
}
|
||||
|
||||
function outputText(value: unknown): string {
|
||||
if (typeof value === 'string') return value
|
||||
if (Buffer.isBuffer(value)) return value.toString()
|
||||
return ''
|
||||
function publicationFiles(target: PackageTarget): PackFile[] {
|
||||
const paths = new Set<string>()
|
||||
addPath(resolve(target.directory, 'package.json'), paths)
|
||||
const declared = Array.isArray(target.manifest.files)
|
||||
? target.manifest.files.filter((value): value is string => typeof value === 'string')
|
||||
: []
|
||||
for (const pattern of [
|
||||
...declared,
|
||||
'README*',
|
||||
'LICENSE*',
|
||||
'LICENCE*',
|
||||
'CHANGELOG*',
|
||||
'CHANGES*',
|
||||
'HISTORY*',
|
||||
'NOTICE*',
|
||||
]) {
|
||||
for (const match of globSync(pattern, { cwd: target.directory })) {
|
||||
addPath(resolve(target.directory, match), paths)
|
||||
}
|
||||
}
|
||||
|
||||
return [...paths]
|
||||
.sort()
|
||||
.map(path => ({
|
||||
name: `package/${relative(target.directory, path).split(sep).join('/')}`,
|
||||
data: readFileSync(path),
|
||||
}))
|
||||
}
|
||||
|
||||
async function runPublint(path: string): Promise<PublintResult> {
|
||||
function addPath(path: string, paths: Set<string>): void {
|
||||
const stat = statSync(path)
|
||||
if (stat.isDirectory()) {
|
||||
for (const entry of readdirSync(path)) addPath(resolve(path, entry), paths)
|
||||
} else if (stat.isFile()) {
|
||||
paths.add(path)
|
||||
}
|
||||
}
|
||||
|
||||
async function runPublint(target: PackageTarget): Promise<PublintResult> {
|
||||
try {
|
||||
const { stdout, stderr } = await execFileAsync(process.execPath, [publintCli, path], {
|
||||
cwd: root,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
const result = await publint({
|
||||
pkgDir: 'package',
|
||||
pack: { files: publicationFiles(target) },
|
||||
})
|
||||
return { path, status: 'passed', stdout, stderr }
|
||||
const manifest = result.pkg as Record<string, unknown>
|
||||
return result.messages.some(message => message.type === 'error')
|
||||
? { path: target.path, status: 'failed', messages: result.messages, manifest }
|
||||
: { path: target.path, status: 'passed', messages: result.messages, manifest }
|
||||
} catch (error: unknown) {
|
||||
const failed = error as { stdout?: unknown; stderr?: unknown; message?: string }
|
||||
return {
|
||||
path,
|
||||
path: target.path,
|
||||
status: 'failed',
|
||||
stdout: outputText(failed.stdout),
|
||||
stderr: outputText(failed.stderr),
|
||||
message: failed.message ?? 'publint failed',
|
||||
messages: [],
|
||||
manifest: target.manifest as Record<string, unknown>,
|
||||
failure: error instanceof Error ? error.message : String(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function runAll(paths: string[], concurrency: number): Promise<PublintResult[]> {
|
||||
async function runAll(targets: PackageTarget[], concurrency: number): Promise<PublintResult[]> {
|
||||
let next = 0
|
||||
const results: Array<PublintResult | undefined> = []
|
||||
await Promise.all(Array.from({ length: concurrency }, async () => {
|
||||
for (;;) {
|
||||
const index = next
|
||||
next += 1
|
||||
const path = paths[index]
|
||||
if (path === undefined) return
|
||||
results[index] = await runPublint(path)
|
||||
const target = targets[index]
|
||||
if (target === undefined) return
|
||||
results[index] = await runPublint(target)
|
||||
}
|
||||
}))
|
||||
|
||||
return paths.map((path, index) => {
|
||||
return targets.map((target, index) => {
|
||||
const result = results[index]
|
||||
if (result === undefined) throw new Error(`publint-all: missing result for ${path}.`)
|
||||
if (result === undefined) throw new Error(`publint-all: missing result for ${target.path}.`)
|
||||
return result
|
||||
})
|
||||
}
|
||||
|
||||
function printResult(result: PublintResult): void {
|
||||
console.log(`Running publint for ${result.path}...`)
|
||||
process.stdout.write(result.stdout)
|
||||
process.stderr.write(result.stderr)
|
||||
if (result.status === 'failed') console.error(result.message)
|
||||
if ('failure' in result) console.error(result.failure)
|
||||
for (const message of result.messages) {
|
||||
console.log(formatMessage(message, result.manifest, { color: false }) ?? message.code)
|
||||
}
|
||||
if (result.status === 'passed' && result.messages.length === 0) console.log('All good!')
|
||||
}
|
||||
|
||||
function parseOptions(args: string[]): Map<string, string> {
|
||||
const parsed = new Map<string, string>()
|
||||
for (let index = 0; index < args.length; index += 2) {
|
||||
const name = args[index]
|
||||
const value = args[index + 1]
|
||||
if (name !== '--packages-root' || value === undefined || value.startsWith('--')) {
|
||||
throw new Error(`publint-all: expected [--packages-root PATH], got ${JSON.stringify(args)}.`)
|
||||
}
|
||||
if (parsed.has(name)) throw new Error(`publint-all: duplicate option ${name}.`)
|
||||
parsed.set(name, value)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
const packages = workspacePackages()
|
||||
|
||||
@@ -8,6 +8,8 @@ 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 { selectStaticGates } from './static-shards.ts'
|
||||
|
||||
type Mode =
|
||||
| 'ci-primary'
|
||||
@@ -161,20 +163,16 @@ function gatesForMode(selected: Mode): Gate[] {
|
||||
pnpmScript('duplication', 'duplication'),
|
||||
]
|
||||
case 'ci-coverage':
|
||||
return [
|
||||
pnpmScript('build', 'build'),
|
||||
coverageGate(),
|
||||
]
|
||||
return [coverageGate()]
|
||||
case 'ci-snapshot':
|
||||
return [
|
||||
pnpmScript('build', 'build'),
|
||||
snapshotGate(),
|
||||
]
|
||||
return flagEnabled('DSH_SNAPSHOT_PREBUILT')
|
||||
? [snapshotGate([])]
|
||||
: [pnpmScript('build', 'build'), snapshotGate()]
|
||||
case 'ci-artifacts':
|
||||
return ciArtifactGates()
|
||||
case 'node-compat':
|
||||
return [
|
||||
pnpmScript('typecheck', 'typecheck'),
|
||||
...flagEnabled('DSH_NODE_COMPAT_SKIP_TYPECHECK') ? [] : [pnpmScript('typecheck', 'typecheck')],
|
||||
pnpmExec('source-worker-smoke', [
|
||||
'vitest',
|
||||
'run',
|
||||
@@ -230,7 +228,7 @@ function ciPrimaryGates(): Gate[] {
|
||||
}
|
||||
|
||||
function ciStaticGates(): Gate[] {
|
||||
return [
|
||||
const gates = [
|
||||
pnpmScript('runtime-closure', 'verify-runtime-closure', { label: 'runtime closure' }),
|
||||
pnpmScript('constraints', 'constraints'),
|
||||
pnpmScript('package-invariants', 'verify-package-invariants', { label: 'package invariants' }),
|
||||
@@ -239,10 +237,12 @@ 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[] {
|
||||
return [
|
||||
const shard = process.env.DSH_ARTIFACT_SHARD
|
||||
const metadataGates = [
|
||||
pnpmScript('build', 'build'),
|
||||
pnpmScript('publint', 'publint', { needs: ['build'] }),
|
||||
pnpmScript('node-next-types', 'verify-node-next-types', {
|
||||
@@ -250,8 +250,14 @@ function ciArtifactGates(): Gate[] {
|
||||
needs: ['build'],
|
||||
}),
|
||||
builtPackageInvariantsGate(['build']),
|
||||
builtBinSmokeGate(),
|
||||
]
|
||||
if (shard === 'metadata') return metadataGates
|
||||
if (shard === 'smoke-1') return [pnpmScript('build', 'build'), builtBinSmokeGate('1/2')]
|
||||
if (shard === 'smoke-2') return [pnpmScript('build', 'build'), builtBinSmokeGate('2/2')]
|
||||
if (shard !== undefined && shard !== '') {
|
||||
throw new Error(`run-gates: unknown DSH_ARTIFACT_SHARD ${JSON.stringify(shard)}.`)
|
||||
}
|
||||
return [...metadataGates, builtBinSmokeGate()]
|
||||
}
|
||||
|
||||
function lintGate(): Gate {
|
||||
@@ -275,25 +281,36 @@ function lintGate(): Gate {
|
||||
}
|
||||
|
||||
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',
|
||||
env: { DSH_EXAMPLE_MODE: 'lib' },
|
||||
needs: ['build'],
|
||||
})
|
||||
}
|
||||
|
||||
// The snapshot suite boots the example bins in `lib` mode (built artifact under plain Node,
|
||||
// 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(): Gate {
|
||||
return pnpmScript('snapshot', 'test:snapshot', {
|
||||
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)}.`)
|
||||
}
|
||||
return pnpmExec('snapshot', [
|
||||
'vitest',
|
||||
'run',
|
||||
'--config',
|
||||
'vitest.snapshot.config.ts',
|
||||
...(shard === undefined || shard === '' ? [] : [`--shard=${shard}`]),
|
||||
], {
|
||||
label: 'test:snapshot',
|
||||
env: { DSH_EXAMPLE_MODE: 'lib' },
|
||||
needs: ['build'],
|
||||
...needs.length === 0 ? {} : { needs },
|
||||
})
|
||||
}
|
||||
|
||||
@@ -314,6 +331,13 @@ function positiveIntArg(envName: string, flag: string): string[] {
|
||||
return [`${flag}=${raw}`]
|
||||
}
|
||||
|
||||
function flagEnabled(envName: string): boolean {
|
||||
const raw = process.env[envName]
|
||||
if (raw === undefined || raw === '') return false
|
||||
if (raw !== '1') throw new Error(`run-gates: ${envName} must be 1 when set, got ${JSON.stringify(raw)}.`)
|
||||
return true
|
||||
}
|
||||
|
||||
function hygieneLeafGates(options: { artifactNeeds?: string[] } = {}): Gate[] {
|
||||
const artifactOptions = options.artifactNeeds === undefined ? {} : { needs: options.artifactNeeds }
|
||||
return [
|
||||
@@ -363,7 +387,7 @@ function docSyncLeafGates(options: {
|
||||
]
|
||||
}
|
||||
|
||||
function builtBinSmokeGate(): Gate {
|
||||
function builtBinSmokeGate(shard?: string): Gate {
|
||||
return pnpmExec('built-bin-smoke', [
|
||||
'vitest',
|
||||
'run',
|
||||
@@ -379,6 +403,7 @@ function builtBinSmokeGate(): 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'],
|
||||
|
||||
16
scripts/static-shards.spec.ts
Normal file
16
scripts/static-shards.spec.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
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('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')
|
||||
})
|
||||
})
|
||||
77
scripts/static-shards.ts
Normal file
77
scripts/static-shards.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/** 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: 'api-contracts',
|
||||
gateIds: ['doc-typecheck', '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', gateIds: ['docs-site'] },
|
||||
] as const satisfies readonly StaticShard[]
|
||||
|
||||
/**
|
||||
* Validate the complete gate partition and optionally select one lane.
|
||||
*
|
||||
* @param gates Complete static gate inventory.
|
||||
* @param name Optional stable shard name.
|
||||
* @returns All gates when no shard is requested, otherwise the selected lane.
|
||||
*/
|
||||
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 shard = staticShards.find(candidate => candidate.name === name)
|
||||
if (shard === undefined) throw new Error(`run-gates: unknown DSH_STATIC_SHARD ${JSON.stringify(name)}.`)
|
||||
const selectedIds = new Set<string>(shard.gateIds)
|
||||
return gates.filter(gate => selectedIds.has(gate.id))
|
||||
}
|
||||
@@ -1,102 +1,106 @@
|
||||
/** Verify every packed companion through its package self-reference under plain Node. */
|
||||
/** Verify every compiled companion through its staged package self-reference under plain Node. */
|
||||
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import {
|
||||
copyFileSync,
|
||||
cpSync,
|
||||
existsSync,
|
||||
globSync,
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} from 'node:fs'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
|
||||
const root = resolve(import.meta.dirname, '..')
|
||||
const loaderUrl = pathToFileURL(resolve(root, 'vendor/loader/lib/index.js')).href
|
||||
const repositoryRoot = resolve(import.meta.dirname, '..')
|
||||
const options = parseOptions(process.argv.slice(2))
|
||||
const packagesRoot = resolve(options.get('--packages-root') ?? repositoryRoot)
|
||||
const loaderUrl = options.get('--loader-url')
|
||||
?? pathToFileURL(resolve(repositoryRoot, 'vendor/loader/lib/index.js')).href
|
||||
const failures = []
|
||||
const manifests = globSync('packages/*/*/package.json', { cwd: root }).sort()
|
||||
const packArgs = ['pack', '--dry-run', '--json', '--ignore-scripts']
|
||||
// Windows cannot spawn npm's .cmd shim directly; setup-node installs this JS
|
||||
// entrypoint beside node.exe, so the probe stays shell-free on every runner.
|
||||
const npmInvocation = process.platform === 'win32'
|
||||
? [process.execPath, [resolve(dirname(process.execPath), 'node_modules/npm/bin/npm-cli.js'), ...packArgs]]
|
||||
: ['npm', packArgs]
|
||||
const manifests = globSync('packages/*/*/package.json', { cwd: packagesRoot }).sort()
|
||||
const { default: Loader } = await import(loaderUrl)
|
||||
const loader = Object.create(Loader.prototype)
|
||||
|
||||
for (const manifestPath of manifests) {
|
||||
const packageDir = dirname(resolve(root, manifestPath))
|
||||
const manifest = JSON.parse(readFileSync(resolve(root, manifestPath), 'utf8'))
|
||||
const packageDir = dirname(resolve(packagesRoot, manifestPath))
|
||||
const manifest = JSON.parse(readFileSync(resolve(packagesRoot, manifestPath), 'utf8'))
|
||||
const packageName = manifest.name
|
||||
if (typeof packageName !== 'string' || packageName.length === 0) {
|
||||
failures.push(`${manifestPath}: missing package name`)
|
||||
continue
|
||||
}
|
||||
|
||||
const pack = spawnSync(npmInvocation[0], npmInvocation[1], {
|
||||
cwd: packageDir,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (pack.status !== 0) {
|
||||
const detail = pack.error?.message
|
||||
?? (pack.stderr.trim() || pack.stdout.trim() || `npm pack exited ${pack.status}`)
|
||||
failures.push(`${packageName}: ${detail}`)
|
||||
const invariantExport = manifest.exports?.['./invariant']
|
||||
if (typeof invariantExport !== 'object'
|
||||
|| invariantExport.default !== './lib/invariant.js'
|
||||
|| !manifest.files?.includes('lib/invariant.js')) {
|
||||
failures.push(`${packageName}: manifest does not publish ./lib/invariant.js as ./invariant`)
|
||||
continue
|
||||
}
|
||||
|
||||
let files
|
||||
try {
|
||||
const result = JSON.parse(pack.stdout)
|
||||
files = result[0]?.files
|
||||
if (!Array.isArray(files)) throw new Error('npm pack returned no file inventory')
|
||||
} catch (error) {
|
||||
failures.push(`${packageName}: cannot parse npm pack inventory: ${String(error)}`)
|
||||
continue
|
||||
}
|
||||
|
||||
// Keep the packed view below its owning package so Node reaches the real
|
||||
// Keep the staged view below its owning package so Node reaches the real
|
||||
// pnpm dependency links. Junctioning node_modules elsewhere breaks pnpm's
|
||||
// relative workspace links on Windows.
|
||||
const stagedPackageDir = mkdtempSync(resolve(packageDir, '.dsh-packed-invariant-'))
|
||||
// relative workspace links on Windows. Copy only the statically required
|
||||
// runtime entry so a companion that imports an undeclared chunk fails here.
|
||||
const stagedPackageDir = mkdtempSync(resolve(packageDir, '.dsh-built-invariant-'))
|
||||
try {
|
||||
for (const file of files) {
|
||||
if (typeof file.path !== 'string'
|
||||
|| (file.path !== 'package.json' && !file.path.startsWith('lib/'))) continue
|
||||
const target = resolve(stagedPackageDir, file.path)
|
||||
mkdirSync(dirname(target), { recursive: true })
|
||||
copyFileSync(resolve(packageDir, file.path), target)
|
||||
}
|
||||
|
||||
const probe = `
|
||||
const companion = await import(${JSON.stringify(`${packageName}/invariant`)});
|
||||
const { default: Loader } = await import(${JSON.stringify(loaderUrl)});
|
||||
if ('default' in companion) throw new Error('companion has a default export');
|
||||
const loader = Object.create(Loader.prototype);
|
||||
const unwrapped = loader.unwrapExports(companion);
|
||||
if (unwrapped !== companion) throw new Error('Loader collapsed the companion namespace');
|
||||
if (typeof unwrapped.name !== 'string') throw new Error('companion name is missing');
|
||||
if (!Array.isArray(unwrapped.inject) || !unwrapped.inject.includes('invariants')) {
|
||||
throw new Error('companion does not inject invariants');
|
||||
}
|
||||
if (typeof unwrapped.apply !== 'function') throw new Error('companion apply is missing');
|
||||
`
|
||||
const result = spawnSync(process.execPath, ['--input-type=module', '--eval', probe], {
|
||||
cwd: stagedPackageDir,
|
||||
encoding: 'utf8',
|
||||
})
|
||||
if (result.status !== 0) {
|
||||
const detail = result.error?.message
|
||||
?? (result.stderr.trim() || result.stdout.trim() || `node exited ${result.status}`)
|
||||
failures.push(`${packageName}: ${detail}`)
|
||||
copyFileSync(resolve(packageDir, 'package.json'), resolve(stagedPackageDir, 'package.json'))
|
||||
copyDeclaredLibFiles(packageDir, stagedPackageDir, manifest.files)
|
||||
const probePath = resolve(stagedPackageDir, 'probe.mjs')
|
||||
writeFileSync(
|
||||
probePath,
|
||||
`import * as companion from ${JSON.stringify(`${packageName}/invariant`)}\nexport default companion\n`,
|
||||
)
|
||||
const { default: companion } = await import(pathToFileURL(probePath).href)
|
||||
if ('default' in companion) throw new Error('companion has a default export')
|
||||
const unwrapped = loader.unwrapExports(companion)
|
||||
if (unwrapped !== companion) throw new Error('Loader collapsed the companion namespace')
|
||||
if (typeof unwrapped.name !== 'string') throw new Error('companion name is missing')
|
||||
if (!Array.isArray(unwrapped.inject) || !unwrapped.inject.includes('invariants')) {
|
||||
throw new Error('companion does not inject invariants')
|
||||
}
|
||||
if (typeof unwrapped.apply !== 'function') throw new Error('companion apply is missing')
|
||||
} catch (error) {
|
||||
failures.push(`${packageName}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
} finally {
|
||||
rmSync(stagedPackageDir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
|
||||
if (failures.length > 0) {
|
||||
console.error('verify-built-package-invariants: packed companion failures:')
|
||||
console.error('verify-built-package-invariants: compiled companion failures:')
|
||||
for (const failure of failures) console.error(` ${failure}`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`verify-built-package-invariants: ${manifests.length} packed companion(s) passed plain-Node Loader checks.`)
|
||||
console.log(`verify-built-package-invariants: ${manifests.length} compiled companion(s) passed plain-Node Loader checks.`)
|
||||
|
||||
function parseOptions(args) {
|
||||
const allowed = new Set(['--packages-root', '--loader-url'])
|
||||
const parsed = new Map()
|
||||
for (let index = 0; index < args.length; index += 2) {
|
||||
const name = args[index]
|
||||
const value = args[index + 1]
|
||||
if (!allowed.has(name) || value === undefined || value.startsWith('--')) {
|
||||
throw new Error(`verify-built-package-invariants: expected [--packages-root PATH] [--loader-url URL], got ${JSON.stringify(args)}.`)
|
||||
}
|
||||
if (parsed.has(name)) throw new Error(`verify-built-package-invariants: duplicate option ${name}.`)
|
||||
parsed.set(name, value)
|
||||
}
|
||||
return parsed
|
||||
}
|
||||
|
||||
function copyDeclaredLibFiles(packageDir, stagedPackageDir, files) {
|
||||
for (const pattern of files) {
|
||||
if (!pattern.startsWith('lib/')) continue
|
||||
for (const relativePath of globSync(pattern, { cwd: packageDir })) {
|
||||
const source = resolve(packageDir, relativePath)
|
||||
if (!existsSync(source)) continue
|
||||
const target = resolve(stagedPackageDir, relativePath)
|
||||
mkdirSync(dirname(target), { recursive: true })
|
||||
cpSync(source, target, { recursive: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
88
scripts/verify-built-package-invariants.spec.ts
Normal file
88
scripts/verify-built-package-invariants.spec.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
|
||||
const verifier = fileURLToPath(new URL('./verify-built-package-invariants.mjs', import.meta.url))
|
||||
const roots: string[] = []
|
||||
|
||||
afterEach(() => {
|
||||
for (const root of roots.splice(0)) rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
function fixture(options: {
|
||||
invariantSource?: string
|
||||
invariantExport?: string
|
||||
runtimeChunk?: string
|
||||
} = {}): { root: string; loaderUrl: string } {
|
||||
const root = mkdtempSync(join(tmpdir(), 'dsh-built-package-invariants-'))
|
||||
roots.push(root)
|
||||
const packageDir = join(root, 'packages/core/probe')
|
||||
mkdirSync(join(packageDir, 'lib'), { recursive: true })
|
||||
writeFileSync(join(packageDir, 'package.json'), `${JSON.stringify({
|
||||
name: '@deepseek-ai/dsh-probe',
|
||||
type: 'module',
|
||||
files: ['lib/invariant.js'],
|
||||
exports: {
|
||||
'./invariant': {
|
||||
default: options.invariantExport ?? './lib/invariant.js',
|
||||
},
|
||||
},
|
||||
}, null, 2)}\n`)
|
||||
writeFileSync(
|
||||
join(packageDir, 'lib/invariant.js'),
|
||||
options.invariantSource ?? "export const name = 'probe-invariant'\nexport const inject = ['invariants']\nexport const apply = () => {}\n",
|
||||
)
|
||||
if (options.runtimeChunk !== undefined) {
|
||||
writeFileSync(join(packageDir, 'lib/chunk.js'), options.runtimeChunk)
|
||||
}
|
||||
const loaderPath = join(root, 'loader.mjs')
|
||||
writeFileSync(loaderPath, 'export default class Loader { unwrapExports(value) { return value } }\n')
|
||||
return { root, loaderUrl: pathToFileURL(loaderPath).href }
|
||||
}
|
||||
|
||||
function verify(root: string, loaderUrl: string) {
|
||||
return spawnSync(process.execPath, [
|
||||
verifier,
|
||||
'--packages-root', root,
|
||||
'--loader-url', loaderUrl,
|
||||
], {
|
||||
encoding: 'utf8',
|
||||
timeout: 5_000,
|
||||
})
|
||||
}
|
||||
|
||||
describe('built package invariant verifier', () => {
|
||||
it('loads the staged compiled self-reference through plain Node and Loader normalization', () => {
|
||||
const { root, loaderUrl } = fixture()
|
||||
const result = verify(root, loaderUrl)
|
||||
expect(result.status, result.stderr).toBe(0)
|
||||
expect(result.stdout).toContain('1 compiled companion(s) passed plain-Node Loader checks')
|
||||
})
|
||||
|
||||
it('rejects a default export and a broken invariant export map', () => {
|
||||
const withDefault = fixture({
|
||||
invariantSource: "export default {}\nexport const name = 'probe-invariant'\nexport const inject = ['invariants']\nexport const apply = () => {}\n",
|
||||
})
|
||||
const defaultResult = verify(withDefault.root, withDefault.loaderUrl)
|
||||
expect(defaultResult.status).toBe(1)
|
||||
expect(defaultResult.stderr).toContain('companion has a default export')
|
||||
|
||||
const brokenExport = fixture({ invariantExport: './lib/missing.js' })
|
||||
const exportResult = verify(brokenExport.root, brokenExport.loaderUrl)
|
||||
expect(exportResult.status).toBe(1)
|
||||
expect(exportResult.stderr).toContain('@deepseek-ai/dsh-probe')
|
||||
})
|
||||
|
||||
it('rejects an invariant bundle that needs an unstaged runtime chunk', () => {
|
||||
const { root, loaderUrl } = fixture({
|
||||
invariantSource: "export * from './chunk.js'\n",
|
||||
runtimeChunk: "export const name = 'probe-invariant'\nexport const inject = ['invariants']\nexport const apply = () => {}\n",
|
||||
})
|
||||
const result = verify(root, loaderUrl)
|
||||
expect(result.status).toBe(1)
|
||||
expect(result.stderr).toContain('chunk.js')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user