Part 1 of 2 of the n/no-process-exit cleanup (umbrella #738): convert every process.exit() call in standalone scripts/** CLIs to the rule-compliant pattern. - New shared helper scripts/lib/cli-exit.cjs: ExitError(code,message) + runMain() which translates a thrown ExitError / returned number into process.exitCode (never process.exit()), flushing output and still firing process.on('exit'). - main()-based entrypoints: throw new ExitError(code) for errors, return <code> for verdicts; invoked via runMain(main). Child exit codes preserved via return. - top-level-only scripts: imperative body extracted into main() so mid-flow aborts (throw ExitError) actually halt; pure consts/helpers stay at module scope. - diff-touches-shipped-paths.cjs: stdin event handling restructured to an async read so the whole flow runs under runMain; uncaughtException/unhandledRejection nets replaced by an in-band catch that preserves EXIT_ERROR=2. Exit codes verified unchanged for every converted script (success/error/help and the 0/1/2 semantic codes in diff-touches). Rule stays warn here; flipped to error in part 2 (#738) once gsd-core/bin/** is also clean. Refs #739 Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,6 +43,7 @@
|
||||
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
||||
|
||||
const EXIT_SHIPPED = 0;
|
||||
const EXIT_NOT_SHIPPED = 1;
|
||||
@@ -88,60 +89,67 @@ function isPushBlocking(diffPath) {
|
||||
return diffPath.replace(/\\/g, '/').startsWith('.github/workflows/');
|
||||
}
|
||||
|
||||
function fail(message, err) {
|
||||
process.stderr.write(`diff-touches-shipped-paths: ${message}\n`);
|
||||
if (err && err.stack) process.stderr.write(`${err.stack}\n`);
|
||||
process.exit(EXIT_ERROR);
|
||||
function readStdin() {
|
||||
return new Promise((resolve, reject) => {
|
||||
let data = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('data', (chunk) => { data += chunk; });
|
||||
process.stdin.on('end', () => resolve(data));
|
||||
process.stdin.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
function main() {
|
||||
// Surface ANY uncaught failure as exit 2 (classifier error) rather
|
||||
// than letting Node's default-1 shadow the legitimate
|
||||
// "no shipped paths" result. Bug #2983.
|
||||
process.on('uncaughtException', (err) => fail('uncaught exception', err));
|
||||
process.on('unhandledRejection', (err) => fail('unhandled rejection', err));
|
||||
|
||||
let shipPrefixes;
|
||||
async function main() {
|
||||
try {
|
||||
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
||||
shipPrefixes = loadShipPrefixes(pkgPath);
|
||||
} catch (err) {
|
||||
return fail(`failed to read package.json from ${process.cwd()}`, err);
|
||||
}
|
||||
|
||||
let buf = '';
|
||||
process.stdin.setEncoding('utf8');
|
||||
process.stdin.on('error', (err) => fail('stdin read error', err));
|
||||
process.stdin.on('data', (chunk) => {
|
||||
buf += chunk;
|
||||
});
|
||||
process.stdin.on('end', () => {
|
||||
let shipPrefixes;
|
||||
try {
|
||||
const paths = buf.split('\n').map((s) => s.trim()).filter(Boolean);
|
||||
// #2980 still wins over #3621: any commit touching .github/workflows/*
|
||||
// is unpickable regardless of other content because the push step
|
||||
// fails on workflow scope rejection. Check this first.
|
||||
if (paths.some(isPushBlocking)) {
|
||||
process.exit(EXIT_NOT_SHIPPED);
|
||||
}
|
||||
if (paths.some((p) => isShipped(p, shipPrefixes))) {
|
||||
process.exit(EXIT_SHIPPED);
|
||||
}
|
||||
// #3621: a commit whose only relevant paths are CI-gating tests is
|
||||
// still pickable — it can change whether the hotfix CI passes even
|
||||
// though it doesn't change what the npm tarball ships.
|
||||
if (paths.some(isCiGating)) {
|
||||
process.exit(EXIT_SHIPPED);
|
||||
}
|
||||
process.exit(EXIT_NOT_SHIPPED);
|
||||
const pkgPath = path.resolve(process.cwd(), 'package.json');
|
||||
shipPrefixes = loadShipPrefixes(pkgPath);
|
||||
} catch (err) {
|
||||
fail('classification failed', err);
|
||||
process.stderr.write(`diff-touches-shipped-paths: failed to read package.json from ${process.cwd()}\n`);
|
||||
if (err && err.stack) process.stderr.write(`${err.stack}\n`);
|
||||
throw new ExitError(EXIT_ERROR);
|
||||
}
|
||||
});
|
||||
|
||||
let buf;
|
||||
try {
|
||||
buf = await readStdin();
|
||||
} catch (err) {
|
||||
process.stderr.write(`diff-touches-shipped-paths: stdin read error\n`);
|
||||
if (err && err.stack) process.stderr.write(`${err.stack}\n`);
|
||||
throw new ExitError(EXIT_ERROR);
|
||||
}
|
||||
|
||||
const paths = buf.split('\n').map((s) => s.trim()).filter(Boolean);
|
||||
// #2980 still wins over #3621: any commit touching .github/workflows/*
|
||||
// is unpickable regardless of other content because the push step
|
||||
// fails on workflow scope rejection. Check this first.
|
||||
if (paths.some(isPushBlocking)) {
|
||||
return EXIT_NOT_SHIPPED;
|
||||
}
|
||||
if (paths.some((p) => isShipped(p, shipPrefixes))) {
|
||||
return EXIT_SHIPPED;
|
||||
}
|
||||
// #3621: a commit whose only relevant paths are CI-gating tests is
|
||||
// still pickable — it can change whether the hotfix CI passes even
|
||||
// though it doesn't change what the npm tarball ships.
|
||||
if (paths.some(isCiGating)) {
|
||||
return EXIT_SHIPPED;
|
||||
}
|
||||
return EXIT_NOT_SHIPPED;
|
||||
} catch (e) {
|
||||
// Re-throw ExitError unchanged; map any unexpected error to EXIT_ERROR=2
|
||||
// (Node's default uncaught-exception code is 1, which is indistinguishable
|
||||
// from the legitimate EXIT_NOT_SHIPPED result — bug #2983).
|
||||
if (e instanceof ExitError) throw e;
|
||||
process.stderr.write(`diff-touches-shipped-paths: classification failed\n`);
|
||||
if (e && e.stack) process.stderr.write(`${e.stack}\n`);
|
||||
throw new ExitError(EXIT_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main();
|
||||
runMain(main);
|
||||
}
|
||||
|
||||
module.exports = { loadShipPrefixes, isShipped, isCiGating, isPushBlocking, EXIT_SHIPPED, EXIT_NOT_SHIPPED, EXIT_ERROR };
|
||||
|
||||
Reference in New Issue
Block a user