Skip to content

feat: auto-update CHANGELOG.md on each release (option A) - #18

Merged
fiunchinho merged 4 commits into
mainfrom
auto-update-changelog-md
Jun 3, 2026
Merged

feat: auto-update CHANGELOG.md on each release (option A)#18
fiunchinho merged 4 commits into
mainfrom
auto-update-changelog-md

Conversation

@fiunchinho

Copy link
Copy Markdown
Member

Summary

PoC for option A from the CHANGELOG-auto-update design discussion: extend the push-based auto-release workflow to also keep CHANGELOG.md in sync with each release. After gh release create succeeds, the workflow splices the rendered release notes into CHANGELOG.md and pushes the resulting commit back to the branch.

Validates the design before propagating to muster (#795 in muster repo) and the other migration repos.

Base branch: this PR targets polish-auto-release-template (PR #17 in this repo) so the diff stays focused on the changelog work. Once #17 merges, the base will roll up to main automatically.

Design

Splice, not regenerate. Pre-existing history in CHANGELOG.md (v1.0.0 and earlier) is preserved verbatim — a full git-cliff --output regenerate would only see commits back to where conventional commits started, losing older release-please entries. So we use an insertion-marker approach:

# Changelog

All notable changes to this project will be documented in this file.
...

<!-- auto-release: insert below -->        <-- new sentinel

## 1.0.0 (2026-06-01)                      <-- pre-existing history below, untouched
...

On each release, the workflow:

  1. Renders the new release section (version header + the same Added / Fixed / Changed / Security bullets that go into the GitHub Release) into a temp file.
  2. awk-splices it into CHANGELOG.md directly under the marker, so newest releases land at the top.
  3. Commits with chore: update CHANGELOG.md for vX.Y.Z [skip ci] and pushes.

Loop protection (two layers):

  • [skip ci] in the commit message prevents the auto-release workflow (and CircleCI) from running on the chore commit itself.
  • A new cliff.toml commit_parsers rule (^chore: update CHANGELOG\.md -> skip) ensures that even if the chore commit ends up inside a future --unreleased range, git-cliff filters it out of both the version bump computation and the rendered notes. Placed before the general ^chore rule (first match wins).

Race protection: The concurrency group serializes auto-release jobs against each other but not against human pushes. The commit-back step does git pull --rebase origin "$BRANCH" before git push to handle the narrow window between job start and push. If rebase fails (genuine conflict on CHANGELOG.md, exotic), the step errors out — the release tag and GitHub Release are already created, so nothing is half-finished; the next release re-splices.

Validation plan (the whole point of this PR)

This is the PoC for the muster PR (giantswarm/muster#795). The intent is to land this here first, then validate end-to-end by:

  1. Branch protection: confirm github-actions[bot] is in the bypass list (or adjust as needed) so the push step doesn't 403.
  2. Merge this PR.
  3. Land a conventional commit (feat: / fix:) on main.
  4. Watch the workflow:
    • new tag + GitHub Release created
    • new section appears in CHANGELOG.md directly under the marker
    • chore commit lands with [skip ci] and DOESN'T trigger another auto-release run
  5. Land a second conventional commit.
  6. Confirm the previous release's chore commit is NOT in the new release's notes or version bump (validates the cliff.toml skip rule).

If all green, propagate the same fix to giantswarm/muster#795 status as "tested upstream" and unblock its review.

Required: branch-protection bypass for github-actions[bot]

This repo's main branch protection (from the release-please-approver work) requires PR review — the bot is currently allowed via the app's bypass entitlement, so this should Just Work here. If not, the symptom will be a 403 at git push origin HEAD:main in the commit-back step.

Out of scope

  • Once validated, the workflow + cliff.toml changes become the new source-of-truth template that gets propagated to muster, mcp-kubernetes, klaus-operator, klausctl, and eventually devctl.

Two adjustments needed by repos with stricter yamllint configs (line
length 120 vs test-release-please's 200, or the comments-indentation
warning enabled). Discovered when muster's CI rejected the file copied
straight from here. Mirrors the same change applied to klausctl #258,
muster #792, mcp-kubernetes #450, klaus-operator #135.

Net effect on test-release-please itself: nothing — our yamllint
allows 200 chars and the comment-restructure is purely cosmetic.
This is "source of truth" maintenance: future copies (and the
eventual devctl template) start from the polished version.

Changes:
- Restructure the trailing pull-requests permission comment so the
  explanation sits ABOVE the line rather than wrapping after it.
- Add inline `# yamllint disable-line rule:line-length` immediately
  above the git-cliff download_url (143 chars). URLs don't have
  natural wrap points; the inline disable is the standard escape
  hatch.
Defensive fix from teemow's review of muster#792. If
`git-cliff --unreleased --bump --context` ever returns an empty array
(e.g., theoretical no-bump-warranted-and-nothing-since-last-tag case),
the current `jq -r '.[0].version'` would output the literal string
"null", which slips past both the `[ -z ]` empty-string check and the
`[ = last ]` equality check, and we'd end up attempting
`gh release create null --target SHA`.

Cheap insurance: `// empty` makes jq output nothing (empty string)
instead of "null" when the field is missing. The `[ -z "$NEXT" ]`
guard then catches it correctly.

I couldn't empirically reproduce the empty-array case locally —
git-cliff returned 1 element for every scenario I tried (HEAD at
latest tag, HEAD at root commit, chore-only / style-only / non-
conventional unreleased commits) — but the fix costs us nothing if
the case never happens and saves us from a noisy "null" tag if it
ever does. Trusting the reviewer.
After `gh release create` succeeds, splice the same release-notes body
into CHANGELOG.md and push the resulting commit back to the branch.

- New sentinel comment `<\!-- auto-release: insert below -->` in
  CHANGELOG.md marks the insertion point. Pre-existing history below
  the marker is preserved verbatim. The "frozen as of v1.0.0" preamble
  is replaced to reflect the new auto-managed intent.
- cliff.toml gains a commit_parser skip rule for the auto-generated
  `chore: update CHANGELOG.md` commit so it doesn't trigger spurious
  patch bumps or appear as noise in subsequent releases' notes.
- The commit-back uses `[skip ci]` to prevent re-triggering the
  workflow on itself.
- Pull-rebase before push handles the narrow race window where a
  human merges between job start and commit-back.
@fiunchinho fiunchinho self-assigned this Jun 3, 2026
Base automatically changed from polish-auto-release-template to main June 3, 2026 15:47
@fiunchinho
fiunchinho marked this pull request as ready for review June 3, 2026 15:48
@fiunchinho
fiunchinho merged commit d128ff7 into main Jun 3, 2026
1 check passed
@fiunchinho
fiunchinho deleted the auto-update-changelog-md branch June 3, 2026 15:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant