From 2f70861a1855f3734346ef3da2060ee7b3619836 Mon Sep 17 00:00:00 2001 From: Andrey Avtomonov Date: Wed, 20 May 2026 17:01:26 +0200 Subject: [PATCH] feat(release): one version everywhere via @semantic-release/git (#186) * feat(release): commit version files back to branch for one-version-everywhere Add @semantic-release/git to the release plugin chain so the bumped package.json, release-policy.json, and packages/cli/package.json land back on the release branch after publish. This keeps the published npm version and the in-repo version files in sync, so local builds from main report the released version (e.g. ktx --version and the daemon /health endpoint via KTX_DAEMON_VERSION). Also widens assertPublicNpmReleaseTag to accept branch- tags, unblocking branch RC publishes that pass through update-public-release- version.mjs. * test(release): pin GITHUB_REF_NAME in main-rc releaseTag assertion The bare releaseTag('rc') call defaulted to process.env.GITHUB_REF_NAME, which on PR CI is the merge ref (e.g. 186/merge) and yields 'branch-186-merge' instead of 'next'. Pass an explicit { GITHUB_REF_NAME: 'main' } so the test exercises the main-rc path regardless of CI env. --- package.json | 1 + pnpm-lock.yaml | 76 +++++++++++++++++++ scripts/public-npm-release-metadata.mjs | 8 +- scripts/public-npm-release-metadata.test.mjs | 21 ++++- scripts/semantic-release-config.cjs | 12 +++ scripts/semantic-release-config.test.mjs | 57 +++++++++++--- scripts/update-public-release-version.mjs | 5 ++ .../update-public-release-version.test.mjs | 26 +++++++ 8 files changed, 192 insertions(+), 14 deletions(-) diff --git a/package.json b/package.json index 7d1b054a..3beebcb2 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "@biomejs/biome": "^2.4.15", "@semantic-release/commit-analyzer": "^13.0.1", "@semantic-release/exec": "^7.1.0", + "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^12.0.8", "@semantic-release/npm": "^13.1.5", "@semantic-release/release-notes-generator": "^14.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9589b831..ac2398ca 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: '@semantic-release/exec': specifier: ^7.1.0 version: 7.1.0(semantic-release@25.0.3(typescript@6.0.3)) + '@semantic-release/git': + specifier: ^10.0.1 + version: 10.0.1(semantic-release@25.0.3(typescript@6.0.3)) '@semantic-release/github': specifier: ^12.0.8 version: 12.0.8(semantic-release@25.0.3(typescript@6.0.3)) @@ -2333,6 +2336,10 @@ packages: peerDependencies: semantic-release: '>=20.1.0' + '@semantic-release/error@3.0.0': + resolution: {integrity: sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==} + engines: {node: '>=14.17'} + '@semantic-release/error@4.0.0': resolution: {integrity: sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==} engines: {node: '>=18'} @@ -2343,6 +2350,12 @@ packages: peerDependencies: semantic-release: '>=24.1.0' + '@semantic-release/git@10.0.1': + resolution: {integrity: sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==} + engines: {node: '>=14.17'} + peerDependencies: + semantic-release: '>=18.0.0' + '@semantic-release/github@12.0.8': resolution: {integrity: sha512-tej5AAgK5X9wHRoDmYhecMXEHEkFeGOY1XsEblKxu8pIQwahzf1STYyr7iPU6Lpbg6C5I3N2w/ocXrBo+L7jhw==} engines: {node: ^22.14.0 || >= 24.10.0} @@ -3608,6 +3621,10 @@ packages: resolution: {integrity: sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==} engines: {node: '>=18.0.0'} + execa@5.1.1: + resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} + engines: {node: '>=10'} + execa@8.0.1: resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==} engines: {node: '>=16.17'} @@ -4087,6 +4104,10 @@ packages: resolution: {integrity: sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==} engines: {node: '>= 20'} + human-signals@2.1.0: + resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} + engines: {node: '>=10.17.0'} + human-signals@5.0.0: resolution: {integrity: sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==} engines: {node: '>=16.17.0'} @@ -4467,6 +4488,9 @@ packages: lodash.uniqby@4.7.0: resolution: {integrity: sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==} + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + logform@2.7.0: resolution: {integrity: sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==} engines: {node: '>= 12.0.0'} @@ -4883,6 +4907,10 @@ packages: resolution: {integrity: sha512-z9nC87iaZXXySbWWtTHfCFJyFvKaUAW6lODhikG7ILSbVgmwuFjUqkgnheHvAUcGedO29e2QGBRXMUD64aurqQ==} engines: {node: '>=20'} + npm-run-path@4.0.1: + resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} + engines: {node: '>=8'} + npm-run-path@5.3.0: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -5055,6 +5083,10 @@ packages: resolution: {integrity: sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==} engines: {node: '>=18'} + p-reduce@2.1.0: + resolution: {integrity: sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==} + engines: {node: '>=8'} + p-reduce@3.0.0: resolution: {integrity: sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==} engines: {node: '>=12'} @@ -5669,6 +5701,10 @@ packages: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} + strip-final-newline@2.0.0: + resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} + engines: {node: '>=6'} + strip-final-newline@3.0.0: resolution: {integrity: sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==} engines: {node: '>=12'} @@ -8530,6 +8566,8 @@ snapshots: transitivePeerDependencies: - supports-color + '@semantic-release/error@3.0.0': {} + '@semantic-release/error@4.0.0': {} '@semantic-release/exec@7.1.0(semantic-release@25.0.3(typescript@6.0.3))': @@ -8544,6 +8582,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@semantic-release/git@10.0.1(semantic-release@25.0.3(typescript@6.0.3))': + dependencies: + '@semantic-release/error': 3.0.0 + aggregate-error: 3.1.0 + debug: 4.4.3 + dir-glob: 3.0.1 + execa: 5.1.1 + lodash: 4.18.1 + micromatch: 4.0.8 + p-reduce: 2.1.0 + semantic-release: 25.0.3(typescript@6.0.3) + transitivePeerDependencies: + - supports-color + '@semantic-release/github@12.0.8(semantic-release@25.0.3(typescript@6.0.3))': dependencies: '@octokit/core': 7.0.6 @@ -9968,6 +10020,18 @@ snapshots: dependencies: eventsource-parser: 3.0.8 + execa@5.1.1: + dependencies: + cross-spawn: 7.0.6 + get-stream: 6.0.1 + human-signals: 2.1.0 + is-stream: 2.0.1 + merge-stream: 2.0.0 + npm-run-path: 4.0.1 + onetime: 5.1.2 + signal-exit: 3.0.7 + strip-final-newline: 2.0.0 + execa@8.0.1: dependencies: cross-spawn: 7.0.6 @@ -10564,6 +10628,8 @@ snapshots: transitivePeerDependencies: - supports-color + human-signals@2.1.0: {} + human-signals@5.0.0: {} human-signals@8.0.1: {} @@ -10903,6 +10969,8 @@ snapshots: lodash.uniqby@4.7.0: {} + lodash@4.18.1: {} + logform@2.7.0: dependencies: '@colors/colors': 1.6.0 @@ -11589,6 +11657,10 @@ snapshots: normalize-url@9.0.0: {} + npm-run-path@4.0.1: + dependencies: + path-key: 3.1.1 + npm-run-path@5.3.0: dependencies: path-key: 4.0.0 @@ -11732,6 +11804,8 @@ snapshots: p-map@7.0.4: {} + p-reduce@2.1.0: {} + p-reduce@3.0.0: {} p-timeout@6.1.4: {} @@ -12525,6 +12599,8 @@ snapshots: strip-bom@3.0.0: {} + strip-final-newline@2.0.0: {} + strip-final-newline@3.0.0: {} strip-final-newline@4.0.0: {} diff --git a/scripts/public-npm-release-metadata.mjs b/scripts/public-npm-release-metadata.mjs index acc77c7e..6aeba682 100644 --- a/scripts/public-npm-release-metadata.mjs +++ b/scripts/public-npm-release-metadata.mjs @@ -6,6 +6,7 @@ import { fileURLToPath } from 'node:url'; export const PUBLIC_NPM_PACKAGE_NAME = '@kaelio/ktx'; export const PUBLIC_NPM_RELEASE_TAGS = new Set(['latest', 'next']); +export const PUBLIC_NPM_BRANCH_RELEASE_TAG_PATTERN = /^branch-[a-z0-9]+(?:-[a-z0-9]+)*$/; const SEMVER_PATTERN = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/; @@ -56,10 +57,13 @@ export function publicNpmPackageVersionToPythonVersion(version) { } export function assertPublicNpmReleaseTag(tag) { - if (!PUBLIC_NPM_RELEASE_TAGS.has(tag)) { + if (typeof tag !== 'string') { throw new Error(`Invalid public npm release tag: ${tag}`); } - return tag; + if (PUBLIC_NPM_RELEASE_TAGS.has(tag) || PUBLIC_NPM_BRANCH_RELEASE_TAG_PATTERN.test(tag)) { + return tag; + } + throw new Error(`Invalid public npm release tag: ${tag}`); } export function readPublicNpmReleaseMetadata(rootDir = scriptRootDir()) { diff --git a/scripts/public-npm-release-metadata.test.mjs b/scripts/public-npm-release-metadata.test.mjs index 3f102ff1..49299d57 100644 --- a/scripts/public-npm-release-metadata.test.mjs +++ b/scripts/public-npm-release-metadata.test.mjs @@ -1,7 +1,7 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { publicNpmPackageVersionToPythonVersion } from './public-npm-release-metadata.mjs'; +import { assertPublicNpmReleaseTag, publicNpmPackageVersionToPythonVersion } from './public-npm-release-metadata.mjs'; describe('publicNpmPackageVersionToPythonVersion', () => { it('keeps stable public npm versions unchanged for Python wheels', () => { @@ -24,3 +24,22 @@ describe('publicNpmPackageVersionToPythonVersion', () => { ); }); }); + +describe('assertPublicNpmReleaseTag', () => { + it('accepts the canonical latest and next tags', () => { + assert.equal(assertPublicNpmReleaseTag('latest'), 'latest'); + assert.equal(assertPublicNpmReleaseTag('next'), 'next'); + }); + + it('accepts branch-prefixed release tags produced by branch RC publishes', () => { + assert.equal(assertPublicNpmReleaseTag('branch-feature-foo'), 'branch-feature-foo'); + assert.equal(assertPublicNpmReleaseTag('branch-rel-1-2-3'), 'branch-rel-1-2-3'); + assert.equal(assertPublicNpmReleaseTag('branch-x'), 'branch-x'); + }); + + it('rejects malformed or non-string tags', () => { + for (const bad of ['', 'BRANCH-x', 'branch-', 'branch--foo', 'branch_foo', 'beta', null, undefined, 42]) { + assert.throws(() => assertPublicNpmReleaseTag(bad), /Invalid public npm release tag/); + } + }); +}); diff --git a/scripts/semantic-release-config.cjs b/scripts/semantic-release-config.cjs index 31b3e5e2..2f7c3426 100644 --- a/scripts/semantic-release-config.cjs +++ b/scripts/semantic-release-config.cjs @@ -161,6 +161,18 @@ function createReleaseConfig(env = process.env) { 'pnpm run artifacts:check', 'pnpm run release:readiness', ].join(' && '), + }, + ], + [ + '@semantic-release/git', + { + assets: ['package.json', 'release-policy.json', 'packages/cli/package.json'], + message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}', + }, + ], + [ + '@semantic-release/exec', + { publishCmd: [ `npm publish dist/artifacts/npm/kaelio-ktx-\${nextRelease.version}.tgz --tag ${tag} --access public --provenance`, 'pnpm run release:published-smoke', diff --git a/scripts/semantic-release-config.test.mjs b/scripts/semantic-release-config.test.mjs index 4fb26d42..56c9e88f 100644 --- a/scripts/semantic-release-config.test.mjs +++ b/scripts/semantic-release-config.test.mjs @@ -5,10 +5,19 @@ import { describe, it } from 'node:test'; const require = createRequire(import.meta.url); const { createReleaseConfig, releaseBranches, releaseKind, releaseTag } = require('./semantic-release-config.cjs'); -function releaseExecOptions(config) { +function prepareExecOptions(config) { return config.plugins.find((plugin) => Array.isArray(plugin) && plugin[0] === '@semantic-release/exec' && plugin[1].prepareCmd)[1]; } +function publishExecOptions(config) { + return config.plugins.find((plugin) => Array.isArray(plugin) && plugin[0] === '@semantic-release/exec' && plugin[1].publishCmd)[1]; +} + +function gitPluginOptions(config) { + const found = config.plugins.find((plugin) => Array.isArray(plugin) && plugin[0] === '@semantic-release/git'); + return found ? found[1] : undefined; +} + function pluginNames(config) { return config.plugins.map((plugin) => (Array.isArray(plugin) ? plugin[0] : plugin)); } @@ -16,7 +25,7 @@ function pluginNames(config) { describe('semantic-release config', () => { it('configures rc releases as a prerelease on main', () => { assert.equal(releaseKind({ KTX_RELEASE_KIND: 'rc' }), 'rc'); - assert.equal(releaseTag('rc'), 'next'); + assert.equal(releaseTag('rc', { GITHUB_REF_NAME: 'main' }), 'next'); assert.deepEqual(releaseBranches({ KTX_RELEASE_KIND: 'rc', GITHUB_REF_NAME: 'main' }), [ { name: 'main', prerelease: 'rc', channel: 'next' }, ]); @@ -28,14 +37,14 @@ describe('semantic-release config', () => { '@semantic-release/npm must not run; the exec publishCmd publishes the pre-built tarball', ); assert.match( - releaseExecOptions(config).prepareCmd, + prepareExecOptions(config).prepareCmd, /update-public-release-version\.mjs "\$\{nextRelease\.version\}" "next"/, ); assert.match( - releaseExecOptions(config).publishCmd, + publishExecOptions(config).publishCmd, /^npm publish dist\/artifacts\/npm\/kaelio-ktx-\$\{nextRelease\.version\}\.tgz --tag next --access public --provenance/, ); - assert.match(releaseExecOptions(config).publishCmd, /pnpm run release:published-smoke/); + assert.match(publishExecOptions(config).publishCmd, /pnpm run release:published-smoke/); assert.doesNotMatch(JSON.stringify(config.plugins), /release:npm-publish/); }); @@ -48,11 +57,11 @@ describe('semantic-release config', () => { const config = createReleaseConfig({ KTX_RELEASE_KIND: 'rc', GITHUB_REF_NAME: 'feature/branch-release' }); assert.match( - releaseExecOptions(config).prepareCmd, + prepareExecOptions(config).prepareCmd, /update-public-release-version\.mjs "\$\{nextRelease\.version\}" "branch-feature-branch-release"/, ); assert.match( - releaseExecOptions(config).publishCmd, + publishExecOptions(config).publishCmd, /^npm publish dist\/artifacts\/npm\/kaelio-ktx-\$\{nextRelease\.version\}\.tgz --tag branch-feature-branch-release --access public --provenance/, ); }); @@ -64,24 +73,50 @@ describe('semantic-release config', () => { const config = createReleaseConfig({ KTX_RELEASE_KIND: 'stable', GITHUB_REF_NAME: 'main' }); assert.match( - releaseExecOptions(config).prepareCmd, + prepareExecOptions(config).prepareCmd, /update-public-release-version\.mjs "\$\{nextRelease\.version\}" "latest"/, ); assert.match( - releaseExecOptions(config).publishCmd, + publishExecOptions(config).publishCmd, /^npm publish dist\/artifacts\/npm\/kaelio-ktx-\$\{nextRelease\.version\}\.tgz --tag latest --access public --provenance/, ); assert.equal(config.plugins.includes('./scripts/semantic-release-version-policy.cjs'), false); }); - it('never commits release files back to the repo', () => { + it('commits release version files back to the branch via @semantic-release/git', () => { for (const kind of ['rc', 'stable']) { const config = createReleaseConfig({ KTX_RELEASE_KIND: kind, GITHUB_REF_NAME: 'main' }); - assert.equal(pluginNames(config).includes('@semantic-release/git'), false, `${kind}: @semantic-release/git`); + const git = gitPluginOptions(config); + assert.ok(git, `${kind}: @semantic-release/git plugin must be configured`); + assert.deepEqual(git.assets, ['package.json', 'release-policy.json', 'packages/cli/package.json']); + assert.match(git.message, /^chore\(release\): \$\{nextRelease\.version\} \[skip ci\]/); + } + }); + + it('keeps @semantic-release/npm and @semantic-release/changelog out of the plugin chain', () => { + for (const kind of ['rc', 'stable']) { + const config = createReleaseConfig({ KTX_RELEASE_KIND: kind, GITHUB_REF_NAME: 'main' }); + assert.equal(pluginNames(config).includes('@semantic-release/npm'), false, `${kind}: @semantic-release/npm`); assert.equal(pluginNames(config).includes('@semantic-release/changelog'), false, `${kind}: @semantic-release/changelog`); } }); + it('orders the prepare exec before @semantic-release/git before the publish exec', () => { + const config = createReleaseConfig({ KTX_RELEASE_KIND: 'stable', GITHUB_REF_NAME: 'main' }); + const prepareIndex = config.plugins.findIndex( + (plugin) => Array.isArray(plugin) && plugin[0] === '@semantic-release/exec' && plugin[1].prepareCmd, + ); + const gitIndex = config.plugins.findIndex( + (plugin) => Array.isArray(plugin) && plugin[0] === '@semantic-release/git', + ); + const publishIndex = config.plugins.findIndex( + (plugin) => Array.isArray(plugin) && plugin[0] === '@semantic-release/exec' && plugin[1].publishCmd, + ); + assert.ok(prepareIndex !== -1 && gitIndex !== -1 && publishIndex !== -1); + assert.ok(prepareIndex < gitIndex, 'prepare exec must run before @semantic-release/git'); + assert.ok(gitIndex < publishIndex, '@semantic-release/git must run before the publish exec'); + }); + it('produces a loadable config regardless of GITHUB_REF_NAME', () => { // Knip and other tooling load .releaserc.cjs on PR runners where // GITHUB_REF_NAME is the merge ref. semantic-release itself enforces the diff --git a/scripts/update-public-release-version.mjs b/scripts/update-public-release-version.mjs index 27adbe2d..0df2d234 100644 --- a/scripts/update-public-release-version.mjs +++ b/scripts/update-public-release-version.mjs @@ -32,6 +32,11 @@ export async function updatePublicReleaseVersion(rootDir, version, tag) { packageJson.version = safeVersion; await writeJson(packageJsonPath, packageJson); + const cliPackageJsonPath = join(rootDir, 'packages', 'cli', 'package.json'); + const cliPackageJson = await readJson(cliPackageJsonPath); + cliPackageJson.version = safeVersion; + await writeJson(cliPackageJsonPath, cliPackageJson); + const policyPath = releasePolicyPath(rootDir); const policy = await readJson(policyPath); policy.publicNpmPackageVersion = safeVersion; diff --git a/scripts/update-public-release-version.test.mjs b/scripts/update-public-release-version.test.mjs index 1bf68eee..3ddd67f2 100644 --- a/scripts/update-public-release-version.test.mjs +++ b/scripts/update-public-release-version.test.mjs @@ -21,6 +21,11 @@ async function writeReleaseFixture(root) { version: '0.0.0-private', private: true, }); + await writeJson(join(root, 'packages', 'cli', 'package.json'), { + name: '@ktx/cli', + version: '0.0.0-private', + private: true, + }); await writeJson(join(root, 'release-policy.json'), { schemaVersion: 1, publicNpmPackageVersion: '0.1.0-rc.1', @@ -60,6 +65,7 @@ describe('updatePublicReleaseVersion', () => { await updatePublicReleaseVersion(root, '0.1.0-rc.2', 'next'); assert.equal((await readJson(join(root, 'package.json'))).version, '0.1.0-rc.2'); + assert.equal((await readJson(join(root, 'packages', 'cli', 'package.json'))).version, '0.1.0-rc.2'); assert.deepEqual(await readJson(join(root, 'release-policy.json')), { schemaVersion: 1, publicNpmPackageVersion: '0.1.0-rc.2', @@ -93,6 +99,26 @@ describe('updatePublicReleaseVersion', () => { } }); + it('accepts branch-prefixed npm release tags produced by branch RC publishes', async () => { + const root = await mkdtemp(join(tmpdir(), 'ktx-release-version-branch-test-')); + try { + await writeReleaseFixture(root); + + await updatePublicReleaseVersion(root, '0.1.0-feature-foo.0', 'branch-feature-foo'); + + assert.equal((await readJson(join(root, 'package.json'))).version, '0.1.0-feature-foo.0'); + assert.equal( + (await readJson(join(root, 'packages', 'cli', 'package.json'))).version, + '0.1.0-feature-foo.0', + ); + const policy = await readJson(join(root, 'release-policy.json')); + assert.equal(policy.publicNpmPackageVersion, '0.1.0-feature-foo.0'); + assert.equal(policy.npm.tag, 'branch-feature-foo'); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + it('rejects invalid versions and tags', async () => { const root = await mkdtemp(join(tmpdir(), 'ktx-release-version-invalid-test-')); try {