mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-07-18 17:05:48 +00:00
* ci(electron): verify packaged app.asar and smoke-test launch Adds coverage for the gap that let #7320 ship: CI built electron TS and ran electron-builder on release, but never exercised the packaged binary. A stray relative import reaching out of electron/** was compiled by tsc into src/app/util/*.js, fell outside the files glob in electron-builder.yaml, and was missing from app.asar — the main process crashed on launch for every 18.2.7 user. Two new checks, both running on ubuntu-latest: 1. tools/verify-electron-requires.js extracts app.asar and re-resolves every relative require() under electron/** against the packaged tree. Runs on every PR that touches electron/**, electron-builder config, package.json, or the workflow itself. ~10s after the build. 2. A launch-under-xvfb smoke test starts the packaged binary, waits 30s for a crash, and greps stderr for 'Cannot find module' / 'Uncaught Exception'. Runs on push to master, release tags, and manual dispatch (not every PR — needs xvfb setup + idle wait). Exposed as npm run electron:verify-asar for local use after dist. * ci(electron): harden smoke workflow and verify script from review Review feedback on the previous commit surfaced a handful of real defects. Addressed here: verify-electron-requires.js - Guard resolved paths against escaping the extracted asar tree. Without this, a relative require with enough `..` climbs above the temp dir and Node's resolver hits the host filesystem, masking a genuinely missing module behind a stray file on the CI runner. - Walk .cjs and .mjs files too. electron/simple-store.test.cjs ships via the `electron/**/*` glob and was being skipped. - Move cleanup out of the path where `process.exit(1)` runs inside a `try/finally` (synchronous exit does not guarantee `finally`), so the temp dir is reliably removed. - Code comment noting that esbuild-bundled preload.js is opaque to this walker — that coverage comes from the launch smoke test. electron-smoke.yml - Broaden pull_request paths filter. The previous filter only matched electron/**, which is exactly the wrong answer: #7320's root cause was a file under src/app/util/ that electron/ imported. Now uses a negative list — docs, translations, android/, ios/, other workflows. - Explicit Xvfb screen geometry (`-screen 0 1280x720x24`) and `--disable-dev-shm-usage` on the Electron side. Both known CI flake preventers. - Expanded crash-marker regex: SIGSEGV, Segmentation fault, TypeError:, ReferenceError:, FATAL ERROR, Check failed. Also scan the log during the liveness loop, not just at the end — catches main-process exceptions that get logged without killing the process. - Move the launch log under .tmp/smoke/ so upload-artifact@v7 reliably finds it on failure; /tmp paths are fragile across runner images. - Sweep stray electron worker processes on teardown via pkill. --------- Co-authored-by: Claude <noreply@anthropic.com>
139 lines
4.5 KiB
JavaScript
139 lines
4.5 KiB
JavaScript
#!/usr/bin/env node
|
|
/*
|
|
* Walks the contents of a packaged app.asar and verifies that every relative
|
|
* require() call inside electron/ .js/.cjs/.mjs files resolves to a file that
|
|
* was actually packaged. Catches #7320-class bugs where tsc happily compiles
|
|
* an import reaching out of the electron tree (e.g. '../src/app/util/foo')
|
|
* but the compiled dependency lives outside the files glob in
|
|
* electron-builder.yaml and is therefore missing from app.asar at runtime.
|
|
*
|
|
* Usage:
|
|
* node tools/verify-electron-requires.js <path/to/app.asar>
|
|
*
|
|
* Exits 0 on success, 1 when unresolvable requires are found, 2 on usage
|
|
* errors.
|
|
*/
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
const path = require('path');
|
|
|
|
const asarPath = process.argv[2];
|
|
if (!asarPath) {
|
|
console.error('Usage: verify-electron-requires.js <path/to/app.asar>');
|
|
process.exit(2);
|
|
}
|
|
if (!fs.existsSync(asarPath)) {
|
|
console.error(`asar not found: ${asarPath}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
let asar;
|
|
try {
|
|
asar = require('@electron/asar');
|
|
} catch (err) {
|
|
console.error(
|
|
'Could not load @electron/asar. Run "npm i" first (it is a transitive of electron-builder).',
|
|
);
|
|
console.error(err.message);
|
|
process.exit(2);
|
|
}
|
|
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'sp-verify-asar-'));
|
|
const errors = [];
|
|
|
|
// Note: `electron/preload.js` is an esbuild bundle produced by
|
|
// electron/scripts/bundle-preload.js. All relative imports are inlined at
|
|
// bundle time, so no require('./...') calls reach this walker — meaning
|
|
// preload-side regressions of the #7320 class are NOT caught here. That
|
|
// coverage comes from the launch smoke test in electron-smoke.yml.
|
|
const SHIPPED_JS_EXT = /\.(c|m)?js$/;
|
|
|
|
const walkSourceFiles = (root, visit) => {
|
|
for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
|
|
const full = path.join(root, entry.name);
|
|
if (entry.isDirectory()) {
|
|
if (entry.name === 'node_modules') continue;
|
|
walkSourceFiles(full, visit);
|
|
} else if (entry.isFile() && SHIPPED_JS_EXT.test(full)) {
|
|
visit(full);
|
|
}
|
|
}
|
|
};
|
|
|
|
const collectRelativeRequires = (src) => {
|
|
const targets = [];
|
|
const re = /\brequire\(\s*(['"])(\.[^'"\n]+)\1\s*\)/g;
|
|
let m;
|
|
while ((m = re.exec(src)) !== null) targets.push(m[2]);
|
|
return targets;
|
|
};
|
|
|
|
// Guard: a resolved path must stay inside the extracted asar tree.
|
|
// Without this, a relative require with enough `..` climbs above `tmp` and
|
|
// Node's resolver happily hits the host filesystem (or the host's
|
|
// node_modules), masking a genuinely missing module behind a stray file.
|
|
const tmpPrefix = tmp.endsWith(path.sep) ? tmp : tmp + path.sep;
|
|
const isInsideTmp = (p) => p === tmp || p.startsWith(tmpPrefix);
|
|
|
|
let exitCode = 0;
|
|
try {
|
|
asar.extractAll(asarPath, tmp);
|
|
|
|
const electronDir = path.join(tmp, 'electron');
|
|
if (!fs.existsSync(electronDir)) {
|
|
console.error(`No electron/ directory found inside ${asarPath}`);
|
|
exitCode = 1;
|
|
} else {
|
|
walkSourceFiles(electronDir, (file) => {
|
|
const src = fs.readFileSync(file, 'utf8');
|
|
for (const target of collectRelativeRequires(src)) {
|
|
const candidate = path.resolve(path.dirname(file), target);
|
|
if (!isInsideTmp(candidate)) {
|
|
errors.push(
|
|
`${path.relative(tmp, file)}: require('${target}') escapes packaged tree`,
|
|
);
|
|
continue;
|
|
}
|
|
try {
|
|
const resolved = require.resolve(candidate, {
|
|
paths: [path.dirname(file)],
|
|
});
|
|
if (!isInsideTmp(resolved)) {
|
|
errors.push(
|
|
`${path.relative(tmp, file)}: require('${target}') resolved to host path ${resolved} — module is missing from app.asar`,
|
|
);
|
|
}
|
|
} catch {
|
|
errors.push(
|
|
`${path.relative(tmp, file)}: cannot resolve require('${target}')`,
|
|
);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
} finally {
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
}
|
|
|
|
if (exitCode !== 0) process.exit(exitCode);
|
|
|
|
if (errors.length) {
|
|
console.error(
|
|
`Found ${errors.length} unresolvable require() target(s) inside ${asarPath}:\n`,
|
|
);
|
|
for (const err of errors) console.error(' ' + err);
|
|
console.error(
|
|
'\nThis means a .ts file under electron/ imported a path that was not packaged.',
|
|
);
|
|
console.error(
|
|
'Fix: move the imported module under electron/shared-with-frontend/ (or another',
|
|
);
|
|
console.error(
|
|
'path covered by files: in electron-builder.yaml) and update importers.',
|
|
);
|
|
process.exit(1);
|
|
}
|
|
|
|
console.log(
|
|
`OK: all relative require() targets under electron/ resolve cleanly in ${asarPath}`,
|
|
);
|