fix: prevent stable release pushes to main (#145)

This commit is contained in:
Andrey Avtomonov 2026-05-19 16:01:07 +02:00 committed by GitHub
parent 7110aa6f5c
commit 03e2f9f0a3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 51 additions and 12 deletions

View file

@ -3,7 +3,7 @@
This runbook covers the maintainer workflow for publishing `@kaelio/ktx` to
npm through GitHub Actions. The workflow uses semantic-release to choose the
next version, update release metadata, publish the package, create the GitHub
release, and commit the release files back to the repository.
release, and commit prerelease files back to the `next` branch.
## Release channels
@ -91,8 +91,9 @@ Publish a stable release from `main` after you have validated an rc package.
7. Run the workflow.
The workflow publishes `@kaelio/ktx` with `--access public --tag latest`, runs
the published package smoke test, creates a GitHub release, and commits the
release metadata.
the published package smoke test, and creates a GitHub release. Stable releases
don't commit release metadata back to `main`, because `main` is protected and
requires changes through pull requests.
## Release metadata
@ -105,7 +106,8 @@ prepare step. That script updates:
The artifact packaging and readiness scripts read `publicNpmPackageVersion`
from `release-policy.json`, so manual version edits in build scripts aren't
needed for rc releases.
needed for rc releases. Stable releases use the updated metadata during the
workflow run, but that generated metadata isn't committed back to `main`.
The bundled Python runtime wheel also derives its version from
`publicNpmPackageVersion`. Stable npm versions are reused as-is, and rc

View file

@ -90,6 +90,26 @@ function releaseTag(kind) {
return kind === 'rc' ? 'next' : 'latest';
}
function releaseChangelogPlugins(kind) {
return kind === 'rc' ? ['@semantic-release/changelog'] : [];
}
function releaseGitPlugins(kind) {
if (kind !== 'rc') {
return [];
}
return [
[
'@semantic-release/git',
{
assets: ['CHANGELOG.md', 'package.json', 'release-policy.json'],
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
},
],
];
}
function releaseBranches(env = process.env) {
const branch = currentBranch(env);
const kind = releaseKind(env);
@ -138,7 +158,7 @@ function createReleaseConfig(env = process.env) {
},
],
'./scripts/semantic-release-version-policy.cjs',
'@semantic-release/changelog',
...releaseChangelogPlugins(kind),
[
'@semantic-release/exec',
{
@ -153,13 +173,7 @@ function createReleaseConfig(env = process.env) {
].join(' && '),
},
],
[
'@semantic-release/git',
{
assets: ['CHANGELOG.md', 'package.json', 'release-policy.json'],
message: 'chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}',
},
],
...releaseGitPlugins(kind),
[
'@semantic-release/github',
{

View file

@ -9,6 +9,14 @@ function releaseExecOptions(config) {
return config.plugins.find((plugin) => Array.isArray(plugin) && plugin[0] === '@semantic-release/exec' && plugin[1].prepareCmd)[1];
}
function releaseExecIndex(config) {
return config.plugins.findIndex((plugin) => Array.isArray(plugin) && plugin[0] === '@semantic-release/exec' && plugin[1].prepareCmd);
}
function pluginNames(config) {
return config.plugins.map((plugin) => (Array.isArray(plugin) ? plugin[0] : plugin));
}
describe('semantic-release config', () => {
it('configures rc releases on a dedicated next prerelease branch', () => {
assert.equal(releaseKind({ KTX_RELEASE_KIND: 'rc' }), 'rc');
@ -23,6 +31,14 @@ describe('semantic-release config', () => {
releaseExecOptions(config).prepareCmd,
/update-public-release-version\.mjs "\$\{nextRelease\.version\}" "next"/,
);
const releaseFilePluginNames = pluginNames(config).filter(
(plugin) => plugin === '@semantic-release/changelog' || plugin === '@semantic-release/git',
);
assert.deepEqual(releaseFilePluginNames, ['@semantic-release/changelog', '@semantic-release/git']);
const names = pluginNames(config);
assert.ok(names.indexOf('@semantic-release/changelog') < releaseExecIndex(config));
assert.ok(names.indexOf('@semantic-release/git') > releaseExecIndex(config));
});
it('configures stable releases only from main with latest tag', () => {
@ -38,6 +54,13 @@ describe('semantic-release config', () => {
assert.ok(config.plugins.includes('./scripts/semantic-release-version-policy.cjs'));
});
it('does not commit release files back to protected main during stable releases', () => {
const config = createReleaseConfig({ KTX_RELEASE_KIND: 'stable', GITHUB_REF_NAME: 'main' });
assert.equal(pluginNames(config).includes('@semantic-release/git'), false);
assert.equal(pluginNames(config).includes('@semantic-release/changelog'), false);
});
it('rejects stable releases from non-main branches', () => {
assert.throws(
() => releaseBranches({ KTX_RELEASE_KIND: 'stable', GITHUB_REF_NAME: 'feature/release-test' }),