ci: enforce bounded build lanes

This commit is contained in:
Tianyi Cui
2026-07-21 20:15:09 +08:00
parent 99f67adb69
commit f1202d0dd8
16 changed files with 769 additions and 191 deletions

View 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')
})
})

View 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`),
]
}

View 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')
})
})

View File

@@ -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()

View File

@@ -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'],

View 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
View 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))
}

View File

@@ -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 })
}
}
}

View 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')
})
})