diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 041d0ff7..4e9d6c7e 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "7.0.43" + ".": "7.1.1" } diff --git a/README.md b/README.md index 4bedf1f6..125520d1 100644 --- a/README.md +++ b/README.md @@ -308,6 +308,15 @@ Being honest about where this still has issues:
v7.x +### v7.1.1 +- Bumped `@imdeadpool/guardex` from `7.1.0` to `7.1.1` so the current + `main` payload can publish under a fresh npm version after `7.1.0` reached + the registry. +- Direct maintainer `npm publish` now checks npm during `prepublishOnly` and + bumps package release metadata to the next unpublished patch version when the + committed version is already published. GitHub Actions release publishes keep + the committed metadata so packed and signed assets stay aligned. + ### v7.0.43 - Budget-friendly CI defaults for gitguardex-managed projects: live workflows drop `push: main`, gate per-PR jobs on `pull_request.draft diff --git a/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/.openspec.yaml b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/.openspec.yaml new file mode 100644 index 00000000..3ac681e3 --- /dev/null +++ b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-06-17 diff --git a/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/design.md b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/design.md new file mode 100644 index 00000000..fe9987cd --- /dev/null +++ b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/design.md @@ -0,0 +1,18 @@ +## Context + +`release-please` owns committed release bumps for the GitHub Actions path, and `.github/workflows/release.yml` already skips `npm publish` when the committed version exists on npm. The missing path is a maintainer running `npm publish` directly from a checkout after the current version has already been published. + +## Decisions + +- Use `prepublishOnly` so direct `npm publish` runs the check before npm creates and uploads the package. +- Query npm for the exact current package version and only mutate files when that version is already published. +- Search patch versions until the first unpublished version so repeated failed manual publishes can recover without guessing the next patch. +- Update `package.json`, root lockfile package metadata, and README release notes to keep release metadata aligned. +- Bump the committed metadata from `7.1.0` to `7.1.1` because the npm registry already contains `7.1.0` and `7.1.1` is currently unpublished. +- Skip by default during GitHub Actions and dry runs. CI release artifacts are generated from the committed package version, so CI should not rewrite the version between packing/signing and publishing. + +## Risks + +- The hook depends on npm registry reachability. It fails closed if the registry lookup cannot distinguish published from unpublished. +- The automatic bump is patch-only and requires the package version to be plain `x.y.z`; non-plain prerelease versions still need an intentional manual release flow. +- The generated README release note is intentionally minimal; maintainers can expand it before committing if a manual publish carries larger release narrative. diff --git a/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/notes.md b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/notes.md new file mode 100644 index 00000000..8f9953d0 --- /dev/null +++ b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/notes.md @@ -0,0 +1,16 @@ +# agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18 (minimal / T1) + +Branch: `agent/codex/auto-bump-package-version-before-npm-pub-2026-06-17-10-18` + +Direct `npm publish` now checks whether the committed package version already exists on npm and bumps to the next unpublished patch version before publishing. The current release metadata is aligned to `7.1.1` because npm already contains `7.1.0`. GitHub Actions and dry runs skip the auto-bump so release artifacts remain tied to committed metadata. + +## Handoff + +- Handoff: change=`agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18`; branch=`agent/codex/auto-bump-package-version-before-npm-pub-2026-06-17-10-18`; scope=`npm publish prepublish version bump`; action=`continue implementation or finish cleanup`. +- Copy prompt: Continue `agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18` on branch `agent/codex/auto-bump-package-version-before-npm-pub-2026-06-17-10-18`. Work inside the existing sandbox, review `openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/notes.md`, continue from the current state instead of creating a new sandbox, and when the work is done run `gx branch finish --branch agent/codex/auto-bump-package-version-before-npm-pub-2026-06-17-10-18 --base main --via-pr --wait-for-merge --cleanup`. + +## Cleanup + +- [ ] Run: `gx branch finish --branch agent/codex/auto-bump-package-version-before-npm-pub-2026-06-17-10-18 --base main --via-pr --wait-for-merge --cleanup` +- [ ] Record PR URL + `MERGED` state in the completion handoff. +- [ ] Confirm sandbox worktree is gone (`git worktree list`, `git branch -a`). diff --git a/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/proposal.md b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/proposal.md new file mode 100644 index 00000000..65b743cb --- /dev/null +++ b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/proposal.md @@ -0,0 +1,23 @@ +## Why + +Direct maintainer `npm publish` attempts fail when `package.json` still names a version that already exists on npm. The repository already has release-please and CI skip logic, but the local publish path still leaves the maintainer to remember a manual patch bump. + +## What Changes + +- Add a `prepublishOnly` lifecycle hook that checks whether the current package version already exists on npm. +- When the exact version is already published, bump `package.json`, `package-lock.json`, and README release notes to the next unpublished patch version before publish continues. +- Align the current release metadata to `7.1.1`, the next unpublished patch after the failed `7.1.0` publish. +- Leave normal release-please and GitHub Actions publish flows anchored to the committed package version unless explicitly overridden. +- Add regression tests for version selection, file updates, skip behavior, and manifest wiring. + +## Capabilities + +### New Capabilities +- None. + +### Modified Capabilities +- `release-workflow`: direct maintainer `npm publish` gains an automatic already-published-version recovery path. + +## Impact + +- Affects `package.json`, `package-lock.json`, `.release-please-manifest.json`, README release notes, the local npm publish lifecycle, a new helper under `scripts/`, metadata tests, and release-workflow OpenSpec requirements. diff --git a/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/specs/release-workflow/spec.md b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/specs/release-workflow/spec.md new file mode 100644 index 00000000..530a2114 --- /dev/null +++ b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/specs/release-workflow/spec.md @@ -0,0 +1,24 @@ +## ADDED Requirements + +### Requirement: Direct npm publish auto-bumps already-published versions +Direct maintainer `npm publish` SHALL check whether the current package version is already present on npm and SHALL bump package release metadata to the next unpublished patch version before publish continues. + +#### Scenario: already-published direct publish version +- **GIVEN** a maintainer runs `npm publish` from the Guardex package checkout +- **AND** the current `package.json` version already exists on npm +- **WHEN** the prepublish lifecycle runs +- **THEN** `package.json` SHALL be updated to the next unpublished patch version +- **AND** `package-lock.json` root package metadata SHALL be updated to the same version +- **AND** README release notes SHALL include the bumped version +- **AND** publish SHALL continue with the bumped package metadata. + +#### Scenario: direct publish version is not published yet +- **GIVEN** a maintainer runs `npm publish` from the Guardex package checkout +- **AND** the current `package.json` version does not exist on npm +- **WHEN** the prepublish lifecycle runs +- **THEN** package release metadata SHALL remain unchanged. + +#### Scenario: CI release publish keeps committed metadata +- **GIVEN** the GitHub Actions release workflow runs `npm publish` +- **WHEN** the prepublish lifecycle runs +- **THEN** the automatic bump SHALL be skipped by default so packed and signed release assets stay aligned with committed package metadata. diff --git a/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/tasks.md b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/tasks.md new file mode 100644 index 00000000..ac3358d2 --- /dev/null +++ b/openspec/changes/agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18/tasks.md @@ -0,0 +1,25 @@ +## 1. Specification + +- [x] 1.1 Define the direct publish auto-bump behavior in `specs/release-workflow/spec.md`. +- [x] 1.2 Capture CI and dry-run skip rationale in `design.md`. + +## 2. Implementation + +- [x] 2.1 Add a `prepublishOnly` lifecycle hook to `package.json`. +- [x] 2.2 Add a publish helper that checks npm, bumps to the next unpublished patch version, and updates `package-lock.json` plus README release notes. +- [x] 2.3 Align committed release metadata to `7.1.1` after confirming npm already has `7.1.0`. +- [x] 2.4 Add regression coverage for version selection, manifest wiring, skip behavior, and file updates. + +## 3. Verification + +- [x] 3.1 Run targeted metadata and prepublish tests. Result: `node --test test/prepublish-bump-version.test.js test/metadata.test.js` passed. +- [ ] 3.2 Run `npm test`. Attempted, but the full suite produced no additional output for several minutes after the TAP header and was stopped with Ctrl-C to avoid leaving a stuck session. +- [x] 3.3 Run OpenSpec validation for this change. Result: `openspec validate agent-codex-auto-bump-package-version-before-npm-pub-2026-06-17-10-18 --type change --strict` passed. +- [x] 3.4 Run full spec validation. Result: `openspec validate --specs` passed with 133 specs. +- [x] 3.5 Run package dry-runs. Result: `env npm_config_cache=/tmp/npm-cache-gitguardex npm pack --dry-run` and `env npm_config_cache=/tmp/npm-cache-gitguardex npm publish --dry-run --access public` both reported `@imdeadpool/guardex@7.1.1`; publish dry-run ran `prepublishOnly` and skipped mutation because it was a dry run. + +## 4. Completion + +- [ ] 4.1 Finish the agent branch via PR merge + cleanup. +- [ ] 4.2 Record PR URL + final `MERGED` state in the completion handoff. +- [ ] 4.3 Confirm sandbox cleanup or capture a `BLOCKED:` handoff if merge/cleanup is pending. diff --git a/package-lock.json b/package-lock.json index 4f5019ec..cce26410 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@imdeadpool/guardex", - "version": "7.1.0", + "version": "7.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@imdeadpool/guardex", - "version": "7.1.0", + "version": "7.1.1", "license": "MIT", "dependencies": { "jsonc-parser": "3.3.1", diff --git a/package.json b/package.json index 31c9ee9b..f2c9c187 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@imdeadpool/guardex", - "version": "7.1.0", + "version": "7.1.1", "description": "Guardian T-Rex for your multi-agent repo. Isolated worktrees, file locks, and PR-only merges stop parallel Codex & Claude agents from overwriting each other's work. Auto-wires Oh My Codex, Oh My Claude, OpenSpec, and Caveman.", "license": "MIT", "preferGlobal": true, @@ -12,6 +12,7 @@ }, "scripts": { "test": "node --test test/*.test.js", + "prepublishOnly": "node scripts/prepublish-bump-version.js", "agent:codex": "bash ./scripts/codex-agent.sh", "agent:branch:start": "bash ./scripts/agent-branch-start.sh", "agent:branch:finish": "bash ./scripts/agent-branch-finish.sh", @@ -40,6 +41,7 @@ "files": [ "bin", "src", + "scripts/prepublish-bump-version.js", "skills", "templates", "README.md", diff --git a/scripts/prepublish-bump-version.js b/scripts/prepublish-bump-version.js new file mode 100644 index 00000000..1862b50b --- /dev/null +++ b/scripts/prepublish-bump-version.js @@ -0,0 +1,191 @@ +#!/usr/bin/env node +'use strict'; + +const cp = require('node:child_process'); +const fs = require('node:fs'); +const path = require('node:path'); + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function writeJson(filePath, value) { + fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`, 'utf8'); +} + +function escapeRegexLiteral(value) { + return String(value).replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function nextPatchVersion(version) { + const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(String(version || '').trim()); + if (!match) { + throw new Error(`Cannot auto-bump non-plain semver version: ${version}`); + } + return `${match[1]}.${match[2]}.${Number(match[3]) + 1}`; +} + +function defaultNpmView(name, version) { + const result = cp.spawnSync('npm', ['view', `${name}@${version}`, 'version', '--json'], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + + if (result.status === 0) { + return { state: 'published' }; + } + + const details = `${result.stdout || ''}\n${result.stderr || ''}`.trim(); + if (/E404|404\s+Not Found|No match found|is not in this registry/i.test(details)) { + return { state: 'unpublished' }; + } + + if (result.error) { + return { state: 'error', message: result.error.message }; + } + + return { state: 'error', message: details || `npm view exited with status ${result.status}` }; +} + +function shouldSkip(env) { + if (env.GUARDEX_SKIP_PUBLISH_BUMP === '1') { + return 'GUARDEX_SKIP_PUBLISH_BUMP=1'; + } + if (env.npm_config_dry_run === 'true') { + return 'npm publish --dry-run'; + } + if (env.GITHUB_ACTIONS === 'true' && env.GUARDEX_ALLOW_PUBLISH_BUMP !== '1') { + return 'GitHub Actions release workflows keep package.json as the release source of truth'; + } + return null; +} + +function findPublishableVersion({ name, version, npmView, maxAttempts = 100 }) { + let candidate = version; + for (let attempt = 0; attempt < maxAttempts; attempt += 1) { + const check = npmView(name, candidate); + if (check.state === 'unpublished') { + return { version: candidate, changed: candidate !== version }; + } + if (check.state !== 'published') { + throw new Error( + `Unable to verify ${name}@${candidate} on npm: ${check.message || `state=${check.state}`}`, + ); + } + candidate = nextPatchVersion(candidate); + } + + throw new Error(`Unable to find an unpublished patch version after ${maxAttempts} attempts`); +} + +function updatePackageLock(repoRoot, name, version) { + const lockPath = path.join(repoRoot, 'package-lock.json'); + if (!fs.existsSync(lockPath)) { + return false; + } + + const lockfile = readJson(lockPath); + lockfile.name = name; + lockfile.version = version; + if (lockfile.packages && lockfile.packages['']) { + lockfile.packages[''].name = name; + lockfile.packages[''].version = version; + } + writeJson(lockPath, lockfile); + return true; +} + +function buildReleaseNote(name, previousVersion, version) { + return ( + `### v${version}\n` + + `- Bumped \`${name}\` from \`${previousVersion}\` to \`${version}\` so direct\n` + + ` \`npm publish\` can continue after \`${previousVersion}\` reached the registry.\n\n` + ); +} + +function updateReadmeReleaseNote(repoRoot, name, previousVersion, version) { + const readmePath = path.join(repoRoot, 'README.md'); + if (!fs.existsSync(readmePath)) { + return false; + } + + const readme = fs.readFileSync(readmePath, 'utf8'); + const headingPattern = new RegExp(`^###\\s+v${escapeRegexLiteral(version)}\\b`, 'm'); + if (headingPattern.test(readme)) { + return false; + } + + const releaseNote = buildReleaseNote(name, previousVersion, version); + const major = String(version).split('.')[0]; + const majorSummaryPattern = new RegExp( + `(v${escapeRegexLiteral(major)}\\.x\\n\\n)`, + ); + if (majorSummaryPattern.test(readme)) { + fs.writeFileSync(readmePath, readme.replace(majorSummaryPattern, `$1${releaseNote}`), 'utf8'); + return true; + } + + if (/## Release notes\n\n/.test(readme)) { + fs.writeFileSync(readmePath, readme.replace(/## Release notes\n\n/, `## Release notes\n\n${releaseNote}`), 'utf8'); + return true; + } + + throw new Error('README.md is missing a Release notes section for the generated publish bump'); +} + +function runPrepublishBump(options = {}) { + const repoRoot = options.repoRoot || path.resolve(__dirname, '..'); + const env = options.env || process.env; + const log = options.log || console.log; + const npmView = options.npmView || defaultNpmView; + const skipReason = shouldSkip(env); + + if (skipReason) { + log(`[guardex] prepublish version bump skipped: ${skipReason}.`); + return { changed: false, reason: 'skipped' }; + } + + const packagePath = path.join(repoRoot, 'package.json'); + const packageJson = readJson(packagePath); + if (!packageJson.name || !packageJson.version) { + throw new Error('package.json must include name and version before publish'); + } + + const next = findPublishableVersion({ + name: packageJson.name, + version: packageJson.version, + npmView, + }); + + if (!next.changed) { + log(`[guardex] ${packageJson.name}@${packageJson.version} is not published yet; keeping package version.`); + return { changed: false, version: packageJson.version }; + } + + const previousVersion = packageJson.version; + packageJson.version = next.version; + writeJson(packagePath, packageJson); + updatePackageLock(repoRoot, packageJson.name, next.version); + updateReadmeReleaseNote(repoRoot, packageJson.name, previousVersion, next.version); + log(`[guardex] bumped ${packageJson.name} from ${previousVersion} to ${next.version} before npm publish.`); + return { changed: true, version: next.version, previousVersion }; +} + +if (require.main === module) { + try { + runPrepublishBump(); + } catch (error) { + console.error(`[guardex] prepublish version bump failed: ${error.message}`); + process.exitCode = 1; + } +} + +module.exports = { + defaultNpmView, + buildReleaseNote, + findPublishableVersion, + nextPatchVersion, + runPrepublishBump, + shouldSkip, + updateReadmeReleaseNote, +}; diff --git a/test/metadata.test.js b/test/metadata.test.js index 09839dc7..7c13a711 100644 --- a/test/metadata.test.js +++ b/test/metadata.test.js @@ -210,6 +210,18 @@ test('package manifest pins runtime and dev dependency versions exactly', () => assert.match(lockfile, /"fast-check": "3\.23\.2"/); }); +test('package publish lifecycle bumps already-published direct npm publish versions', () => { + const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8')); + const scriptPath = path.join(repoRoot, 'scripts', 'prepublish-bump-version.js'); + const scriptSource = fs.readFileSync(scriptPath, 'utf8'); + + assert.equal(pkg.scripts?.prepublishOnly, 'node scripts/prepublish-bump-version.js'); + assert.match(pkg.files.join('\n'), /^scripts\/prepublish-bump-version\.js$/m); + assert.match(scriptSource, /npm view/); + assert.match(scriptSource, /GITHUB_ACTIONS/); + assert.match(scriptSource, /package-lock\.json/); +}); + test('frontend mirror workflow skips cleanly when the mirror PAT is missing', () => { const workflowPath = path.join(repoRoot, '.github', 'workflows', 'sync-frontend-mirror.yml'); const workflow = fs.readFileSync(workflowPath, 'utf8'); diff --git a/test/prepublish-bump-version.test.js b/test/prepublish-bump-version.test.js new file mode 100644 index 00000000..fc631470 --- /dev/null +++ b/test/prepublish-bump-version.test.js @@ -0,0 +1,153 @@ +const test = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); + +const { + findPublishableVersion, + nextPatchVersion, + runPrepublishBump, + shouldSkip, +} = require('../scripts/prepublish-bump-version'); + +function makePackage(version = '1.2.3') { + const repoRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'guardex-prepublish-')); + fs.writeFileSync( + path.join(repoRoot, 'package.json'), + `${JSON.stringify({ name: '@imdeadpool/guardex', version }, null, 2)}\n`, + 'utf8', + ); + fs.writeFileSync( + path.join(repoRoot, 'package-lock.json'), + `${JSON.stringify( + { + name: '@imdeadpool/guardex', + version, + lockfileVersion: 3, + packages: { + '': { + name: '@imdeadpool/guardex', + version, + }, + }, + }, + null, + 2, + )}\n`, + 'utf8', + ); + fs.writeFileSync( + path.join(repoRoot, 'README.md'), + '## Release notes\n\n
\nv7.x\n\n### v7.1.0\n- Existing release.\n', + 'utf8', + ); + return repoRoot; +} + +function readPackage(repoRoot) { + return JSON.parse(fs.readFileSync(path.join(repoRoot, 'package.json'), 'utf8')); +} + +function readLockfile(repoRoot) { + return JSON.parse(fs.readFileSync(path.join(repoRoot, 'package-lock.json'), 'utf8')); +} + +function readReadme(repoRoot) { + return fs.readFileSync(path.join(repoRoot, 'README.md'), 'utf8'); +} + +test('nextPatchVersion increments plain semver patch versions', () => { + assert.equal(nextPatchVersion('7.1.0'), '7.1.1'); + assert.equal(nextPatchVersion('0.0.9'), '0.0.10'); + assert.throws(() => nextPatchVersion('7.1.0-beta.1'), /non-plain semver/); +}); + +test('findPublishableVersion keeps the current version when npm does not have it yet', () => { + const result = findPublishableVersion({ + name: '@imdeadpool/guardex', + version: '7.1.0', + npmView: () => ({ state: 'unpublished' }), + }); + + assert.deepEqual(result, { version: '7.1.0', changed: false }); +}); + +test('findPublishableVersion advances to the next unpublished patch version', () => { + const published = new Set(['7.1.0', '7.1.1']); + const checked = []; + + const result = findPublishableVersion({ + name: '@imdeadpool/guardex', + version: '7.1.0', + npmView: (name, version) => { + checked.push(`${name}@${version}`); + return published.has(version) ? { state: 'published' } : { state: 'unpublished' }; + }, + }); + + assert.deepEqual(checked, [ + '@imdeadpool/guardex@7.1.0', + '@imdeadpool/guardex@7.1.1', + '@imdeadpool/guardex@7.1.2', + ]); + assert.deepEqual(result, { version: '7.1.2', changed: true }); +}); + +test('runPrepublishBump updates package and lockfile when the current version exists on npm', () => { + const repoRoot = makePackage('7.1.0'); + const logs = []; + const result = runPrepublishBump({ + repoRoot, + env: {}, + log: (line) => logs.push(line), + npmView: (_name, version) => (version === '7.1.0' ? { state: 'published' } : { state: 'unpublished' }), + }); + + const pkg = readPackage(repoRoot); + const lockfile = readLockfile(repoRoot); + + assert.equal(result.changed, true); + assert.equal(result.previousVersion, '7.1.0'); + assert.equal(result.version, '7.1.1'); + assert.equal(pkg.version, '7.1.1'); + assert.equal(lockfile.version, '7.1.1'); + assert.equal(lockfile.packages[''].version, '7.1.1'); + assert.match(readReadme(repoRoot), /^### v7\.1\.1\n- Bumped `@imdeadpool\/guardex` from `7\.1\.0` to `7\.1\.1`/m); + assert.match(logs.join('\n'), /bumped @imdeadpool\/guardex from 7\.1\.0 to 7\.1\.1/); +}); + +test('runPrepublishBump leaves files unchanged when the current version is unpublished', () => { + const repoRoot = makePackage('7.1.0'); + const result = runPrepublishBump({ + repoRoot, + env: {}, + log: () => {}, + npmView: () => ({ state: 'unpublished' }), + }); + + assert.equal(result.changed, false); + assert.equal(readPackage(repoRoot).version, '7.1.0'); + assert.equal(readLockfile(repoRoot).version, '7.1.0'); +}); + +test('runPrepublishBump skips dry-run and GitHub Actions publishes by default', () => { + assert.equal(shouldSkip({ npm_config_dry_run: 'true' }), 'npm publish --dry-run'); + assert.match(shouldSkip({ GITHUB_ACTIONS: 'true' }), /GitHub Actions/); + assert.equal(shouldSkip({ GITHUB_ACTIONS: 'true', GUARDEX_ALLOW_PUBLISH_BUMP: '1' }), null); +}); + +test('runPrepublishBump fails closed when npm version lookup cannot be verified', () => { + const repoRoot = makePackage('7.1.0'); + + assert.throws( + () => + runPrepublishBump({ + repoRoot, + env: {}, + log: () => {}, + npmView: () => ({ state: 'error', message: 'registry timeout' }), + }), + /Unable to verify @imdeadpool\/guardex@7\.1\.0 on npm: registry timeout/, + ); +});