fix(ci): make accelerated checks runtime-safe

This commit is contained in:
Neil
2026-07-27 12:47:50 -07:00
parent 40b8e5e4f4
commit d221fa8368
7 changed files with 129 additions and 58 deletions
@@ -2,10 +2,10 @@ name: Install Node dependencies
description: Installs the Node toolchain and repository dependencies for Linux CI jobs.
inputs:
ignore-scripts:
description: Skip dependency lifecycle scripts when the job does not need native runtimes.
native-runtime:
description: Native runtime to prepare after the script-free install (none, node, or electron).
required: false
default: 'false'
default: none
runs:
using: composite
@@ -22,9 +22,22 @@ runs:
node-version-file: package.json
cache: pnpm
- name: Validate native runtime
shell: bash
env:
NATIVE_RUNTIME: ${{ inputs.native-runtime }}
run: |
case "$NATIVE_RUNTIME" in
none|node|electron) ;;
*)
echo "::error::native-runtime must be none, node, or electron"
exit 2
;;
esac
# pnpm's bundled gyp_main.py is not executable on fresh Linux runners.
- name: Use external node-gyp
if: inputs.ignore-scripts != 'true'
if: inputs.native-runtime != 'none'
shell: bash
run: |
npm install -g node-gyp@11.5.0
@@ -40,15 +53,19 @@ runs:
- name: Install dependencies
shell: bash
env:
IGNORE_SCRIPTS: ${{ inputs.ignore-scripts }}
run: |
install_args=(--no-frozen-lockfile --prefer-frozen-lockfile=false --os=current --os=win32 --cpu=current)
if [ "$IGNORE_SCRIPTS" = "true" ]; then
install_args+=(--ignore-scripts)
elif [ "$IGNORE_SCRIPTS" != "false" ]; then
echo "::error::ignore-scripts must be true or false"
exit 2
fi
pnpm install "${install_args[@]}"
pnpm install \
--no-frozen-lockfile \
--prefer-frozen-lockfile=false \
--os=current \
--os=win32 \
--cpu=current \
--ignore-scripts
git diff --exit-code package.json pnpm-lock.yaml
- name: Prepare native runtime
if: inputs.native-runtime != 'none'
shell: bash
env:
NATIVE_RUNTIME: ${{ inputs.native-runtime }}
run: node config/scripts/ensure-native-runtime.mjs --runtime="$NATIVE_RUNTIME"
+8 -16
View File
@@ -24,8 +24,6 @@ jobs:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
ignore-scripts: 'true'
- name: Lint
run: pnpm exec oxlint --format github
@@ -73,8 +71,6 @@ jobs:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
ignore-scripts: 'true'
- run: pnpm typecheck
@@ -89,8 +85,6 @@ jobs:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
ignore-scripts: 'true'
- name: Verify Git binary compatibility matrix
run: |
@@ -144,10 +138,11 @@ jobs:
run: sudo apt-get update && sudo apt-get install -y zsh
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: node
- name: Test real shell contracts
run: |
node config/scripts/ensure-native-runtime.mjs --runtime=node
pnpm exec vitest run --config config/vitest.config.ts \
src/main/daemon/shell-ready.test.ts \
src/main/daemon/node-pty-fd-leak.test.ts \
@@ -172,7 +167,7 @@ jobs:
- uses: ./.github/actions/install-node-dependencies
with:
ignore-scripts: 'true'
native-runtime: node
- name: Install Electron package binary for tests
run: node config/scripts/install-electron-package-binary.mjs
@@ -198,10 +193,6 @@ jobs:
with:
persist-credentials: false
- uses: ./.github/actions/install-node-dependencies
with:
ignore-scripts: 'true'
- name: Cache electron-builder downloads
uses: actions/cache@v5
with:
@@ -212,16 +203,17 @@ jobs:
restore-keys: |
electron-builder-linux-
# Native preparation does not touch the bundle output directories.
- uses: ./.github/actions/install-node-dependencies
with:
native-runtime: electron
- name: Build package inputs
run: |
pnpm run ensure:electron-runtime &
pids=("$!")
status=0
pnpm run build:cli || status=1
scripts=(build:relay build:electron-vite:parallel)
pids=()
for script in "${scripts[@]}"; do
pnpm run "$script" &
pids+=("$!")
@@ -26,4 +26,10 @@ describe('Electron Vite output contract', () => {
expect(external('@xterm/headless', undefined, false)).toBe(false)
expect(external('@xterm/addon-serialize', undefined, false)).toBe(false)
})
it('isolates renderer entry side effects behind strict facades', () => {
expect(electronViteConfig.renderer?.build?.rollupOptions?.preserveEntrySignatures).toBe(
'strict'
)
})
})
+34 -28
View File
@@ -49,15 +49,18 @@ describe('PR workflow parallelism', () => {
const shellStep = workflow.jobs.shell_contracts.steps.find(
(step) => step.name === 'Test real shell contracts'
)
const shellInstall = workflow.jobs.shell_contracts.steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(workflow.jobs.test.steps.some((step) => step.name === 'Install zsh')).toBe(false)
expect(workflow.jobs.shell_contracts.steps.some((step) => step.name === 'Install zsh')).toBe(
true
)
expect(shellInstall.with['native-runtime']).toBe('node')
for (const testFile of nativeShellContractFiles) {
expect(shellStep.run).toContain(testFile)
}
expect(shellStep.run).toContain('ensure-native-runtime.mjs --runtime=node')
})
it('keeps every real-zsh test in the dedicated shell lane', () => {
@@ -74,7 +77,6 @@ describe('PR workflow parallelism', () => {
)
expect(buildStep.run).toContain('scripts=(build:relay build:electron-vite:parallel)')
expect(buildStep.run).toContain('pnpm run ensure:electron-runtime &')
expect(buildStep.run).toContain('pnpm run "$script" &')
expect(
workflow.jobs.package.steps.find(
@@ -94,41 +96,45 @@ describe('PR workflow parallelism', () => {
expect(steps[nodeIndex].with.cache).toBe('pnpm')
})
it('skips lifecycle scripts outside native-runtime test setup', () => {
for (const jobName of [
'static_analysis',
'typecheck',
'git_compatibility',
'test',
'package'
]) {
const installStep = workflow.jobs[jobName].steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(installStep.with['ignore-scripts'], jobName).toBe('true')
}
const shellInstall = workflow.jobs.shell_contracts.steps.find(
it('restores Electron downloads before preparing the package runtime', () => {
const steps = workflow.jobs.package.steps
const cacheIndex = steps.findIndex((step) => step.name === 'Cache electron-builder downloads')
const installIndex = steps.findIndex(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
expect(shellInstall.with).toBeUndefined()
expect(cacheIndex).toBeLessThan(installIndex)
})
it('prepares each native runtime before its consumers start', () => {
const installFor = (jobName) =>
workflow.jobs[jobName].steps.find(
(step) => step.uses === './.github/actions/install-node-dependencies'
)
for (const jobName of ['static_analysis', 'typecheck', 'git_compatibility']) {
expect(installFor(jobName).with, jobName).toBeUndefined()
}
expect(installFor('shell_contracts').with['native-runtime']).toBe('node')
expect(installFor('test').with['native-runtime']).toBe('node')
expect(installFor('package').with['native-runtime']).toBe('electron')
expect(
dependencyAction.runs.steps.find((step) => step.name === 'Use external node-gyp').if
).toBe("inputs.ignore-scripts != 'true'")
).toBe("inputs.native-runtime != 'none'")
const dependencyInstall = dependencyAction.runs.steps.find(
(step) => step.name === 'Install dependencies'
)
expect(dependencyInstall.env.IGNORE_SCRIPTS).toBe('${{ inputs.ignore-scripts }}')
expect(dependencyInstall.run).toContain(
'--no-frozen-lockfile --prefer-frozen-lockfile=false --os=current --os=win32 --cpu=current'
)
expect(dependencyInstall.run).toContain('install_args+=(--ignore-scripts)')
expect(workflow.jobs.test.steps.find((step) => step.name === 'Test shard').run).not.toContain(
'ensure-native-runtime'
expect(dependencyInstall.run).toContain('--no-frozen-lockfile')
expect(dependencyInstall.run).toContain('--ignore-scripts')
const prepareRuntime = dependencyAction.runs.steps.find(
(step) => step.name === 'Prepare native runtime'
)
expect(prepareRuntime.if).toBe("inputs.native-runtime != 'none'")
expect(prepareRuntime.run).toContain('ensure-native-runtime.mjs --runtime="$NATIVE_RUNTIME"')
})
it('reuses native preparation only after the concurrent runtime gate', () => {
it('reuses native preparation after the dependency action gate', () => {
const buildStep = workflow.jobs.package.steps.find(
(step) => step.name === 'Build package inputs'
)
@@ -136,7 +142,7 @@ describe('PR workflow parallelism', () => {
(step) => step.name === 'Package unpacked app'
)
expect(buildStep.run).toContain('pnpm run ensure:electron-runtime &')
expect(buildStep.run).not.toContain('ensure:electron-runtime')
expect(packageStep.env.ORCA_REUSE_PREPARED_NATIVE_RUNTIME).toBe('1')
})
@@ -19,6 +19,36 @@ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
const selectedFiles = new Set(['web-index.html'])
const visitedEntries = new Set()
function assertEntryIsolation() {
const entryKeys = new Set(
Object.entries(manifest)
.filter(([, entry]) => entry?.isEntry === true)
.map(([key]) => key)
)
for (const sourceEntry of entryKeys) {
const visited = new Set()
const pending = [sourceEntry]
while (pending.length > 0) {
const key = pending.pop()
if (visited.has(key)) {
continue
}
visited.add(key)
const entry = manifest[key]
if (!entry || typeof entry !== 'object') {
throw new Error(`Renderer manifest is missing entry: ${key}`)
}
for (const dependency of [...(entry.imports ?? []), ...(entry.dynamicImports ?? [])]) {
if (entryKeys.has(dependency) && dependency !== sourceEntry) {
throw new Error(`Renderer entry ${sourceEntry} executes entry ${dependency}`)
}
pending.push(dependency)
}
}
}
}
function addOutputPath(outputPath) {
if (
typeof outputPath !== 'string' ||
@@ -107,6 +137,7 @@ async function minifyWebOutput() {
)
}
assertEntryIsolation()
visitManifestEntry('web-index.html')
includeReferencedOutputs()
@@ -98,4 +98,20 @@ describe('renderer web client projection', () => {
expect(result.status).toBe(1)
expect(result.stderr).toContain('Renderer manifest is missing entry: web-index.html')
})
it('rejects renderer entries that execute another entry root', () => {
const root = createRendererFixture()
const manifestPath = join(root, 'out/renderer/.vite/manifest.json')
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'))
manifest['web-index.html'].dynamicImports.push('index.html')
writeFileSync(manifestPath, JSON.stringify(manifest))
const result = spawnSync(process.execPath, [scriptPath], {
cwd: root,
encoding: 'utf8'
})
expect(result.status).toBe(1)
expect(result.stderr).toContain('Renderer entry web-index.html executes entry index.html')
})
})
+3
View File
@@ -277,6 +277,9 @@ export const electronViteConfig: UserConfig = {
// stay listed — overriding input otherwise drops electron-vite's default
// renderer entry.
rollupOptions: {
// Why: shared chunks must never import an HTML entry whose module mounts
// a different React root.
preserveEntrySignatures: 'strict',
input: {
index: resolve('src/renderer/index.html'),
popout: resolve('src/renderer/popout.html'),