Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.

Commit e61c920

Browse files
committed
FEA-1543: Factor Electron binary CI verification
Move the Electron binary repair guard into a reusable desktop script, then call it from the CI test and release workflows that run desktop tests or package/release under Node 24. Document the reusable workflow rule in AGENTS.md so future changes do not reintroduce inline Electron download logic or apply the guard to static/headless audit jobs. Validation: pnpm -C apps/desktop verify:electron-binary repair path passed; pnpm -C apps/desktop verify:electron-binary no-op path passed; pnpm install --frozen-lockfile passed; pnpm -r prebuild passed; pnpm -C apps/desktop lint passed; pnpm typecheck passed; pnpm -C apps/desktop assert:design-system-boundary passed; pnpm -C apps/desktop test:boot:design-system-off passed; pnpm -C apps/desktop measure:agent-dashboard-storage passed; pnpm test passed.
1 parent 9f9c5f2 commit e61c920

5 files changed

Lines changed: 136 additions & 35 deletions

File tree

.github/workflows/release.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ jobs:
5858
env:
5959
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6060

61+
- name: Verify Electron binary install
62+
if: steps.check_release.outputs.skip != 'true'
63+
run: pnpm -C apps/desktop verify:electron-binary
64+
6165
- name: Generate build info
6266
if: steps.check_release.outputs.skip != 'true'
6367
run: pnpm -r prebuild

.github/workflows/test.yml

Lines changed: 3 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -36,42 +36,10 @@ jobs:
3636
env:
3737
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
3838

39+
# Node 24 Ubuntu runners have installed the electron package without a
40+
# usable platform binary/path; verify first and repair only if needed.
3941
- name: Verify Electron binary install
40-
working-directory: apps/desktop
41-
run: |
42-
unset ELECTRON_SKIP_BINARY_DOWNLOAD
43-
electron_dir="$(node -p 'const path = require("node:path"); path.dirname(require.resolve("electron/package.json"))')"
44-
version="$(node -p 'require("electron/package.json").version')"
45-
platform="$(node -p 'process.env.npm_config_platform || process.platform')"
46-
arch="$(node -p 'process.env.npm_config_arch || process.arch')"
47-
platform_path="$(node -p 'const paths = { darwin: "Electron.app/Contents/MacOS/Electron", freebsd: "electron", linux: "electron", mas: "Electron.app/Contents/MacOS/Electron", openbsd: "electron", win32: "electron.exe" }; const value = paths[process.argv[1]]; if (!value) throw new Error(`Electron builds are not available on platform: ${process.argv[1]}`); value' "$platform")"
48-
artifact="electron-v$version-$platform-$arch.zip"
49-
checksum="$(node -p 'const path = require("node:path"); const { createRequire } = require("node:module"); const electronDir = path.dirname(require.resolve("electron/package.json")); const electronRequire = createRequire(path.join(electronDir, "install.js")); const value = electronRequire("./checksums.json")[process.argv[1]]; if (!value) throw new Error(`Missing Electron checksum for ${process.argv[1]}`); value' "$artifact")"
50-
51-
download_dir="${RUNNER_TEMP:-$(mktemp -d)}"
52-
zip_path="$download_dir/$artifact"
53-
curl --fail --location --retry 3 --output "$zip_path" \
54-
"https://github.com/electron/electron/releases/download/v$version/$artifact"
55-
if command -v sha256sum >/dev/null 2>&1; then
56-
actual_checksum="$(sha256sum "$zip_path" | awk '{print $1}')"
57-
else
58-
actual_checksum="$(shasum -a 256 "$zip_path" | awk '{print $1}')"
59-
fi
60-
if [ "$actual_checksum" != "$checksum" ]; then
61-
echo "Electron checksum mismatch for $artifact" >&2
62-
echo "expected $checksum, got $actual_checksum" >&2
63-
exit 1
64-
fi
65-
66-
rm -rf "$electron_dir/dist"
67-
mkdir -p "$electron_dir/dist"
68-
unzip -q "$zip_path" -d "$electron_dir/dist"
69-
if [ -f "$electron_dir/dist/electron.d.ts" ]; then
70-
mv "$electron_dir/dist/electron.d.ts" "$electron_dir/electron.d.ts"
71-
fi
72-
printf "%s" "$platform_path" > "$electron_dir/path.txt"
73-
74-
node -e 'const fs = require("node:fs"); const electronBinary = require("electron"); if (!fs.existsSync(electronBinary)) throw new Error(`Electron binary missing after install at ${electronBinary}`); console.log(`Electron verified at ${electronBinary}`)'
42+
run: pnpm -C apps/desktop verify:electron-binary
7543

7644
- name: Generate build info
7745
run: pnpm -r prebuild

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ TypeScript is strict-mode (`tsconfig.base.json`) and ESM (`NodeNext`).
3838
- Prefix intentionally unused variables/args with `_` to satisfy lint rules.
3939
- Do not edit `apps/desktop/src/shared/build-info.ts` manually (auto-generated in prebuild).
4040
- When changing the desktop Node tooling baseline or Electron runtime assumptions, keep `@types/node` pinned to the lowest supported Node runtime major so TypeScript cannot accept newer Node-only APIs.
41+
- For GitHub Actions jobs that run desktop tests or package/release under Node 24, verify Electron's platform binary with `pnpm -C apps/desktop verify:electron-binary` immediately after `pnpm install --frozen-lockfile`. Do not inline ad hoc Electron download logic in workflows; keep the shared verifier out of static/headless audit jobs that do not launch or import Electron.
4142
- Avoid unnecessary TypeScript casts. Prefer importing concrete shared types, narrowing with type guards, or shaping helper return types so call sites do not need `as` to satisfy the compiler.
4243
- Use shared constants, generated enums, or exported enum-like objects for statuses, reasons, protocol modes, channel names, storage keys, and other contract values. Do not duplicate hardcoded strings when a constant or enum exists.
4344
- Export and reuse shared TypeScript types for cross-module contracts or metadata patches instead of duplicating inline `Pick`/`Partial` shapes in callers.

apps/desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"assert:design-system-boundary": "tsx --test test/agent-dashboard-boundary.test.ts",
2525
"test:boot:design-system-off": "node --import tsx scripts/assert-design-system-boot-off.mjs",
2626
"measure:agent-dashboard-storage": "node scripts/measure-agent-dashboard-storage.mjs",
27+
"verify:electron-binary": "node scripts/ensure-electron-binary.mjs",
2728
"test": "tsx --test --test-concurrency=1 test/*.test.ts && node --test \"scripts/agent-monitor-packs/__tests__/*.test.js\" \"scripts/agent-monitor-pull-requests/__tests__/*.test.js\"",
2829
"pretest:contract": "pnpm build:agent-monitor",
2930
"test:contract": "node --test \"test-e2e/agent-monitor/specs/api-contract/*.test.mjs\"",
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env node
2+
/**
3+
* Verifies Electron's platform binary and repairs the install when GitHub
4+
* Actions leaves the npm package present but without a usable dist/path.txt.
5+
*
6+
* The repair path intentionally avoids @electron/get because that helper was
7+
* unreliable on the Node 24 Ubuntu runner during FEA-1543 CI remediation.
8+
*/
9+
import { spawnSync } from "node:child_process";
10+
import { createHash } from "node:crypto";
11+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
12+
import { createRequire } from "node:module";
13+
import { tmpdir } from "node:os";
14+
import path from "node:path";
15+
16+
const appRequire = createRequire(import.meta.url);
17+
const electronPackageJsonPath = appRequire.resolve("electron/package.json");
18+
const electronDir = path.dirname(electronPackageJsonPath);
19+
const electronRequire = createRequire(path.join(electronDir, "install.js"));
20+
const electronIndexPath = appRequire.resolve("electron");
21+
const electronPackage = JSON.parse(readFileSync(electronPackageJsonPath, "utf8"));
22+
23+
function getPlatformPath(platform) {
24+
const platformPaths = {
25+
darwin: "Electron.app/Contents/MacOS/Electron",
26+
freebsd: "electron",
27+
linux: "electron",
28+
mas: "Electron.app/Contents/MacOS/Electron",
29+
openbsd: "electron",
30+
win32: "electron.exe",
31+
};
32+
const platformPath = platformPaths[platform];
33+
if (platformPath == null) {
34+
throw new Error(`Electron builds are not available on platform: ${platform}`);
35+
}
36+
return platformPath;
37+
}
38+
39+
function getArch(platform) {
40+
if (process.env.npm_config_arch) {
41+
return process.env.npm_config_arch;
42+
}
43+
if (platform !== "darwin" || process.platform !== "darwin" || process.arch !== "x64") {
44+
return process.arch;
45+
}
46+
47+
const translated = spawnSync("sysctl", ["-in", "sysctl.proc_translated"], { encoding: "utf8" });
48+
return translated.status === 0 && translated.stdout.trim() === "1" ? "arm64" : process.arch;
49+
}
50+
51+
function verifyInstalledBinary() {
52+
delete appRequire.cache[electronIndexPath];
53+
const electronBinary = appRequire("electron");
54+
if (typeof electronBinary !== "string" || electronBinary.length === 0) {
55+
throw new Error("Electron module did not resolve to a binary path.");
56+
}
57+
if (!existsSync(electronBinary)) {
58+
throw new Error(`Electron binary is missing at ${electronBinary}`);
59+
}
60+
const binaryStats = statSync(electronBinary);
61+
if (!binaryStats.isFile() || binaryStats.size === 0) {
62+
throw new Error(`Electron binary is not a non-empty file at ${electronBinary}`);
63+
}
64+
return electronBinary;
65+
}
66+
67+
function runChecked(command, args) {
68+
const result = spawnSync(command, args, { stdio: "inherit" });
69+
if (result.error) {
70+
throw result.error;
71+
}
72+
if (result.status !== 0) {
73+
throw new Error(`${command} ${args.join(" ")} exited with status ${result.status}`);
74+
}
75+
}
76+
77+
function repairElectronBinary() {
78+
const version = electronPackage.version;
79+
const platform = process.env.npm_config_platform || process.platform;
80+
const arch = getArch(platform);
81+
const platformPath = getPlatformPath(platform);
82+
const artifact = `electron-v${version}-${platform}-${arch}.zip`;
83+
const checksums = electronRequire("./checksums.json");
84+
const expectedChecksum = checksums[artifact];
85+
if (expectedChecksum == null) {
86+
throw new Error(`Missing Electron checksum for ${artifact}`);
87+
}
88+
89+
const downloadDir = process.env.RUNNER_TEMP || path.join(tmpdir(), "closedloop-electron-binary");
90+
mkdirSync(downloadDir, { recursive: true });
91+
const zipPath = path.join(downloadDir, artifact);
92+
const downloadUrl = `https://github.com/electron/electron/releases/download/v${version}/${artifact}`;
93+
94+
console.log(`Downloading ${artifact}`);
95+
runChecked("curl", ["--fail", "--location", "--retry", "3", "--output", zipPath, downloadUrl]);
96+
97+
const actualChecksum = createHash("sha256").update(readFileSync(zipPath)).digest("hex");
98+
if (actualChecksum !== expectedChecksum) {
99+
throw new Error(`Electron checksum mismatch for ${artifact}: expected ${expectedChecksum}, got ${actualChecksum}`);
100+
}
101+
102+
const distDir = path.join(electronDir, "dist");
103+
rmSync(distDir, { recursive: true, force: true });
104+
mkdirSync(distDir, { recursive: true });
105+
runChecked("unzip", ["-q", zipPath, "-d", distDir]);
106+
107+
const extractedTypes = path.join(distDir, "electron.d.ts");
108+
if (existsSync(extractedTypes)) {
109+
renameSync(extractedTypes, path.join(electronDir, "electron.d.ts"));
110+
}
111+
writeFileSync(path.join(electronDir, "path.txt"), platformPath);
112+
}
113+
114+
try {
115+
try {
116+
const electronBinary = verifyInstalledBinary();
117+
console.log(`Electron binary verified at ${electronBinary}`);
118+
} catch (verificationError) {
119+
console.log(`Electron binary verification failed: ${verificationError.message}`);
120+
repairElectronBinary();
121+
const electronBinary = verifyInstalledBinary();
122+
console.log(`Electron binary repaired and verified at ${electronBinary}`);
123+
}
124+
} catch (error) {
125+
console.error(error.stack || error);
126+
process.exitCode = 1;
127+
}

0 commit comments

Comments
 (0)