ci(release): keep the changelog out of a protected branch
CI / check (push) Skipped
CI / e2e (push) Skipped

main is protected (enable_push: false, empty whitelist), so
@semantic-release/git's commit-back is rejected by the pre-receive
hook -- and it would be rejected *after* the tag was pushed, leaving a
tagged release the run then reports as failed. Found by trying to push
this branch to main.

Whitelisting the CI user was the alternative and is declined: it
weakens a protection someone set deliberately and lets a bot push to
main without the checks every human PR has to pass.

So the release page is the changelog. The changelog plugin now writes a
gitignored .release-notes.md, which exists only to carry the notes into
gitea-release.sh without interpolating them into a shell command, and
CHANGELOG.md is a signpost -- a file claiming to be a changelog while
silently never updating is worse than no file.

Tags are not protected, so the tag push is unaffected.
This commit is contained in:
t
2026-08-17 19:46:48 -04:00
committed by logan
parent 9ce79ee416
commit 8d5d8af297
7 changed files with 89 additions and 37 deletions
+25 -16
View File
@@ -77,32 +77,41 @@ plugins:
section: Build
hidden: true
# Write CHANGELOG.md.
# Render the notes to a file.
#
# This plugin is load-bearing for more than the changelog: it is how the
# release notes reach the Gitea API *without being interpolated into a
# shell command*. Release notes are rendered commit messages — arbitrary
# **This plugin is here to carry the notes, not to maintain a document.**
# It is how they reach the Gitea API *without being interpolated into a
# shell command*: release notes are rendered commit messages — arbitrary
# text carrying backticks, quotes and `$` — so templating
# ${nextRelease.notes} into `publishCmd` would be a shell injection with
# the commit log as its input. scripts/gitea-release.sh reads the top
# section of this file instead, and the only thing interpolated below is
# a semver string.
#
# The target is a gitignored build artifact rather than CHANGELOG.md,
# because nothing commits it back — see below.
- - "@semantic-release/changelog"
- changelogFile: CHANGELOG.md
changelogTitle: "# Changelog"
- changelogFile: .release-notes.md
changelogTitle: "# Release notes"
# Create the Gitea release, whose body is that changelog section.
# Create the Gitea release, whose body is that section.
# `publish` runs after `prepare`, so the tag already exists by here.
- - "@semantic-release/exec"
- publishCmd: "./scripts/gitea-release.sh ${nextRelease.version}"
# Commit the changelog back to main.
# **There is deliberately no @semantic-release/git here.**
#
# This is the push that would otherwise re-enter release.yml; the
# workflow guards on this subject rather than trusting [skip ci], whose
# handling in Gitea is one more thing that would have to be verified.
# The subject must also satisfy scripts/commit-check.sh.
- - "@semantic-release/git"
- assets:
- CHANGELOG.md
message: "chore(release): ${nextRelease.version} [skip ci]"
# `main` is a protected branch with `enable_push: false` and an empty
# push whitelist, so a changelog commit-back would be rejected by the
# pre-receive hook — *after* the tag had already been pushed, leaving a
# tagged release the run then reported as failed. The alternative was to
# whitelist the CI user, which weakens a protection someone set on
# purpose and lets a bot push to main without passing the checks every
# human PR has to.
#
# So the release page is the changelog. Tags are not protected, so the
# tag push semantic-release does itself is unaffected. CHANGELOG.md in
# the repo is a signpost to the releases page and is not written by any
# of this; a file that claimed to be a changelog and silently stopped
# updating would be worse than no file at all.