mirror of
https://github.com/Kaelio/ktx.git
synced 2026-06-13 08:15:14 +02:00
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-<sanitized> tags, unblocking branch RC publishes that pass through update-public-release- version.mjs.
This commit is contained in:
parent
2667952aa9
commit
c859a22fda
8 changed files with 191 additions and 13 deletions
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue