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>
150 lines
6.3 KiB
JavaScript
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 };
|