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, + ); +});