Merge remote-tracking branch 'origin/master' into worktree/3116-docs-mpa-idempotence

This commit is contained in:
Yichen Jiang
2026-08-26 14:33:23 +08:00
14 changed files with 173 additions and 360 deletions
+2 -2
View File
@@ -98,9 +98,9 @@ describe('CI workflow', () => {
))
expect(buildCommands.map(step => step.run)).toContain('pnpm run check:ci:windows-blocking')
// windows-coverage uses the lower 4-partition profile.
// windows-coverage runs the 6-partition profile.
expect(windowsCoverage.name).toBe('windows node 24 / coverage')
expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '4' })
expect(windowsCoverage.env).toMatchObject({ DSH_COVERAGE_PARTITIONS: '6' })
const coverageSteps = windowsCoverage.steps as unknown[]
const coverageCommands = coverageSteps.filter((step): step is Record<string, unknown> & { run: string } => (
isRecord(step) && typeof step.run === 'string'
+10 -9
View File
@@ -34,20 +34,21 @@ export const coverageExemptHeavySuites: readonly CoverageExemptSuite[] = [
filter: 'packages/typert/generator/tests/',
exclude: 'packages/typert/generator/tests/**',
},
// The webworker-runtime package is outside the coverage requirement by
// decision: vitest.config.ts threshold-excludes its src, so every suite
// runs uninstrumented. This tree includes the full-corpus import gate, a
// single 900s-budget case that spawns a child sweep over every built
// bundle; inside an instrumented partition it exceeds the Windows
// partition budget under load.
{
filter: 'packages/experimental/webworker-runtime/tests/',
exclude: 'packages/experimental/webworker-runtime/tests/**',
},
// Real child-process fixtures over scripts/ sources, which coverage never measures.
{ filter: 'scripts/install-lefthook.spec.ts', exclude: 'scripts/install-lefthook.spec.ts' },
{ filter: 'scripts/oxlint-contract.spec.ts', exclude: 'scripts/oxlint-contract.spec.ts' },
{ filter: 'scripts/change-scope.spec.ts', exclude: 'scripts/change-scope.spec.ts' },
{ filter: 'scripts/translation-pairing-merge.spec.ts', exclude: 'scripts/translation-pairing-merge.spec.ts' },
// Spawns the full-corpus transform gate in a child process (Node's ESM
// loader is its oracle), so no measured file executes in-process; the
// package src is threshold-excluded in vitest.config.ts. A single
// 900s-budget case; running it inside an instrumented partition exceeds
// the Windows partition budget under load.
{
filter: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts',
exclude: 'packages/experimental/webworker-runtime/tests/compile/transform-corpus.spec.ts',
},
// Built-artifact proof. Packer/runtime src is threshold-excluded, and the
// native Windows aggregate makes this uninstrumented gate wait for build so
// the suite never observes a partially emitted workspace closure.
+35 -9
View File
@@ -11,7 +11,7 @@ import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { parseArgs } from 'node:util'
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
import { isEntry, run } from './process.ts'
import { isEntry, runConcurrent } from './process.ts'
import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
/** Where pack output lands when `--out` is omitted. */
@@ -24,8 +24,8 @@ const DEFAULT_OUTPUT = 'dist/npm'
* @param destination - absolute output directory.
* @returns The tarball filename.
*/
function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): string {
run('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
async function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): Promise<string> {
await runConcurrent('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
const filename = tarballName(member)
const tarball = join(destination, filename)
@@ -34,13 +34,27 @@ function packMember(family: ReleaseFamily, member: ReleaseMember, destination: s
return filename
}
/**
* @returns The validated `--concurrency` value; 1 (the default) packs the
* members one at a time, exactly as the credentialed publish workflows run it.
*/
function parseConcurrency(raw: string | undefined): number {
if (raw === undefined) return 1
const parsed = Number.parseInt(raw, 10)
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
throw new Error(`--concurrency must be a positive integer, got ${JSON.stringify(raw)}`)
}
return parsed
}
/** Pack the family named by `--family` into `--out`. */
function main(): void {
async function main(): Promise<void> {
const { values } = parseArgs({
options: { family: { type: 'string' }, out: { type: 'string' } },
options: { family: { type: 'string' }, out: { type: 'string' }, concurrency: { type: 'string' } },
allowPositionals: false,
})
if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm]')
if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm] [--concurrency 1]')
const concurrency = parseConcurrency(values.concurrency)
const family = releaseFamily(values.family)
const root = process.cwd()
@@ -52,11 +66,23 @@ function main(): void {
rmSync(destination, { recursive: true, force: true })
mkdirSync(destination, { recursive: true })
const order: string[] = []
for (const member of members) order.push(packMember(family, member, destination))
// Members pack in a bounded pool; the recorded publish order stays the
// members' order regardless of completion order, because each worker writes
// its result at the member's own position.
const order = new Array<string>(members.length)
let cursor = 0
await Promise.all(Array.from({ length: Math.min(concurrency, members.length) }, async () => {
while (cursor < members.length) {
const index = cursor
cursor += 1
const member = members[index]
if (member === undefined) break
order[index] = await packMember(family, member, destination)
}
}))
writeFileSync(join(destination, PUBLISH_ORDER_FILE), `${order.join('\n')}\n`)
console.log(`release pack: family ${family.id}, ${String(order.length)} tarball(s) in ${values.out ?? DEFAULT_OUTPUT}`)
}
if (isEntry(import.meta.url)) main()
if (isEntry(import.meta.url)) await main()
+14 -7
View File
@@ -3,7 +3,7 @@
* `pnpm`, `npm`, and `tar`, and each needs one of three failure behaviours.
*/
import { spawnSync } from 'node:child_process'
import { spawn, spawnSync } from 'node:child_process'
import { realpathSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
@@ -71,16 +71,23 @@ export function capture(command: string, args: readonly string[], options: RunOp
}
/**
* Run a command with inherited streams, so its progress reaches the log, and
* fail on a non-zero exit.
* Run a command with inherited streams without blocking the event loop, so a
* caller can hold several commands in flight, and fail on a non-zero exit.
* Concurrent children interleave their output at line granularity.
* @param command - executable name.
* @param args - command arguments.
* @param options - working directory and environment.
* @returns Resolves when the command exits with status zero.
*/
export function run(command: string, args: readonly string[], options: RunOptions = {}): void {
const result = spawnSync(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' })
if (result.error !== undefined) throw result.error
if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}`)
export function runConcurrent(command: string, args: readonly string[], options: RunOptions = {}): Promise<void> {
return new Promise((resolveRun, rejectRun) => {
const child = spawn(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' })
child.once('error', rejectRun)
child.once('close', (status, signal) => {
if (status === 0) resolveRun()
else rejectRun(new Error(`${command} ${args.join(' ')} exited with ${String(status ?? signal)}`))
})
})
}
/**