refactor(dev-infra): trim gate plan surfaces

This commit is contained in:
Tianyi Cui
2026-07-27 23:58:27 +08:00
parent 3f27434f38
commit 3e6fbccffa
5 files changed
+37 -104

No files matched your search

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/process/2026-07-27-replayable-gate-plans.md
2026-07-27-replayable-gate-plans.md: a312481a4da68f0d990e28c07cced4511c922b9b
2026-07-27-replayable-gate-plans.zh.md: c213068f5413110a2c5952ac05ebc326de12682c
2026-07-27-replayable-gate-plans.md: 8a42ae3c89a3f75bb248f78583f6ef825af3241e
2026-07-27-replayable-gate-plans.zh.md: 572ef1ce131a0ced8d723e1caa822fe47556fcee
@@ -14,7 +14,7 @@ Operators also need the scheduler-owned environment and dependency context for a
[`scripts/run-gates.ts`](../../../../scripts/run-gates.ts) constructs a complete `GatePlan` before execution and validates that it is non-empty, every ID is unique and replay-safe, every dependency exists, and the graph is acyclic. `executeGatePlan()` repeats validation at the process boundary, so an invalid injected plan cannot start a child. The empty `pre-push` mode is absent; Git hooks retain their separate narrow contract.
Every mode supports deterministic `--list` output and a versioned stable `--list --json` object. Machine consumers invoke `pnpm --silent run <owning-script> -- --list --json`; `--silent` removes pnpm's outer command banner so stdout is exactly one JSON object. Both views expose canonical gate order, IDs, display commands, dependencies, blocking disposition, the plan-owned worker ceiling, and scheduler-owned environment operations. Environment overrides remain declarative until spawn, so inspection never enumerates or bakes in inherited values; values under secret-like names are redacted.
Every mode supports deterministic `--list` output and a versioned stable `--list --json` object. Machine consumers invoke `pnpm --silent run <owning-script> -- --list --json`; `--silent` removes pnpm's outer command banner so stdout is exactly one JSON object. Both views expose canonical gate order, IDs, display commands, dependencies, blocking disposition, the plan-owned worker ceiling, and scheduler-owned environment operations. Environment overrides remain declarative until spawn and support only the forms current plans use: setting a value or appending one with a space. Inspection therefore never enumerates or bakes in inherited values; values under secret-like names are redacted.
`--only <gate-id>` runs the named gate with its complete transitive dependency closure in canonical plan order. Its banner identifies the run as partial diagnostic evidence and names the complete owning package script. Every failed or skipped gate prints the cross-platform replay command `pnpm run <owning-script> -- --only <gate-id>`, which restores dependency and environment semantics through the scheduler.
@@ -14,7 +14,7 @@ Status: implemented
[`scripts/run-gates.ts`](../../../../scripts/run-gates.ts) 在执行前构造完整的 `GatePlan`,并验证计划不为空、每个 ID 唯一且可安全用于回放、每项依赖都存在、依赖图无环。`executeGatePlan()` 在进程边界再次执行验证,因此注入的无效计划无法启动子进程。空的 `pre-push` 模式不存在;Git 钩子仍遵循独立的狭窄契约。
每种模式都支持确定性的 `--list` 输出,以及带版本标识且保持稳定的 `--list --json` 对象。机器消费方使用 `pnpm --silent run <owning-script> -- --list --json``--silent` 会去除 pnpm 外层的命令横幅,使 stdout 恰好只包含一个 JSON 对象。两种视图都公开规范的门禁顺序、ID、显示命令、依赖、阻塞属性、计划掌管的工作进程上限,以及由调度器掌管的环境操作。环境覆盖在 spawn 之前保持声明式,因此检查结果不会枚举或固化继承值;名称疑似机密项的值会被脱敏。
每种模式都支持确定性的 `--list` 输出,以及带版本标识且保持稳定的 `--list --json` 对象。机器消费方使用 `pnpm --silent run <owning-script> -- --list --json``--silent` 会去除 pnpm 外层的命令横幅,使 stdout 恰好只包含一个 JSON 对象。两种视图都公开规范的门禁顺序、ID、显示命令、依赖、阻塞属性、计划掌管的工作进程上限,以及由调度器掌管的环境操作。环境覆盖在 spawn 之前保持声明式,并且只支持当前计划使用的两种形式:设置值,或以空格分隔后追加值。检查结果因此不会枚举或固化继承值;名称疑似机密项的值会被脱敏。
`--only <gate-id>` 按规范的计划顺序运行指定门禁及其完整的传递依赖闭包。启动横幅明确标记本次运行只构成局部诊断证据,并给出所属的完整包(package)脚本。每个失败或跳过的门禁都打印跨平台回放命令 `pnpm run <owning-script> -- --only <gate-id>`,该命令通过调度器还原依赖与环境语义。
+5 -32
View File
@@ -1,10 +1,4 @@
import {
mkdtempSync,
rmSync,
symlinkSync,
} from 'node:fs'
import { spawnSync } from 'node:child_process'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, describe, expect, it, vi } from 'vitest'
import {
@@ -15,7 +9,6 @@ import {
formatOnlyNotice,
gateDependencyClosure,
gatePlanForMode,
isMainModule,
listedGatePlan,
parseCliRequest,
replayCommand,
@@ -28,13 +21,9 @@ import {
type GateResult,
} from './run-gates.ts'
const temporaryRoots: string[] = []
const repositoryRoot = join(import.meta.dirname, '..')
afterEach(() => {
vi.unstubAllEnvs()
for (const root of temporaryRoots.splice(0)) rmSync(root, { recursive: true, force: true })
})
afterEach(() => vi.unstubAllEnvs())
function gate(id: string, options: Partial<Gate> = {}): Gate {
return {
@@ -64,12 +53,6 @@ function resultFor(subject: Gate, status: GateResult['status'] = 'passed'): Gate
}
}
function temporaryRoot(prefix = 'dsh-run-gates-'): string {
const root = mkdtempSync(join(tmpdir(), prefix))
temporaryRoots.push(root)
return root
}
function withPnpmEntrypoint<T>(action: () => T): T {
const previous = process.env.npm_execpath
process.env.npm_execpath = '/private/pnpm.cjs'
@@ -172,10 +155,10 @@ describe('gate plan validation', () => {
describe('gate plan inspection and replay', () => {
it('parses package-script separators, list JSON, and focused runs', () => {
expect(parseCliRequest(['check-all', '--', '--list', '--json'])).toEqual({
kind: 'run', mode: 'check-all', list: true, json: true,
mode: 'check-all', list: true, json: true,
})
expect(parseCliRequest(['check-all', '--only', 'snapshot'])).toEqual({
kind: 'run', mode: 'check-all', list: false, json: false, only: 'snapshot',
mode: 'check-all', list: false, json: false, only: 'snapshot',
})
expect(() => parseCliRequest(['check-all', '--json'])).toThrow('--json requires --list')
expect(() => parseCliRequest(['pre-push'])).toThrow('expected mode')
@@ -252,15 +235,6 @@ describe('gate plan inspection and replay', () => {
})
})
it.skipIf(process.platform === 'win32')('recognizes a symlinked script entry path', () => {
const temporary = temporaryRoot('dsh-run-gates-entry-')
const entry = join(temporary, 'run-gates.ts')
symlinkSync(join(repositoryRoot, 'scripts/run-gates.ts'), entry)
expect(isMainModule(entry)).toBe(true)
expect(isMainModule(join(temporary, 'missing.ts'))).toBe(false)
})
it('renders a cross-platform scheduler replay and labels focused evidence', () => {
const subject = plan([gate('snapshot')])
expect(replayCommand(subject, 'snapshot')).toBe('pnpm run check:all -- --only snapshot')
@@ -269,14 +243,13 @@ describe('gate plan inspection and replay', () => {
)
})
it('resolves append, set, and unset operations only when spawning', () => {
it('resolves append and set operations only when spawning', () => {
const resolved = resolveGateEnvironment(gate('subject', {
env: {
NODE_OPTIONS: { operation: 'append', value: '--max-old-space-size=8192' },
MODE: { operation: 'set', value: 'lib' },
REMOVE_ME: { operation: 'unset' },
},
}), { NODE_OPTIONS: '--trace-warnings', REMOVE_ME: 'yes', INHERITED: 'kept' })
}), { NODE_OPTIONS: '--trace-warnings', INHERITED: 'kept' })
expect(resolved).toEqual({
NODE_OPTIONS: '--trace-warnings --max-old-space-size=8192',
MODE: 'lib',
+28 -68
View File
@@ -6,38 +6,38 @@
* @see ../.agents/notes/implemented/process/2026-07-27-replayable-gate-plans.md
*/
import { spawn } from 'node:child_process'
import { realpathSync } from 'node:fs'
import { availableParallelism } from 'node:os'
import { resolve } from 'node:path'
import { performance } from 'node:perf_hooks'
import { pathToFileURL } from 'node:url'
const MODES = [
'ci-primary',
'ci-static',
'ci-lint',
'ci-coverage',
'ci-snapshot',
'ci-artifacts',
'ci-consumers',
'ci-windows-blocking',
'ci-windows-complete',
'ci-windows-observational',
'node-compat',
'check-all',
'doc-sync',
] as const
const MODE_SCRIPTS = {
'ci-primary': 'check:ci',
'ci-static': 'check:ci:static',
'ci-lint': 'check:ci:lint',
'ci-coverage': 'check:ci:coverage',
'ci-snapshot': 'check:ci:snapshot',
'ci-artifacts': 'check:ci:artifacts',
'ci-consumers': 'check:ci:consumers',
'ci-windows-blocking': 'check:ci:windows-blocking',
'ci-windows-complete': 'check:ci:windows-complete',
'ci-windows-observational': 'check:ci:windows-observational',
'node-compat': 'check:node-compat',
'check-all': 'check:all',
'doc-sync': 'doc-sync',
} as const
/** A named aggregate exposed by the gate runner. */
export type Mode = typeof MODES[number]
export type Mode = keyof typeof MODE_SCRIPTS
const MODES = Object.keys(MODE_SCRIPTS) as Mode[]
type GateStatus = 'pending' | 'running' | 'passed' | 'failed' | 'skipped'
/** One scheduler-owned environment operation, resolved against inherited values only at spawn time. */
export type GateEnvironmentOverride =
| { operation: 'set'; value: string }
| { operation: 'unset' }
| { operation: 'append'; value: string; separator?: string }
| { operation: 'append'; value: string }
/** A command and its dependency metadata inside one gate plan. */
export interface Gate {
@@ -91,18 +91,13 @@ export interface ResolvedConcurrency {
}
interface RunRequest {
kind: 'run'
mode: Mode
list: boolean
json: boolean
only?: string
}
interface ListedEnvironmentOverride {
operation: GateEnvironmentOverride['operation']
value?: string
separator?: string
}
type ListedEnvironmentOverride = GateEnvironmentOverride
interface ListedGate {
id: string
@@ -126,24 +121,11 @@ type GateExecutor = (gate: Gate) => Promise<GateResult>
type ResultObserver = (result: GateResult) => void
const root = resolve(import.meta.dirname, '..')
const MODE_SCRIPTS: Record<Mode, string> = {
'ci-primary': 'check:ci',
'ci-static': 'check:ci:static',
'ci-lint': 'check:ci:lint',
'ci-coverage': 'check:ci:coverage',
'ci-snapshot': 'check:ci:snapshot',
'ci-artifacts': 'check:ci:artifacts',
'ci-consumers': 'check:ci:consumers',
'ci-windows-blocking': 'check:ci:windows-blocking',
'ci-windows-complete': 'check:ci:windows-complete',
'ci-windows-observational': 'check:ci:windows-observational',
'node-compat': 'check:node-compat',
'check-all': 'check:all',
'doc-sync': 'doc-sync',
const entry = process.argv[1]
if (entry !== undefined && import.meta.url === pathToFileURL(resolve(entry)).href) {
process.exitCode = await main(process.argv.slice(2))
}
if (isMainModule()) process.exitCode = await main(process.argv.slice(2))
async function main(args: string[]): Promise<number> {
const request = parseCliRequest(args)
const completePlan = gatePlanForMode(request.mode)
@@ -174,21 +156,6 @@ async function main(args: string[]): Promise<number> {
: 0
}
/**
* Decide whether this module is the process entry, including through a symlinked path.
* @param entry - process entry path to compare with this module.
* @returns Whether the entry resolves to this module.
*/
export function isMainModule(entry: string | undefined = process.argv[1]): boolean {
if (entry === undefined) return false
if (import.meta.url === pathToFileURL(resolve(entry)).href) return true
try {
return import.meta.url === pathToFileURL(realpathSync(entry)).href
} catch {
return false
}
}
/**
* Parse one runner invocation without constructing or starting its plan.
* @param args - command-line arguments after the script entrypoint.
@@ -220,7 +187,7 @@ export function parseCliRequest(args: readonly string[]): RunRequest {
}
if (json && !list) throw new Error('run-gates: --json requires --list.')
if (list && only !== undefined) throw new Error('run-gates: --list and --only are mutually exclusive.')
return { kind: 'run', mode, list, json, ...only === undefined ? {} : { only } }
return { mode, list, json, ...only === undefined ? {} : { only } }
}
function parseMode(raw: string | undefined): Mode {
@@ -798,13 +765,8 @@ function listedEnvironment(
): Record<string, ListedEnvironmentOverride> {
if (environment === undefined) return {}
return Object.fromEntries(Object.entries(environment).sort(([left], [right]) => left.localeCompare(right)).map(([name, override]) => {
const value = 'value' in override
? { value: sensitiveEnvironmentName(name) ? '<redacted>' : override.value }
: {}
const separator = override.operation === 'append' && override.separator !== undefined
? { separator: override.separator }
: {}
return [name, { operation: override.operation, ...value, ...separator }]
const value = sensitiveEnvironmentName(name) ? '<redacted>' : override.value
return [name, { operation: override.operation, value }]
}))
}
@@ -874,15 +836,13 @@ export function formatOnlyNotice(plan: GatePlan, gateId: string): string {
export function resolveGateEnvironment(gate: Gate, inherited: NodeJS.ProcessEnv): NodeJS.ProcessEnv {
const resolved = { ...inherited }
for (const [name, override] of Object.entries(gate.env ?? {})) {
if (override.operation === 'unset') {
Reflect.deleteProperty(resolved, name)
} else if (override.operation === 'set') {
if (override.operation === 'set') {
resolved[name] = override.value
} else {
const current = resolved[name]
resolved[name] = current === undefined || current === ''
? override.value
: `${current}${override.separator ?? ' '}${override.value}`
: `${current} ${override.value}`
}
}
return resolved