fix(ci): bound CLI help children inside aggregate gates

This commit is contained in:
liukx0205
2026-10-30 13:47:01 +08:00
parent 7fc5c2ecb2
commit c0b48f8961
7 changed files with 65 additions and 10 deletions
+2 -2
View File
@@ -27,8 +27,8 @@
en: dba2c72ee6ffa930
zh: b65c27fbb9129ec8
/development-guide/contributor-reference/git-integrations:
en: 2efb7c4e482413fc
zh: 75386476a283111a
en: 075658ecfc10da20
zh: 69e664417bbde87e
/development-guide/contributor-reference/ci-gates:
en: dfa3a24d9070478f
zh: fa889b3536ab58e2
+2
View File
@@ -155,6 +155,8 @@ Contributors can opt into the comprehensive local gate set with `pnpm run check:
Outside CI (`CI` other than `true`), default worker counts follow the [local worker budget](../scripts/parallelism.ts): 12 for gate schedulers, `build`, `typecheck`, Oxlint, and publint, and 48 for Vitest forks and local coverage. `doc-sync`, `test:docs`, `hygiene`, and `check:all` run at most 8 gates at once, in CI as well. `os.availableParallelism()` reports host CPUs rather than a container's CPU quota, so uncapped defaults oversubscribe shared hosts. CI keeps the runner's CPU count. `DSH_GATE_CONCURRENCY`, `DSH_OXLINT_THREADS`, `DSH_COVERAGE_MAX_WORKERS`, and Vitest `--maxWorkers` override the defaults.
`run-gates` gives the CLI help generator one child at a time; direct `pnpm run verify-cli-help` uses the standalone worker budget. `DSH_CLI_HELP_CONCURRENCY` overrides that child limit with a positive integer validated before startup. Each invocation retains its private `DSH_HOME` and `DSH_AGENTS_HOME` and 30-second deadline.
### CI gates
The keyless [CI workflow](../.github/workflows/ci.yml) groups independent gates into broad lanes and runs a smaller compatibility signal across supported Node versions. Artifact consumers wait for one build within their lane. Coverage partitions reuse file timings scoped to their platform and runner pool to balance work within the configured worker limit. Native Windows lanes run in the separate [CI Windows workflow](../.github/workflows/ci-windows.yml) and Python checks in the separate [CI Python workflow](../.github/workflows/ci-python.yml), each of which can be re-run independently. Required benchmarks run separately on standard GitHub-hosted Linux; the [benchmark runner reference](../benchmarks/AGENTS.md) owns routing and the job timeout. The separate real-API workflow runs `pnpm run test:e2e` with its configured worker bound. See [scripts/run-gates.ts](../scripts/run-gates.ts) and the workflow files for the current gate and job inventory.
+2
View File
@@ -160,6 +160,8 @@ pi-ai 检查会在内存中重新生成预期元数据。数据过期时,检
在 CI 之外(`CI` 不为 `true`),默认 worker 数遵循[本地 worker 预算](../scripts/parallelism.ts):门禁调度器、`build`、`typecheck`、Oxlint 与 publint 为 12,Vitest fork 与本地覆盖率为 48。`doc-sync`、`test:docs`、`hygiene` 与 `check:all` 最多同时运行 8 个门禁,CI 中同样如此。`os.availableParallelism()` 报告的是宿主 CPU 数而不是容器的 CPU 配额,因此不设上限的默认值会在共享主机上超额订阅。CI 仍使用 runner 的 CPU 数。`DSH_GATE_CONCURRENCY`、`DSH_OXLINT_THREADS`、`DSH_COVERAGE_MAX_WORKERS` 与 Vitest 的 `--maxWorkers` 可覆盖这些默认值。
`run-gates` 让 CLI 帮助生成器每次只运行一个子进程;直接执行 `pnpm run verify-cli-help` 时使用独立运行的 worker 预算。`DSH_CLI_HELP_CONCURRENCY` 可用正整数覆盖该子进程上限,并在启动前校验。每次调用仍使用独立的 `DSH_HOME` 与 `DSH_AGENTS_HOME`,超时限制仍为 30 秒。
### CI 门禁
keyless [CI 工作流](../.github/workflows/ci.yml) 将独立门禁分组到若干宽粒度 lane,并在受支持的 Node 版本上运行一组较小的兼容性检查。产物消费方在各自 lane 内等待一次 build。覆盖率分区复用按平台和运行器池隔离的文件耗时,在配置的 worker 上限内均衡工作量。原生 Windows lane 运行在独立的 [CI Windows 工作流](../.github/workflows/ci-windows.yml)中,Python 检查运行在独立的 [CI Python 工作流](../.github/workflows/ci-python.yml)中,两者都可单独重新运行。必需 benchmark 在标准 GitHub 托管 Linux 上独立运行;[benchmark 运行器说明](../benchmarks/AGENTS.md)拥有路由及 job 超时。单独的真实 API 工作流按其配置的 worker 上限运行 `pnpm run test:e2e`。当前门禁和 job 清单以 [scripts/run-gates.ts](../scripts/run-gates.ts) 和工作流文件为准。
+23 -2
View File
@@ -1,7 +1,11 @@
/** Command discovery and complete output preservation for the CLI reference. */
import { describe, expect, it } from 'vitest'
import { collectCliHelp, parseCliHelp, prefetchHelp, topLevelHelpArgs } from './gen-cli-help.ts'
import { execFile } from 'node:child_process'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { cliHelpConcurrency, collectCliHelp, main, parseCliHelp, prefetchHelp, topLevelHelpArgs } from './gen-cli-help.ts'
vi.mock('node:child_process', () => ({ execFile: vi.fn() }))
afterEach(() => { vi.unstubAllEnvs(); vi.clearAllMocks() })
interface PendingRead {
args: string
@@ -26,6 +30,23 @@ async function flush(): Promise<void> {
await new Promise<void>((resolve) => { setImmediate(resolve) })
}
describe('CLI help child budget', () => {
it.each([undefined, ''])('keeps the independent worker budget for %j', (raw) => {
expect(cliHelpConcurrency(raw, 12)).toBe(12)
})
it('uses an explicit caller allowance', () => {
expect(cliHelpConcurrency('1', 12)).toBe(1)
expect(cliHelpConcurrency('3', 12)).toBe(3)
})
it.each(['0', '-1', '1.5', '2workers', '02', '9007199254740992'])('rejects %j before spawning help', async (raw) => {
vi.stubEnv('DSH_CLI_HELP_CONCURRENCY', raw)
await expect(main()).rejects.toThrow('DSH_CLI_HELP_CONCURRENCY must be a positive integer')
expect(execFile).not.toHaveBeenCalled()
})
})
describe('CLI help discovery', () => {
it('reads command rows while retaining wrapped descriptions and examples', () => {
const help = 'Usage: dsh\r\n\r\nCommands:\r\n web [options] Serve the browser\r\n with a wrapped description\r\n plugin Manage plugins\r\n\r\nExamples:\r\n dsh web\r\n'
+19 -2
View File
@@ -66,6 +66,22 @@ export function topLevelHelpArgs(profiles: readonly string[]): string[][] {
return [[], ['plugin'], ...[...profiles].sort().flatMap(profile => [[profile], ['--profile', profile]])]
}
/**
* Resolve the child-process allowance before any help invocation starts.
* @param raw - optional DSH_CLI_HELP_CONCURRENCY override.
* @param fallback - worker budget for an invocation without an explicit allowance.
* @returns the maximum number of help children in flight.
* @throws when a supplied override is not a positive decimal integer.
*/
export function cliHelpConcurrency(raw: string | undefined, fallback: number): number {
if (raw === undefined || raw === '') return fallback
const parsed = Number.parseInt(raw, 10)
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
throw new Error(`gen-cli-help: DSH_CLI_HELP_CONCURRENCY must be a positive integer, got ${JSON.stringify(raw)}.`)
}
return parsed
}
/** Help reads started ahead of an in-order consumer. */
export interface PrefetchedHelpReader {
/**
@@ -177,9 +193,10 @@ export async function collectCliHelp(
}
async function collectHelp(): Promise<HelpPage[]> {
const concurrency = cliHelpConcurrency(process.env.DSH_CLI_HELP_CONCURRENCY, defaultParallelism())
const directory = mkdtempSync(join(tmpdir(), 'dsh-cli-help-'))
const profiles = Object.keys(PROFILE_TEMPLATES)
// Concurrent profile boots share the repository worker budget.
// Aggregate callers allocate this leaf's children separately from the other running gates.
const reader = prefetchHelp(async (args, signal) => {
// Each invocation boots against its own empty Harness and agents homes.
const home = mkdtempSync(join(directory, 'invocation-'))
@@ -207,7 +224,7 @@ async function collectHelp(): Promise<HelpPage[]> {
} finally {
signal.removeEventListener('abort', abort)
}
}, topLevelHelpArgs(profiles), defaultParallelism())
}, topLevelHelpArgs(profiles), concurrency)
try {
return await collectCliHelp(reader.readHelp, profiles)
} finally {
+12 -3
View File
@@ -23,6 +23,7 @@ beforeEach(() => {
vi.stubEnv('DSH_WEB_SNAPSHOT_WORKERS', undefined)
vi.stubEnv('DSH_COVERAGE_MAX_WORKERS', undefined)
vi.stubEnv('DSH_COVERAGE_PARTITIONS', undefined)
vi.stubEnv('DSH_CLI_HELP_CONCURRENCY', undefined)
})
afterEach(() => vi.unstubAllEnvs())
@@ -305,13 +306,21 @@ describe('gate graph validation', () => {
expect(ids).toContain('public-repository-links')
})
it('checks the complete CLI reference locally and in the static CI lane', () => {
for (const mode of ['doc-sync', 'ci-static'] as const) {
it.each([undefined, ''])('allocates one CLI help child inside each aggregate when its allowance is %j', (raw) => {
vi.stubEnv('DSH_CLI_HELP_CONCURRENCY', raw)
for (const mode of ['doc-sync', 'ci-static', 'ci-windows-observational-ready'] as const) {
const gates = withPnpmEntrypoint(() => gatesForMode(mode))
expect(gates.find(gate => gate.id === 'cli-help')).toBeDefined()
expect(gates.find(gate => gate.id === 'cli-help')).toMatchObject({ env: { DSH_CLI_HELP_CONCURRENCY: '1' } })
expect(gates.filter(gate => gate.id !== 'cli-help').every(gate => gate.env?.DSH_CLI_HELP_CONCURRENCY === undefined)).toBe(true)
}
})
it.each(['3', 'invalid'])('passes an explicit CLI help allowance %j to its owner', (raw) => {
vi.stubEnv('DSH_CLI_HELP_CONCURRENCY', raw)
const gates = withPnpmEntrypoint(() => gatesForMode('ci-windows-observational-ready'))
expect(gates.find(gate => gate.id === 'cli-help')).toMatchObject({ env: { DSH_CLI_HELP_CONCURRENCY: raw } })
})
it('keeps the concrete terminology policy in the documentation gate', () => {
const ids = withPnpmEntrypoint(() => gatesForMode('doc-sync').map(subject => subject.id))
+5 -1
View File
@@ -833,7 +833,11 @@ function docSyncLeafGates(options: {
: [pnpmScript('doc-typecheck', options.docTypecheckScript ?? 'doc-typecheck', docTypecheckOptions)],
pnpmScript('docs-site-build', options.docsBuildScript ?? 'docs:build', { label: 'documentation build' }),
pnpmScript('doc-graphs', 'verify-doc-graphs', { label: 'doc graphs' }),
pnpmScript('cli-help', 'verify-cli-help', { label: 'CLI help reference' }),
pnpmScript('cli-help', 'verify-cli-help', {
label: 'CLI help reference',
// Other gate workers already run beside this leaf; direct help generation keeps its own worker budget.
env: { DSH_CLI_HELP_CONCURRENCY: process.env.DSH_CLI_HELP_CONCURRENCY || '1' },
}),
pnpmScript('markdown-links', 'verify-md-links', { label: 'markdown links', quick: true }),
pnpmScript('type-equivalence', 'verify-type-equiv', { label: 'type equivalence', quick: true }),
pnpmScript('cordis-catalog', 'verify-cordis-catalog', { label: 'cordis catalog' }),