Skip to content

ci: group and filter the release changelog, and stamp an install footer - #50

Merged
stanlyzoolo merged 3 commits into
mainfrom
worktree-release-changelog
Jul 30, 2026
Merged

ci: group and filter the release changelog, and stamp an install footer#50
stanlyzoolo merged 3 commits into
mainfrom
worktree-release-changelog

Conversation

@stanlyzoolo

Copy link
Copy Markdown
Owner

Release notes were a flat commit list — v0.1.0's body was written by hand. Two edits to .goreleaser.yaml plus a commit-discipline note in CLAUDE.md.

Changelog groups

Keeps use: github and sort: asc (alphabetical by message, not chronological — scoped subjects like feat(model): cluster inside their group). Excludes docs/test/chore/ci/build, merge commits and go mod tidy. Three groups: New features (feat), Bug fixes (fix), and Other changes with no regexp — the catch-all that keeps refactor:/perf:/rebrand: visible instead of dropping them.

The two regexp shapes are not interchangeable and this is the part worth reviewing: a group regexp matches the rendered line, which use: github prefixes with the SHA, hence the leading ^.*?; an exclude matches the raw subject and anchors at ^docs. Swap them and either the groups match nothing or the excludes match far too much.

Install footer

Goes on the existing release: key — a second release: is a YAML unmarshal error (mapping key "release" already defined). {{ .Tag }} is the only template expression.

Inline code, not a fenced block: this body is also what keepkit renders in panel [2], where mdInline strips backticks and the three install paths read as one sentence. A fence would split that sentence across three plated blocks.

Verification

Rendered the changelog against real history (31 commits since v0.1.0):

### New features      7 × feat
### Bug fixes        11 × fix
### Other changes     1 × perf(model)

Every docs: and merge commit filtered out. This also confirmed both regexp halves empirically — the SHA prefix is present in the rendered lines, and the ^docs excludes still fired.

goreleaser check exit 0 (only the pre-existing brews deprecation warning). Full preflight green: go build, go vet ./..., go test -race ./... (10 packages), golangci-lint run 0 issues.

goreleaser check does not validate templates, so a footer typo would only surface as a failed release job after the tag is pushed. Closed that gap directly: extracted the footer from the parsed YAML and ran it through text/template with Option("missingkey=error"), so a mistyped field fails hard instead of publishing <no value>. Renders go install github.com/stanlyzoolo/keepkit@v0.1.1.Tag keeps its leading v.

Notes for the reviewer

Four things diverged from the plan (all recorded in docs/plans/completed/20260727-release-changelog-groups.md):

  1. Rebased onto main first. The branch sat on f8ad9d2; main had moved 31 commits ahead and rewritten CLAUDE.md by 89 lines — the exact file this change edits. .goreleaser.yaml was untouched by all 31, so the config work was unaffected.
  2. The plan's local-render recipe cannot work. use: github pulls from the compare API, and a local temp tag doesn't exist on the remote → 404 on compare/v0.1.0...v0.1.1-preview. Pushing a preview tag isn't acceptable, so I rendered through a throwaway copy of the config with use: git. Same filter/group code path, no token, committed config untouched.
  3. Expected render output flipped. The plan predicted an empty changelog because on its original base everything since v0.1.0 was docs/chore. After the rebase it's populated — a stronger check, since all three groups fire at once.
  4. The footer rationale was stale. The plan justified inline code by stripMarkdown mangling fences; e81bfb6 replaced that with markdownToLines, which handles fences properly. Decision unchanged, reason rewritten — verified by probing markdownToLines with the real footer text (## Install → heading with prefix stripped, commands → body with backticks gone).

Still unproven end-to-end: GoReleaser only renders the footer when actually cutting a release, so its publish-time plumbing waits for the next tag. The template itself is checked.

fetch-depth: 0 in release.yml stays — GoReleaser needs full history for the tag range. No other workflow references changelog behavior. README needed no change; both its "changelog" mentions are the TUI's [c] key.

🤖 Generated with Claude Code

stanlyzoolo and others added 3 commits July 29, 2026 23:48
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Release notes assembled themselves from a flat commit list, so v0.1.0's
body was written by hand. GoReleaser now groups the range into New
features (feat), Bug fixes (fix) and a catch-all Other changes, and drops
docs/test/chore/ci/build subjects and merge commits. The catch-all is
what keeps an off-nomenclature subject — refactor, perf, rebrand —
visible instead of silently vanishing.

The two regexp shapes are not interchangeable, which is the one thing to
get right here. A group regexp matches the rendered line, which use:
github prefixes with the commit SHA, so it needs the leading ^.*? — an
exclude pattern is applied to the raw subject and anchors at ^docs.
Confirmed on a real render rather than read off the docs: the SHA prefix
is there and the ^docs excludes still fired.

The Install block v0.1.0 carried by hand moves into release.footer, on
the existing release: key — a second one is a YAML unmarshal error. It
stays inline code rather than a fenced block because this body is also
what panel [2] renders, where mdInline drops backticks and the three
install paths read as one sentence; a fence would split that sentence
across three plated blocks. Note the plan's own reason for that choice
had gone stale: it was written against stripMarkdown, which e81bfb6
replaced with markdownToLines.

goreleaser check cannot validate a footer template, so the one typo class
that would fail the release job after the tag is already pushed was
checked separately: the footer parses and executes under text/template
with missingkey=error, rendering the real tag with its leading v.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ^.*? prefix on the two group regexps was wrong, and the reasoning
recorded for it was wrong in the same way. Grouping runs before an entry
is formatted, so it matches the bare commit subject and never sees the
SHA the rendered line carries — confirmed in GoReleaser's own source
(changelog.go:185 matches entry.Message; client/github.go:156 sets that
to the subject, so it holds for use: github too) and by a render with the
wildcard removed, which grouped every entry correctly while the output
still showed its SHA prefix.

With the wildcard, feat and fix were substring matches. git revert writes
Revert "feat: x" by default, so a reverted feature was announced under
New features — as the first line, since sort: asc compares bytes and R
precedes f. fixup! and squash! subjects leaked the same way. Verified
after the fix: both fall to Other changes, and all four breaking-marker
shapes still group right.

The false confirmation is worth naming, since the mechanism repeats. The
earlier test saw a SHA in the output and concluded grouping matched the
formatted line; rendering happens after grouping, so that does not
follow. And ^.*? matches under either hypothesis, so the experiment could
not distinguish them and confirmed nothing. The decisive test was to
delete the wildcard and see whether grouping still worked.

Two smaller things found in the same pass. The go mod tidy exclude was
unanchored and silently dropped any subject containing the phrase, so
"feat: run go mod tidy before build" vanished from the notes; it anchors
now. And the entry SHA was the default full 40 characters, which GitHub
shortens on the release page but keepkit's own card does not — panel [2]
is ~27 cells, so abbrev: 7 buys back most of every entry's width and
stays autolinked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@stanlyzoolo

Copy link
Copy Markdown
Owner Author

Self-review found a real defect in the first commit. Fixed in 2eb5d09.

The ^.*? prefix was wrong

Grouping runs before an entry is formatted, so a group regexp matches the bare commit subject and never sees the SHA the rendered line carries. Confirmed in GoReleaser's source — internal/pipe/changelog/changelog.go:185 matches on entry.Message, and internal/client/github.go:156 sets that to the subject's first line, so it holds for use: github too.

With the wildcard, feat/fix were substring matches. git revert writes Revert "feat: x" by default:

### New features
* 2ccee73 Revert "feat: reverted thing"   <- reverted feature announced as a feature
* c6a5e50 feat(ui)!: scoped breaking
* b12fda5 feat: alpha feature
* 3267f37 fixup! feat: wip

It sorts first because sort: asc compares bytes and R precedes f — so a revert would have led the release notes. After the fix both Revert "…" and fixup! land in Other changes, and all four breaking-marker shapes (feat!:, feat(ui)!:, fix!:, fix(model)!:) still group correctly.

The verification in the first commit was invalid

Worth naming, because the failure mode repeats. The original test saw a SHA in the rendered output and concluded grouping must match the formatted line — a non-sequitur, since rendering happens after grouping. And ^.*? matches under both hypotheses, so the experiment had no power to distinguish them; it confirmed nothing while being written up as confirmation. The decisive test was to delete the wildcard and check whether grouping still worked. That claim had propagated into the config comment, the PR body and CLAUDE.md — all three corrected.

Two smaller fixes in the same pass

  1. "go mod tidy" was an unanchored exclude, so any subject containing the phrase silently vanished — feat: run go mod tidy before build was dropped. Anchored to ^go mod tidy; verified the real subject is kept and a bare go mod tidy commit is still excluded.
  2. No abbrev, so entries carried the full 40-char SHA. GitHub shortens it on the release page, but keepkit's own card does not — panel [2] is ~27 cells, so it consumed most of every entry. abbrev: 7 stays autolinked.

Known gaps, deliberately left

  • Excludes are bare prefixes, not prefix: matches, so a subject starting testutil… or circular… would also be dropped. Noted in CLAUDE.md with the tightening (^ci(\(.+\))?!?:) available if it ever bites.
  • An all-excluded tag range renders a bare ## Changelog heading with nothing under it, then the footer. Reachable for a docs-only release. Cosmetic.
  • The footer is still unproven at publish time — GoReleaser only renders it when actually cutting a release. The template itself is checked under text/template with missingkey=error.

goreleaser check exit 0, go build/go vet clean.

@stanlyzoolo
stanlyzoolo merged commit faadf60 into main Jul 30, 2026
3 checks passed
@stanlyzoolo
stanlyzoolo deleted the worktree-release-changelog branch July 30, 2026 05:19
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