Files
msd-core/scripts/diff-touches-shipped-paths.cjs
Tom Boucher c03f97188f chore(#1328): remove orphaned root vitest.config.ts left by SDK retirement
vitest.config.ts configured Vitest (not a dependency) to run .ts test files (the repo has none) rooted at ./sdk, a directory deleted when the SDK package seam was retired in #191 (ADR-0174). No npm script, workflow, or dependency references it.

Also drop the now-dead sdk/src/*.test.* branch in diff-touches-shipped-paths.cjs isCiGating(), which can never match since the sdk/ tree no longer exists.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 08:24:21 -04:00

150 lines
6.3 KiB
JavaScript

#!/usr/bin/env node
/**
* Used by the release-sdk hotfix cherry-pick loop to decide whether a
* candidate commit can possibly change what ships in the npm package.
*
* Reads a newline-separated list of paths from stdin (typically the
* output of `git diff-tree --no-commit-id --name-only -r <SHA>`) and
* exits with one of three codes so the workflow can distinguish a
* legitimate "skip this commit" signal from a classifier failure.
*
* "Shipped" = the union of:
* - package.json (always included by `npm pack`, regardless of `files`)
* - every entry in package.json `files`, treated as either an exact
* file match or a directory prefix (matching `npm pack` semantics).
* - CI-gating test paths: `tests/<anything>` — these don't ship in the
* tarball, but they gate the hotfix-branch test job. A test fixture
* update that aligns with a cherry-picked production fix MUST be
* pickable or CI fails on the hotfix run.
* #3621 — root cause of the v1.42.3 hotfix red CI.
*
* `package-lock.json` is intentionally NOT considered shipped — `npm pack`
* excludes it from the tarball unless it's explicitly in `files`, and at
* the time of writing this repo's `files` whitelist does not include it.
*
* Exit codes (the workflow MUST treat these distinctly — bug #2983):
* 0 at least one path is shipped → cherry-pick is meaningful
* 1 no shipped paths → CI / test / docs / planning
* only; hotfix loop skips
* 2 classifier error → bad/missing package.json,
* I/O failure, or any
* uncaught exception. The
* workflow MUST fail-fast on
* this code rather than
* treating it as a skip.
*
* Why distinct codes: Node's default exit code for uncaught throws is 1,
* which would otherwise be indistinguishable from the legitimate "no
* shipped paths" result. CodeRabbit on PR #2981 / bug #2983.
*/
'use strict';
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;
const EXIT_ERROR = 2;
function loadShipPrefixes(pkgPath) {
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
const files = Array.isArray(pkg.files) ? pkg.files : [];
return ['package.json', ...files];
}
// #3621: paths that gate hotfix-branch CI even though they don't appear
// in the npm tarball. When a cherry-picked production fix changes behavior
// that an existing test on the v1.42.2 base asserts against, the matching
// test fixture from `main` must also be cherry-picked or CI fails on the
// hotfix run (exactly what happened on v1.42.3 — production fix(3562) was
// picked, the bundled test-fixture correction in commit 08848df8 was not).
// Combined with the `test:` prefix being added to the candidate-loop regex
// in hotfix.yml, this lets `test(####):` fixture-alignment commits be
// cherry-picked alongside their production counterparts.
function isCiGating(diffPath) {
return diffPath.startsWith('tests/');
}
function isShipped(diffPath, shipPrefixes) {
// Normalize Windows-style separators just in case (git always emits
// forward slashes, but a developer running this locally on a different
// tool's output shouldn't get a false negative).
const p = diffPath.replace(/\\/g, '/');
return shipPrefixes.some((s) => p === s || p.startsWith(s + '/'));
}
// #2980: commits that touch `.github/workflows/*` cannot be cherry-picked
// onto a hotfix branch because the default GITHUB_TOKEN lacks the
// `workflow` permission and the push step fails. Detect them upfront so a
// `test:`-eligible commit bundling a workflow edit still gets skipped.
function isPushBlocking(diffPath) {
return diffPath.replace(/\\/g, '/').startsWith('.github/workflows/');
}
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);
});
}
async function main() {
try {
let shipPrefixes;
try {
const pkgPath = path.resolve(process.cwd(), 'package.json');
shipPrefixes = loadShipPrefixes(pkgPath);
} catch (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) {
runMain(main);
}
module.exports = { loadShipPrefixes, isShipped, isCiGating, isPushBlocking, EXIT_SHIPPED, EXIT_NOT_SHIPPED, EXIT_ERROR };