From 7fb5a7e78cbcd54dd2d895e282d3096998dd8cd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Jake=C5=A1?= Date: Fri, 15 May 2026 17:38:14 +0200 Subject: [PATCH] Fix release prepare status parsing Preserve the leading porcelain status column when parsing git status output. Trimming the whole output dropped the first character of the first changed path when the line started with an unstaged status column, so Cargo.lock could be read as argo.lock. --- package.json | 2 +- scripts/release-prepare.mjs | 24 ++++++++++++++-------- scripts/release-prepare.test.mjs | 34 ++++++++++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 9 deletions(-) create mode 100644 scripts/release-prepare.test.mjs diff --git a/package.json b/package.json index 650e9761..fa9d75a0 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "build": "astro build", "preview": "astro preview", "check": "astro check", - "test": "node --test src/remark-rewrite-doc-links.test.mjs src/astro-config.test.mjs scripts/release-metadata.test.mjs scripts/release-homebrew-formula.test.mjs scripts/install.test.mjs scripts/forkpress-launcher.test.mjs", + "test": "node --test src/remark-rewrite-doc-links.test.mjs src/astro-config.test.mjs scripts/release-metadata.test.mjs scripts/release-prepare.test.mjs scripts/release-homebrew-formula.test.mjs scripts/install.test.mjs scripts/forkpress-launcher.test.mjs", "release:prepare": "node scripts/release-prepare.mjs", "release:validate": "node scripts/release-validate.mjs", "validate": "npm run check && npm run test && npm run build" diff --git a/scripts/release-prepare.mjs b/scripts/release-prepare.mjs index d9067119..ec8fcfd5 100755 --- a/scripts/release-prepare.mjs +++ b/scripts/release-prepare.mjs @@ -140,22 +140,30 @@ function remoteRefExists(args) { } function requireExpectedChanges(initialChangedFiles) { - const status = runOutput('git', ['status', '--porcelain']).trim(); - if (status === '') { + const status = runOutput('git', ['status', '--porcelain']); + requireChangedReleaseFiles([...initialChangedFiles, cargoLock], status); +} + +export function requireChangedReleaseFiles(expectedFiles, status) { + if (status.trim() === '') { throw new ReleaseMetadataError('Release metadata is already at the requested version.'); } - const changedFiles = new Set( + const changedFiles = changedFilesFromStatus(status); + for (const file of expectedFiles) { + if (!changedFiles.has(file)) { + throw new ReleaseMetadataError(`Expected release file was not changed: ${file}`); + } + } +} + +export function changedFilesFromStatus(status) { + return new Set( status .split('\n') .map((line) => line.slice(3)) .filter(Boolean), ); - for (const file of [...initialChangedFiles, cargoLock]) { - if (!changedFiles.has(file)) { - throw new ReleaseMetadataError(`Expected release file was not changed: ${file}`); - } - } } function run(command, args, options = {}) { diff --git a/scripts/release-prepare.test.mjs b/scripts/release-prepare.test.mjs new file mode 100644 index 00000000..f1060c2b --- /dev/null +++ b/scripts/release-prepare.test.mjs @@ -0,0 +1,34 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { + ReleaseMetadataError, + cargoLock, + installerManifest, + productionCrateManifests, +} from './release-metadata.mjs'; +import { changedFilesFromStatus, requireChangedReleaseFiles } from './release-prepare.mjs'; + +test('parses changed files from git status porcelain output', () => { + assert.deepEqual( + [...changedFilesFromStatus(' M Cargo.lock\nM crates/forkpress-cli/Cargo.toml\n')], + ['Cargo.lock', 'crates/forkpress-cli/Cargo.toml'], + ); +}); + +test('requires release metadata and Cargo.lock to be dirty', () => { + const expectedFiles = [...productionCrateManifests, installerManifest, cargoLock]; + const status = expectedFiles.map((path) => ` M ${path}`).join('\n'); + + assert.doesNotThrow(() => requireChangedReleaseFiles(expectedFiles, status)); +}); + +test('rejects release prepare when an edited metadata file is missing from git status', () => { + const [missingFile, ...changedFiles] = productionCrateManifests; + const status = [...changedFiles, installerManifest, cargoLock].map((path) => ` M ${path}`).join('\n'); + + assert.throws( + () => requireChangedReleaseFiles([missingFile, ...changedFiles, installerManifest, cargoLock], status), + ReleaseMetadataError, + ); +});