refactor(dev-infra): trim gate plan surfaces
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user