ci(test): download the Electron binary once before unit specs run

pnpm skips Electron's postinstall (electron is deliberately not in
onlyBuiltDependencies), so the binary is downloaded lazily on the first
require('electron'). Since Tier A projects run concurrently (#1701),
several electron-backend and database specs resolve and exec it at the
same time; one process execs it while another is still extracting it
and fails with `spawnSync .../electron ETXTBSY`.

tools/testing/ensure-electron-binary.mjs downloads the binary once and
fails unless it runs under ELECTRON_RUN_AS_NODE and reports the pinned
version. The unit job runs it right after pnpm install, coverage:unit:ci
runs it first for local runs, and the performance journeys job runs it
before its ldd check, which passed vacuously while no binary existed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Opus 5.5 committed 2026-09-27 21:47:03 +02:00
1 parent 887ac64d18
commit dd28911ee4
4 files changed
+87 -1

No files matched your search

+12
View File
@@ -285,9 +285,12 @@ jobs:
# and never launch a Playwright browser, so no `playwright
# install`. The runner image ships Electron's shared libraries and
# xvfb; fail fast with a clear message if an image update drops one.
# pnpm skips Electron's postinstall, so download the binary first;
# otherwise ldd sees no file and the check passes vacuously.
- name: Check Electron runtime dependencies
run: |
command -v xvfb-run || { echo "::error::xvfb-run is missing on the runner"; exit 1; }
node tools/testing/ensure-electron-binary.mjs || { echo "::error::Electron binary download failed"; exit 1; }
missing="$(ldd node_modules/electron/dist/electron | grep 'not found' || true)"
if [ -n "$missing" ]; then
echo "::error::Electron is missing shared libraries:"
@@ -368,6 +371,15 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
# pnpm skips Electron's postinstall (it is not in
# onlyBuiltDependencies), so the binary is fetched on the first
# require('electron'). Specs that run SQLite under
# ELECTRON_RUN_AS_NODE resolve it that way while Tier A projects
# run concurrently, and one exec'ing it mid-extraction fails with
# ETXTBSY. Download and verify it once, before any test starts.
- name: Download Electron binary
run: node tools/testing/ensure-electron-binary.mjs
- name: Validate agent guidance
run: pnpm run agents:validate
+12
View File
@@ -107,6 +107,18 @@ with `diagnostics: false`); `isolatedModules`-incompatible syntax such as a
type re-export without `export type` still fails at load time, and spec type
errors are caught by `typecheck:spec` (see Unit And Type Checks).
Some `electron-backend` and `database` specs run SQLite code in the Electron
binary under `ELECTRON_RUN_AS_NODE`, resolving it with `require('electron')`.
pnpm does not run Electron's postinstall (`electron` is deliberately absent from
`onlyBuiltDependencies`, see [workspace shell](workspace-shell.md)), so the
first `require` downloads and extracts the binary. With projects and Jest
workers in parallel, one process can exec it while another is still extracting
it (`spawnSync … ETXTBSY` on Linux). `coverage:unit:ci` and a CI step right
after `pnpm install` therefore run `tools/testing/ensure-electron-binary.mjs`
first, which downloads the binary once and fails unless it runs and reports
the pinned version. In a fresh worktree, run it before starting several of
these specs at once by other means.
In CI, a pull request skips the Tier A suite (and the merged-coverage upload)
when every changed file is outside Tier A test inputs:
`tools/coverage/unit-coverage-scope.mjs` holds the allowlist (Markdown, `docs/`,
+1 -1
View File
@@ -45,7 +45,7 @@
"styles:inputs:check": "node tools/nx/check-stylesheet-inputs.mjs",
"styles:inputs:validate": "pnpm run styles:inputs:test && pnpm run styles:inputs:check",
"coverage:tools:test": "node --test tools/coverage/coverage-integrity.test.mjs tools/coverage/e2e-shard-reports.test.mjs tools/coverage/coverage-run-pool.test.mjs tools/coverage/unit-coverage-scope.test.mjs",
"coverage:unit:ci": "node tools/coverage/run-tier-a-coverage.mjs",
"coverage:unit:ci": "node tools/testing/ensure-electron-binary.mjs && node tools/coverage/run-tier-a-coverage.mjs",
"coverage:merge": "node tools/coverage/merge-coverage.mjs",
"coverage:health": "node tools/coverage/coverage-health.mjs",
"coverage:policy:check": "node tools/coverage/check-coverage-policy.mjs",
+62
View File
@@ -0,0 +1,62 @@
#!/usr/bin/env node
// Downloads the Electron binary once and proves it runs, before anything
// spawns it concurrently.
//
// pnpm does not run Electron's postinstall (it is not in
// `onlyBuiltDependencies`), so `pnpm install` leaves no binary behind and
// `require('electron')` downloads and extracts it on first use. Unit specs
// that execute SQLite code under `ELECTRON_RUN_AS_NODE` resolve it that way,
// and Tier A projects and Jest workers run in parallel: one process can exec
// the binary while another is still extracting it (ETXTBSY on Linux, clobbered
// framework symlinks on macOS). Running this first leaves nothing to download.
//
// Usage: node tools/testing/ensure-electron-binary.mjs
import { execFileSync } from 'node:child_process';
import { createRequire } from 'node:module';
import process from 'node:process';
const require = createRequire(import.meta.url);
const { version } = require('electron/package.json');
let electronPath;
try {
electronPath = require('electron');
} catch (error) {
console.error(
`Electron ${version} binary download failed: ${error.message}`
);
process.exit(1);
}
let reported;
try {
reported = execFileSync(
electronPath,
['-e', 'process.stdout.write(process.versions.electron)'],
{
encoding: 'utf8',
// The child's stderr (e.g. a dyld or loader error) goes straight
// to the log instead of being repeated in the message below.
stdio: ['ignore', 'pipe', 'inherit'],
env: { ...process.env, ELECTRON_RUN_AS_NODE: '1' },
timeout: 60_000,
}
).trim();
} catch (error) {
console.error(
`Electron binary at ${electronPath} does not run (${error.code ?? `exit ${error.status}`}). ` +
'If an interrupted download left a partial copy, delete node_modules/electron/dist and path.txt, then rerun.'
);
process.exit(1);
}
if (reported !== version) {
console.error(
`Electron binary at ${electronPath} reports ${reported || 'no version'}, expected ${version}.`
);
process.exit(1);
}
console.log(`Electron ${version} binary ready at ${electronPath}`);