From a59bd0f50fa878096de7bbe8030f536dbe5113a1 Mon Sep 17 00:00:00 2001 From: lei Date: Tue, 2 Jun 2026 23:46:30 +0300 Subject: [PATCH 1/2] fix(release): generate release notes from unreleased commits, not previous tag Release notes were built with `git-cliff --tag --latest`, but that step runs before the tag is created. With no tag yet, `--latest` selects the most recent existing tag, so each release shipped the previous version's changelog (v1.7.1 showed v1.7.0's entries). Use `--unreleased --tag ` instead: it selects the not-yet-tagged commits and labels the section with the new version. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e2db0c4..53ed48c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,8 +49,11 @@ jobs: run: | # Full changelog for CHANGELOG.md (committed to repo) make changelog VERSION=${{ github.event.inputs.version }} - # Release notes for this version only (passed to goreleaser) - git-cliff --tag ${{ github.event.inputs.version }} --latest --strip header -o RELEASE_NOTES.md + # Release notes for this version only (passed to goreleaser). + # --unreleased (not --latest): the tag is created in a later step, so these + # commits aren't tagged yet. --latest would select the previous tag (v1.7.0), + # which is the bug this fixes. --tag labels the unreleased section with VERSION. + git-cliff --unreleased --tag ${{ github.event.inputs.version }} --strip header -o RELEASE_NOTES.md - name: Commit changelog and tag release run: | From 071715f8162d6fbefd2e9580d940db4746f81977 Mon Sep 17 00:00:00 2001 From: lei Date: Tue, 2 Jun 2026 23:51:28 +0300 Subject: [PATCH 2/2] feat(release): auto-update [Unreleased] changelog on every merge to main Adds an Update Changelog workflow that runs on push to main, regenerates CHANGELOG.md via `make changelog.unreleased` (git-cliff with no tag), and commits the refreshed [Unreleased] section back. A release then promotes [Unreleased] -> [vX.Y.Z] through the existing --tag path. Loop/no-op guards: - auto-commit uses `chore(changelog): ... [skip ci]` so its own push does not re-trigger the workflow - release commit gains `[skip ci]` so a release push doesn't fire a redundant changelog run - cliff.toml skips `chore(changelog)` and `chore(release)` so neither bookkeeping commit appears as a changelog entry - concurrency group serializes rapid merges to avoid push races Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/changelog.yml | 45 +++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 4 ++- Makefile | 5 +++- cliff.toml | 1 + 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 .github/workflows/changelog.yml diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..ff949d8 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,45 @@ +name: Update Changelog + +# Keeps the [Unreleased] section of CHANGELOG.md current as work lands on main. +# A release (release.yml) later promotes [Unreleased] -> [vX.Y.Z] via --tag. +on: + push: + branches: [main] + +permissions: + contents: write + +# Serialize so two quick merges can't race on pushing CHANGELOG.md back to main. +concurrency: + group: changelog-main + cancel-in-progress: false + +jobs: + changelog: + name: Update Changelog + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + token: ${{ secrets.RELEASE_TOKEN }} + fetch-depth: 0 + + - name: Install git-cliff + run: pip install git-cliff + + - name: Regenerate unreleased changelog + run: make changelog.unreleased + + - name: Commit if changed + run: | + if git diff --quiet -- CHANGELOG.md; then + echo "CHANGELOG.md already up to date" + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add CHANGELOG.md + # [skip ci] stops this push from re-triggering the workflow (loop guard). + git commit -m "chore(changelog): update unreleased [skip ci]" + git push origin HEAD:main diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 53ed48c..b26e128 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -62,7 +62,9 @@ jobs: git config user.email "github-actions[bot]@users.noreply.github.com" git remote set-url origin https://x-access-token:${{ secrets.RELEASE_TOKEN }}@github.com/${{ github.repository }} git add CHANGELOG.md - git commit -m "chore(release): update changelog for ${VERSION}" + # [skip ci]: the changelog already reflects this release; no need to + # re-trigger the Update Changelog workflow on this push. + git commit -m "chore(release): update changelog for ${VERSION} [skip ci]" git tag "${VERSION}" git push origin main --tags diff --git a/Makefile b/Makefile index b2cc232..d01f6c9 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ OUTPUT_DIR ?= bin -.PHONY: all build clean lint lint.fix security test test.integration test-s3-integration fmt changelog hooks.install pre-commit help +.PHONY: all build clean lint lint.fix security test test.integration test-s3-integration fmt changelog changelog.unreleased hooks.install pre-commit help ## Build ------------------------------------------------------------------- @@ -49,6 +49,9 @@ ifndef VERSION endif @git-cliff --tag $(VERSION) -o CHANGELOG.md +changelog.unreleased: ## Regenerate CHANGELOG.md with pending commits under [Unreleased] + @git-cliff -o CHANGELOG.md + ## Git Hooks --------------------------------------------------------------- hooks.install: ## Configure git to use githooks/ as the hooks directory diff --git a/cliff.toml b/cliff.toml index 224a3ef..7e553c3 100644 --- a/cliff.toml +++ b/cliff.toml @@ -32,4 +32,5 @@ topo_order = false sort_commits = "oldest" commit_parsers = [ { message = "^chore\\(release\\)", skip = true }, + { message = "^chore\\(changelog\\)", skip = true }, ]