|
2 | 2 | import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; |
3 | 3 | import { tmpdir } from "node:os"; |
4 | 4 | import { join } from "node:path"; |
| 5 | +import { pathToFileURL } from "node:url"; |
5 | 6 | import { spawnSync } from "node:child_process"; |
6 | 7 |
|
7 | | -const requestedChecks = new Set(process.argv.slice(2)); |
8 | | -const validArgs = new Set(["--root", "--mcp"]); |
9 | | -const invalidArgs = [...requestedChecks].filter((arg) => !validArgs.has(arg)); |
| 8 | +function main() { |
| 9 | + const requestedChecks = new Set(process.argv.slice(2)); |
| 10 | + const validArgs = new Set(["--root", "--mcp"]); |
| 11 | + const invalidArgs = [...requestedChecks].filter((arg) => !validArgs.has(arg)); |
10 | 12 |
|
11 | | -if (invalidArgs.length > 0) { |
12 | | - console.error(`Unknown changelog check option: ${invalidArgs.join(", ")}`); |
13 | | - process.exit(1); |
14 | | -} |
| 13 | + if (invalidArgs.length > 0) { |
| 14 | + console.error(`Unknown changelog check option: ${invalidArgs.join(", ")}`); |
| 15 | + process.exit(1); |
| 16 | + } |
15 | 17 |
|
16 | | -const tempDir = mkdtempSync(join(tmpdir(), "loopover-changelog-")); |
| 18 | + const tempDir = mkdtempSync(join(tmpdir(), "loopover-changelog-")); |
17 | 19 |
|
18 | | -try { |
19 | | - const checks = [ |
20 | | - { |
21 | | - label: "root changelog", |
22 | | - output: "CHANGELOG.md", |
23 | | - command: "npm run changelog:root", |
24 | | - selector: "--root", |
25 | | - runner: () => { |
26 | | - const generatedPath = join(tempDir, "CHANGELOG.md"); |
27 | | - run(["git-cliff", "--config", "cliff.toml", "--output", generatedPath], "root changelog"); |
28 | | - return generatedPath; |
| 20 | + try { |
| 21 | + const checks = [ |
| 22 | + { |
| 23 | + label: "root changelog", |
| 24 | + output: "CHANGELOG.md", |
| 25 | + command: "npm run changelog:root", |
| 26 | + selector: "--root", |
| 27 | + runner: () => { |
| 28 | + const generatedPath = join(tempDir, "CHANGELOG.md"); |
| 29 | + run(["git-cliff", "--config", "cliff.toml", "--output", generatedPath], "root changelog"); |
| 30 | + return generatedPath; |
| 31 | + }, |
29 | 32 | }, |
30 | | - }, |
31 | | - { |
32 | | - label: "MCP package changelog", |
33 | | - output: "packages/loopover-mcp/CHANGELOG.md", |
34 | | - command: "npm run changelog:mcp", |
35 | | - selector: "--mcp", |
36 | | - runner: () => { |
37 | | - const generatedPath = join(tempDir, "MCP_CHANGELOG.md"); |
38 | | - const version = JSON.parse(readFileSync("packages/loopover-mcp/package.json", "utf8")).version; |
39 | | - writeFileSync(generatedPath, readFileSync("packages/loopover-mcp/CHANGELOG.md", "utf8")); |
40 | | - run(["node", "scripts/generate-mcp-changelog.mjs", "--output", generatedPath, "--version", version], "MCP package changelog"); |
41 | | - return generatedPath; |
| 33 | + { |
| 34 | + label: "MCP package changelog", |
| 35 | + output: "packages/loopover-mcp/CHANGELOG.md", |
| 36 | + command: "npm run changelog:mcp", |
| 37 | + selector: "--mcp", |
| 38 | + runner: () => { |
| 39 | + const generatedPath = join(tempDir, "MCP_CHANGELOG.md"); |
| 40 | + const version = JSON.parse(readFileSync("packages/loopover-mcp/package.json", "utf8")).version; |
| 41 | + writeFileSync(generatedPath, readFileSync("packages/loopover-mcp/CHANGELOG.md", "utf8")); |
| 42 | + run(["node", "scripts/generate-mcp-changelog.mjs", "--output", generatedPath, "--version", version], "MCP package changelog"); |
| 43 | + return generatedPath; |
| 44 | + }, |
42 | 45 | }, |
43 | | - }, |
44 | | - ].filter((check) => requestedChecks.size === 0 || requestedChecks.has(check.selector)); |
| 46 | + ].filter((check) => requestedChecks.size === 0 || requestedChecks.has(check.selector)); |
45 | 47 |
|
46 | | - const failures = []; |
47 | | - for (const check of checks) { |
48 | | - const generatedPath = check.runner(); |
49 | | - const expected = readFileSync(generatedPath, "utf8"); |
50 | | - const actual = readFileSync(check.output, "utf8"); |
51 | | - if (normalize(actual) !== normalize(expected)) failures.push(`${check.output} is stale; run ${check.command}.`); |
52 | | - } |
| 48 | + const failures = []; |
| 49 | + for (const check of checks) { |
| 50 | + const generatedPath = check.runner(); |
| 51 | + const expected = readFileSync(generatedPath, "utf8"); |
| 52 | + const actual = readFileSync(check.output, "utf8"); |
| 53 | + if (normalize(actual) !== normalize(expected)) failures.push(`${check.output} is stale; run ${check.command}.`); |
| 54 | + } |
53 | 55 |
|
54 | | - if (failures.length > 0) { |
55 | | - console.error(failures.join("\n")); |
56 | | - process.exit(1); |
57 | | - } |
| 56 | + if (failures.length > 0) { |
| 57 | + console.error(failures.join("\n")); |
| 58 | + process.exit(1); |
| 59 | + } |
58 | 60 |
|
59 | | - console.log(`${checks.map((check) => check.output).join(", ")} current`); |
60 | | -} finally { |
61 | | - rmSync(tempDir, { recursive: true, force: true }); |
| 61 | + console.log(`${checks.map((check) => check.output).join(", ")} current`); |
| 62 | + } finally { |
| 63 | + rmSync(tempDir, { recursive: true, force: true }); |
| 64 | + } |
62 | 65 | } |
63 | 66 |
|
64 | | -function run(command, label) { |
65 | | - const result = spawnSync(command[0], command.slice(1), { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); |
| 67 | +/** Run a command, failing the process with its real output on a non-zero status. `spawn` and `onFailure` are |
| 68 | + * injectable purely for testability; every real caller uses the defaults. When the command cannot even |
| 69 | + * launch (`status` is null, e.g. the binary is not on PATH), `result.error` holds the actual ENOENT/EACCES |
| 70 | + * reason -- surface its message (#7772) instead of the generic `${label} failed`, which wastes debugging time. */ |
| 71 | +export function run(command, label, { spawn = spawnSync, onFailure = defaultOnFailure } = {}) { |
| 72 | + const result = spawn(command[0], command.slice(1), { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }); |
66 | 73 | if (result.status !== 0) { |
67 | | - process.stderr.write(result.stderr || result.stdout || `${label} failed`); |
68 | | - process.exit(result.status ?? 1); |
| 74 | + const message = result.stderr || result.stdout || (result.error ? `${label}: ${result.error.message}\n` : `${label} failed`); |
| 75 | + onFailure(message, result.status ?? 1); |
69 | 76 | } |
70 | 77 | } |
71 | 78 |
|
| 79 | +function defaultOnFailure(message, code) { |
| 80 | + process.stderr.write(message); |
| 81 | + process.exit(code); |
| 82 | +} |
| 83 | + |
72 | 84 | function normalize(value) { |
73 | 85 | return value.replace(/\r\n/g, "\n").trimEnd(); |
74 | 86 | } |
| 87 | + |
| 88 | +/* v8 ignore next -- entrypoint guard: runs the checks only as a CLI, so importing `run` for tests is a no-op. */ |
| 89 | +if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) main(); |
0 commit comments