Speed up process-host import enforcement in CI (#27124)

Reduce import-scanner work by omitting unused AST parents and the duplicate traversal. Run the mandatory process-host import check alongside static work and explicitly join it before CI can succeed.

Update both workflow phase/enforcement contracts and migrate the newer benchmark to the public subprocess package API. Keep the original package build path, manifests, runtime, test selection, and shard count. Independent native review is clean; validation and timing evidence are in the PR.
This commit is contained in:
Neil
2026-10-09 22:12:37 -07:00
committed by GitHub
parent 16e4d32978
commit 71733ccc04
6 changed files with 104 additions and 39 deletions
+14 -3
View File
@@ -203,6 +203,17 @@ jobs:
with:
bun-version-file: config/.bun-version
- name: Check process-host imports
if: '!cancelled()'
id: process-host-imports
background: true
env:
PREFLIGHT_PHASE_SELECTED: ${{ needs.code_paths.outputs.static_analysis == 'true' }}
PREFLIGHT_PRIOR_SUCCESS: ${{ job.status == 'success' }}
run: |
if [ "$PREFLIGHT_PHASE_SELECTED" != true ] || [ "$PREFLIGHT_PRIOR_SUCCESS" != true ]; then exit 0; fi
pnpm run check:process-host-imports
# Keep each check in its own log while sharing this runner.
- name: Lint
if: '!cancelled()'
@@ -268,9 +279,7 @@ jobs:
- name: Check reliability gate manifest
if: needs.code_paths.outputs.static_analysis == 'true'
run: |
pnpm run check:reliability-gates
pnpm run check:process-host-imports
run: pnpm run check:reliability-gates
- name: Enforce dead design-system classes
if: needs.code_paths.outputs.static_analysis == 'true'
@@ -443,6 +452,8 @@ jobs:
- run: pnpm run typecheck
if: needs.code_paths.outputs.typecheck == 'true'
- wait: process-host-imports
- wait: unit-plan
- uses: actions/upload-artifact@v7
@@ -4,7 +4,7 @@ import { appendFileSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from
import { availableParallelism, cpus, release, tmpdir, totalmem } from 'node:os'
import { dirname, join, relative, resolve, sep } from 'node:path'
import { performance } from 'node:perf_hooks'
import { pathToFileURL } from 'node:url'
import { fileURLToPath, pathToFileURL } from 'node:url'
const root = resolve(import.meta.dirname, '../..')
const digest = (value) => createHash('sha256').update(value).digest('hex')
@@ -134,7 +134,8 @@ function writeFixture(filePath, eventCount) {
async function runComparison() {
const { build, version: esbuildVersion } = await import('esbuild')
const { runProcessSync, describeProcessFailure } = await import('./script-child-process.mjs')
const { runProcessSync } = await import('@orca/process-host')
const { describeProcessFailure } = await import('./process-failure-message.mjs')
const { buildCounterbalancedSchedule } = await import('./counterbalanced-benchmark-schedule.mjs')
const eventCount = positiveInteger(argument('--events', '100000'), 'events')
const pairCount = positiveInteger(argument('--pairs', '2'), 'pairs')
@@ -275,7 +276,8 @@ async function runComparison() {
instrumentationSha256: Object.fromEntries(
[
import.meta.filename,
join(import.meta.dirname, 'script-child-process.mjs'),
fileURLToPath(import.meta.resolve('@orca/process-host')),
join(import.meta.dirname, 'process-failure-message.mjs'),
join(import.meta.dirname, 'counterbalanced-benchmark-schedule.mjs')
].map((path) => [relative(root, path).split(sep).join('/'), digest(readFileSync(path))])
),
@@ -218,6 +218,7 @@ it('pins every foreground and background step to its selected phase', () => {
.filter((step) => step.background)
.map((step) => [step.id, step.env.PREFLIGHT_PHASE_SELECTED])
).toEqual([
['process-host-imports', `\${{ ${staticPhase} }}`],
['root-lint', `\${{ ${staticPhase} }}`],
['native-code-quality', `\${{ ${staticPhase} }}`],
['changed-code-quality', `\${{ ${staticPhase} }}`],
@@ -11,7 +11,16 @@ describe('PR workflow parallelism', () => {
const guard = workflow.jobs.preflight.steps.find((step) =>
step.run?.includes('pnpm run check:process-host-imports')
)
expect(guard?.if).toBe("needs.code_paths.outputs.static_analysis == 'true'")
expect(guard?.if).toBe('!cancelled()')
expect(guard?.env.PREFLIGHT_PHASE_SELECTED).toBe(
"${{ needs.code_paths.outputs.static_analysis == 'true' }}"
)
expect(guard?.background).toBe(true)
const typecheckIndex = workflow.jobs.preflight.steps.findIndex(
(step) => step.run === 'pnpm run typecheck'
)
const waitIndex = workflow.jobs.preflight.steps.findIndex((step) => step.wait === guard.id)
expect(waitIndex).toBeGreaterThan(typecheckIndex)
expect(guard?.['continue-on-error']).toBeUndefined()
})
+50 -32
View File
@@ -36,46 +36,66 @@ function propertyName(node) {
return ts.isElementAccessExpression(node) ? literalString(node.argumentExpression) : null
}
function isConstDeclaration(node) {
return ts.isVariableDeclarationList(node.parent) && (node.parent.flags & ts.NodeFlags.Const) !== 0
}
function collectBindings(source) {
const strings = new Map()
const declarations = []
const destructures = []
const imports = []
function visit(node) {
if (ts.isVariableDeclaration(node) && node.initializer) {
const initializer = unwrap(node.initializer)
if (ts.isIdentifier(node.name)) {
declarations.push({ name: node.name.text, initializer })
const value = literalString(initializer)
if (isConstDeclaration(node) && value !== null) {
strings.set(node.name.text, [...(strings.get(node.name.text) ?? []), value])
const modules = []
function visit(node, isConst = false) {
switch (node.kind) {
case ts.SyntaxKind.VariableDeclarationList:
for (const declaration of node.declarations) {
visit(declaration, (node.flags & ts.NodeFlags.Const) !== 0)
}
} else if (ts.isObjectBindingPattern(node.name)) {
for (const element of node.name.elements) {
const key = element.propertyName ?? element.name
const name = ts.isIdentifier(key) ? key.text : literalString(key)
if (ts.isIdentifier(element.name) && name !== null) {
destructures.push({ name: element.name.text, key: name, initializer })
return
case ts.SyntaxKind.VariableDeclaration: {
if (!node.initializer) {
break
}
const initializer = unwrap(node.initializer)
if (ts.isIdentifier(node.name)) {
declarations.push({ name: node.name.text, initializer })
const value = literalString(initializer)
if (isConst && value !== null) {
strings.set(node.name.text, [...(strings.get(node.name.text) ?? []), value])
}
} else if (ts.isObjectBindingPattern(node.name)) {
for (const element of node.name.elements) {
const key = element.propertyName ?? element.name
const name = ts.isIdentifier(key) ? key.text : literalString(key)
if (ts.isIdentifier(element.name) && name !== null) {
destructures.push({ name: element.name.text, key: name, initializer })
}
}
}
break
}
} else if (ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier)) {
imports.push({ module: node.moduleSpecifier.text, clause: node.importClause })
} else if (
ts.isImportEqualsDeclaration(node) &&
ts.isExternalModuleReference(node.moduleReference) &&
ts.isStringLiteral(node.moduleReference.expression)
) {
imports.push({ module: node.moduleReference.expression.text, namespace: node.name })
case ts.SyntaxKind.ImportDeclaration:
if (ts.isStringLiteral(node.moduleSpecifier)) {
imports.push({ module: node.moduleSpecifier.text, clause: node.importClause })
}
modules.push(node)
break
case ts.SyntaxKind.ImportEqualsDeclaration:
if (
ts.isExternalModuleReference(node.moduleReference) &&
ts.isStringLiteral(node.moduleReference.expression)
) {
imports.push({ module: node.moduleReference.expression.text, namespace: node.name })
}
modules.push(node)
break
case ts.SyntaxKind.ExportDeclaration:
case ts.SyntaxKind.ImportType:
case ts.SyntaxKind.CallExpression:
modules.push(node)
break
}
ts.forEachChild(node, visit)
}
visit(source)
return { strings, declarations, destructures, imports }
return { strings, declarations, destructures, imports, modules }
}
/**
@@ -88,11 +108,11 @@ function collectBindings(source) {
* are not resolved.
*/
export function collectModuleSpecifiers(file, contents) {
const source = ts.createSourceFile(file, contents, ts.ScriptTarget.Latest, true)
const source = ts.createSourceFile(file, contents, ts.ScriptTarget.Latest, false)
if (source.parseDiagnostics.length > 0) {
throw new Error(`Cannot parse ${file}; the process-host import check must not skip it.`)
}
const { strings, declarations, destructures, imports } = collectBindings(source)
const { strings, declarations, destructures, imports, modules } = collectBindings(source)
const loaders = new Set(['require'])
const createRequires = new Set()
const moduleNamespaces = new Set()
@@ -212,7 +232,7 @@ export function collectModuleSpecifiers(file, contents) {
specifiers.add(specifier)
}
}
function visit(node) {
for (const node of modules) {
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) {
add(node.moduleSpecifier)
} else if (
@@ -233,8 +253,6 @@ export function collectModuleSpecifiers(file, contents) {
add(node.arguments[0])
}
}
ts.forEachChild(node, visit)
}
visit(source)
return [...specifiers]
}
@@ -118,6 +118,30 @@ describe('static module specifiers', () => {
).toHaveLength(1)
})
it('resolves escaped loader names and literals after parsing', () => {
expect(
specifiers(String.raw`
const load = \u0072equire
const CHILD_PROCESS = '\u0063hild_process'
load(CHILD_PROCESS)
process['get\u0042uiltinModule']('node:\u0063hild_process')
`)
).toEqual(['child_process', 'node:child_process'])
})
it('preserves const binding order across nested declaration lists', () => {
expect(
specifiers(`
const nested = (() => { const NAME = 'first'; return NAME })(), NAME = 'second'
require(NAME)
`)
).toEqual(['first', 'second'])
})
it('rejects invalid syntax even without imports or loader names', () => {
expect(() => specifiers('const broken = (')).toThrow('Cannot parse file.ts')
})
it('ignores unrelated objects, computed property values, and non-constant specifiers', () => {
expect(
specifiers(