From 09b1cbeaf4b8a6b6b573bfefea8e907d2752209a Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Tue, 1 Sep 2026 08:55:32 +1000 Subject: [PATCH 1/3] test: define CLI package version contract --- scripts/verify-cli-version.mjs | 21 ++++++++++++ tests/release.test.mjs | 58 +++++++++++++++++++++++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 scripts/verify-cli-version.mjs diff --git a/scripts/verify-cli-version.mjs b/scripts/verify-cli-version.mjs new file mode 100644 index 0000000..65661cd --- /dev/null +++ b/scripts/verify-cli-version.mjs @@ -0,0 +1,21 @@ +#!/usr/bin/env node +import { execFileSync } from "node:child_process"; +import { readFileSync } from "node:fs"; + +const [cli, manifest = "package.json"] = process.argv.slice(2); +if (!cli) { + console.error("Usage: verify-cli-version [package.json]"); + process.exit(1); +} + +const expected = JSON.parse(readFileSync(manifest, "utf8")).version; +const command = cli.endsWith(".js") ? process.execPath : cli; +const args = cli.endsWith(".js") ? [cli, "--version"] : ["--version"]; +const actual = execFileSync(command, args, { encoding: "utf8" }).trim(); + +if (actual !== expected) { + console.error(`CLI version mismatch: package.json=${expected}, CLI=${actual}`); + process.exit(1); +} + +console.log(`CLI version ${actual} matches package.json.`); diff --git a/tests/release.test.mjs b/tests/release.test.mjs index 751d65e..c33d62d 100644 --- a/tests/release.test.mjs +++ b/tests/release.test.mjs @@ -1,7 +1,9 @@ import test from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import { readFileSync } from "node:fs"; +import { chmodSync, cpSync, mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; test("release tag must exactly match package version", () => { for (const tag of [undefined, "0.1.0", "v0.1", "v9.9.9"]) { @@ -12,6 +14,60 @@ test("release tag must exactly match package version", () => { assert.equal(spawnSync(process.execPath, ["scripts/validate-release-tag.mjs", "v0.1.0"]).status, 0); }); +test("CLI version verification rejects package mismatches", () => { + const directory = mkdtempSync(join(tmpdir(), "promptdiff-version-check-")); + const cli = join(directory, "promptdiff"); + const manifest = join(directory, "package.json"); + try { + writeFileSync(manifest, JSON.stringify({ version: "9.8.7" })); + writeFileSync(cli, "#!/bin/sh\nprintf '9.8.7\\n'\n"); + chmodSync(cli, 0o755); + let run = spawnSync(process.execPath, ["scripts/verify-cli-version.mjs", cli, manifest], { encoding: "utf8" }); + assert.equal(run.status, 0, run.stderr); + + writeFileSync(cli, "#!/bin/sh\nprintf '0.1.0\\n'\n"); + run = spawnSync(process.execPath, ["scripts/verify-cli-version.mjs", cli, manifest], { encoding: "utf8" }); + assert.equal(run.status, 1); + assert.match(run.stderr, /package\.json=9\.8\.7, CLI=0\.1\.0/); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + +test("built and installed CLIs report a supplied package version", () => { + const directory = mkdtempSync(join(tmpdir(), "promptdiff-version-package-")); + const stage = join(directory, "stage"); + const consumer = join(directory, "consumer"); + try { + mkdirSync(stage); + cpSync("dist", join(stage, "dist"), { recursive: true }); + writeFileSync(join(stage, "package.json"), JSON.stringify({ + name: "promptdiff-version-fixture", + version: "9.8.7", + type: "module", + bin: { promptdiff: "./dist/cli.js" }, + files: ["dist"], + })); + + let run = spawnSync(process.execPath, [join(stage, "dist", "cli.js"), "--version"], { encoding: "utf8" }); + assert.equal(run.status, 0, run.stderr); + assert.equal(run.stdout, "9.8.7\n"); + + const packed = spawnSync("npm", ["pack", "--silent"], { cwd: stage, encoding: "utf8" }); + assert.equal(packed.status, 0, packed.stderr); + mkdirSync(consumer); + assert.equal(spawnSync("npm", ["init", "--yes"], { cwd: consumer, encoding: "utf8" }).status, 0); + const tarball = join(stage, packed.stdout.trim()); + const install = spawnSync("npm", ["install", tarball], { cwd: consumer, encoding: "utf8" }); + assert.equal(install.status, 0, install.stderr); + run = spawnSync(join(consumer, "node_modules", ".bin", "promptdiff"), ["--version"], { encoding: "utf8" }); + assert.equal(run.status, 0, run.stderr); + assert.equal(run.stdout, "9.8.7\n"); + } finally { + rmSync(directory, { recursive: true, force: true }); + } +}); + test("release workflow validates before publishing and publishes before GitHub release", () => { const workflow = readFileSync(".github/workflows/release.yml", "utf8"); const validate = workflow.indexOf("npm run release:tag"); From 7ea09294f9d6e91207f9d0cea51e6b822e702ce0 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Tue, 1 Sep 2026 08:55:32 +1000 Subject: [PATCH 2/3] fix: derive CLI version from package metadata --- scripts/package-smoke.mjs | 11 +++++++++++ src/cli.ts | 7 ++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/package-smoke.mjs b/scripts/package-smoke.mjs index cf3b16a..8a376b2 100644 --- a/scripts/package-smoke.mjs +++ b/scripts/package-smoke.mjs @@ -3,6 +3,9 @@ import { execFileSync } from "node:child_process"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const verifyCliVersion = fileURLToPath(new URL("./verify-cli-version.mjs", import.meta.url)); const requiredFiles = [ "dist/cli.js", @@ -30,6 +33,10 @@ if (missing.length > 0) { console.log(`Package smoke passed with ${pack.files.length} files.`); +execFileSync(process.execPath, [verifyCliVersion, join(process.cwd(), "dist", "cli.js")], { + stdio: "inherit", +}); + const consumer = mkdtempSync(join(tmpdir(), "promptdiff-package-smoke-")); try { const tarball = execFileSync("npm", ["pack", "--silent"], { encoding: "utf8" }).trim(); @@ -38,6 +45,10 @@ try { const cli = join(consumer, "node_modules", ".bin", "promptdiff"); const help = execFileSync(cli, ["--help"], { cwd: consumer, encoding: "utf8" }); if (!help.includes("promptdiff compare")) throw new Error("Installed CLI help was not usable"); + execFileSync(process.execPath, [verifyCliVersion, cli, join(process.cwd(), "package.json")], { + cwd: consumer, + stdio: "inherit", + }); rmSync(join(process.cwd(), tarball)); } finally { rmSync(consumer, { recursive: true, force: true }); diff --git a/src/cli.ts b/src/cli.ts index 0bbcb59..1ea7621 100755 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,4 +1,5 @@ #!/usr/bin/env node +import { readFileSync } from 'node:fs'; import { mkdir, writeFile, readdir, stat } from 'node:fs/promises'; import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'; import { analyzePromptDiff } from './analyzer.js'; @@ -15,6 +16,10 @@ interface ParsedArgs { flags: Map; } +const packageVersion = JSON.parse( + readFileSync(new URL('../package.json', import.meta.url), 'utf8'), +) as { version: string }; + function parseArgs(argv: string[]): ParsedArgs { const [command, ...rest] = argv; const flags = new Map(); @@ -168,7 +173,7 @@ export async function main(argv = process.argv.slice(2)): Promise { return 0; } if (args.command === '--version' || args.command === '-v') { - process.stdout.write('0.1.0\n'); + process.stdout.write(`${packageVersion.version}\n`); return 0; } if (args.command === 'examples') { From 49ee8b308445b0cf99e7e4bfbd581056b7386884 Mon Sep 17 00:00:00 2001 From: Roger Chappel Date: Tue, 1 Sep 2026 08:55:32 +1000 Subject: [PATCH 3/3] docs: simplify release version updates --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7862646..616acd5 100644 --- a/README.md +++ b/README.md @@ -202,8 +202,8 @@ npm run package:smoke npm run release:check ``` -The package smoke checks the tarball contents, installs that tarball into a clean temporary consumer, and invokes the installed CLI. +The package smoke checks the tarball contents, verifies the built CLI reports the package version, installs that tarball into a clean temporary consumer, and verifies the installed CLI reports the same version. Continuous integration runs the full `npm run release:check` suite on Node.js 20, 22, and 24. Node.js 20 is the package's declared minimum supported runtime, while 22 and 24 cover the current supported major releases. -Version releases are distributed through npm. Set `package.json` to the intended version, run `npm run release:tag -- v` and `npm run release:check`, then push that exact tag. The tag workflow validates the tag again, publishes the public package to npm with provenance, and creates the GitHub release only after npm publication succeeds. +Version releases are distributed through npm. Set `package.json` to the intended version; the CLI reads that value directly, so no source literal needs a separate update. Run `npm run release:tag -- v` and `npm run release:check`, then push that exact tag. The tag workflow validates the tag again, publishes the public package to npm with provenance, and creates the GitHub release only after npm publication succeeds.