Skip to content

feat(changelog): curated CHANGELOG with show-once teaser + real release notes#381

Open
schmitthub wants to merge 9 commits into
mainfrom
chore/better-release-notes
Open

feat(changelog): curated CHANGELOG with show-once teaser + real release notes#381
schmitthub wants to merge 9 commits into
mainfrom
chore/better-release-notes

Conversation

@schmitthub

Copy link
Copy Markdown
Owner

What

Users miss user-facing changes after brew upgrade, and GoReleaser ships a flat commit list as release notes. This adds a curated changelog with two payoffs:

  • Show-once "What's new" teaser. The CLI fetches a hand-maintained CHANGELOG.md from main over the network (like the update checker — clawker runs on the host, always online), and after a command completes prints a one-time, cursor-driven teaser of entries gained since the last shown version. TTY-only, suppressed on CI / CLAWKER_NO_UPDATE_NOTIFIER / DEV, loaded in a background goroutine so it never blocks the command.
  • Real release notes. The release workflow awk-extracts the tag's ## [x.y.z] section from CHANGELOG.md and feeds it to GoReleaser via --release-header (curated section above the auto commit groups, not replacing them).

There is no on-demand command — only the automatic teaser. The metadata fetch is async to the release, so a forgotten entry self-heals: commit it to CHANGELOG.md later and the live fetch picks it up without a re-release.

Shape

  • internal/changelog — pure core (Parse / Between, no I/O) + I/O layer (Fetch, Loader = fetch + on-disk cache + TTL gate + graceful degrade; degrade-path failures now log to the file log).
  • internal/statestorage.Store[CliState] facade (checked_at / versions / last_seen_changelog / changelog_fetched_at); field-merge writes so the update goroutine and the changelog cursor never clobber each other. Reads the existing update-state.yaml in place.
  • internal/update — made pure (fetch + compare, no persistence); the caller owns state. URL-parameterized unexported core so tests drive real code.
  • internal/semver — consolidated leaf (moved out of internal/bundler/semver); CompareStrings (v-tolerant, total) + IsValidLoose.
  • internal/storageKindTime support for time.Time fields (RFC3339 scalar).

Verification

  • Affected-package tests green; golangci-lint clean; go build ./... clean.
  • Release-notes flow (awk → --release-header) is fully exercised only by a real tag; CHANGELOG.md must land on main for the teaser fetch (pre-merge it degrades 404 → cache → empty, as designed).

Review

Two review rounds run; all findings actioned or explained in commit 9d2e555a (silent-failure log on the loader degrade path, doc/signature accuracy, test cleanup, a dead firewall path rule).

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings June 15, 2026 10:40
@mintlify

mintlify Bot commented Jun 15, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
schmitthub 🟢 Ready View Preview Jun 15, 2026, 10:42 AM

💡 Tip: Enable Workflows to automatically generate PRs for you.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a curated, user-facing changelog system that powers (1) a show-once “What’s new” teaser in the CLI after upgrades and (2) curated release-notes headers prepended to GoReleaser’s auto-generated commit groups. This reshapes update checking into a pure fetch+compare unit, centralizes persisted CLI runtime state, and adds semver helpers and storage support for time.Time.

Changes:

  • Add internal/changelog (pure parse/range + fetch/cache/TTL loader) and wire a background “What’s new” teaser into internal/clawker/Main.
  • Add internal/state as the single owner of update-state.yaml (update-check cache + changelog cursor + changelog fetch TTL) using storage.Store field-merge writes.
  • Consolidate semver utilities into internal/semver, add storage support for time.Time, and update release tooling to extract curated release headers from CHANGELOG.md.

Reviewed changes

Copilot reviewed 49 out of 49 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
internal/update/update.go Makes update checking pure (no persistence) and switches semver compare to internal/semver.
internal/update/update_test.go Updates tests for new update API contract (always returns CheckResult, IsNewer flag).
internal/update/CLAUDE.md Documents the new “pure update checker” contract and integration.
internal/storage/write.go Ensures time.Time serializes as a scalar instead of flattening to {}.
internal/storage/field.go Adds KindTime and treats time.Time as an opaque leaf field.
internal/storage/field_test.go Extends schema normalization tests to cover time.Time.
internal/storage/CLAUDE.md Documents KindTime and its serialization behavior.
internal/state/state.go Adds CLI runtime state facade over storage.Store[CliState].
internal/state/state_test.go Adds round-trip, non-clobber, legacy-read, and migration-wiring tests for CLI state.
internal/state/CLAUDE.md Documents state schema, API, and field-merge invariants.
internal/semver/semver.go Adds CompareStrings, IsValidLoose, and supporting helpers.
internal/semver/semver_test.go Adds tests for CompareStrings behavior and invalid input ordering.
internal/semver/CLAUDE.md Documents semver package role as a stdlib-only leaf shared across the DAG.
internal/consts/consts.go Adds GitHub repo/base URL consts, DEV sentinel, state/cache filenames.
internal/cmdutil/factory.go Adds new factory nouns: State() and Changelog().
internal/cmdutil/CLAUDE.md Updates Factory documentation to include new nouns and their semantics.
internal/cmd/factory/default.go Wires State and Changelog lazy constructors into the default factory.
internal/cmd/factory/CLAUDE.md Documents new stateFunc() wiring and behavior.
internal/clawker/cmd.go Adds background changelog load + show-once teaser, updates update persistence flow.
internal/clawker/cmd_test.go Adds extensive tests for changelog teaser cursor/suppression behavior and IsNewer gating.
internal/clawker/CLAUDE.md Documents show-once teaser algorithm and background loading discipline.
internal/changelog/testdata/CHANGELOG.md Adds fixture changelog for parser tests.
internal/changelog/parse.go Implements Keep-a-Changelog parsing + metadata extraction.
internal/changelog/loader.go Adds loader orchestration: fetch + cache + TTL gate + parse + degrade behavior.
internal/changelog/loader_test.go Tests loader TTL behavior, cache fallback, nil-state behavior, etc.
internal/changelog/fetch.go Adds HTTP fetch helper with short timeout and non-200 errors.
internal/changelog/fetch_test.go Tests fetch success, non-200 errors, context cancellation, nil client.
internal/changelog/consts.go Centralizes parsing tokens and sets ChangelogURL from internal/consts.
internal/changelog/CLAUDE.md Documents changelog format, API, loader behavior, and tests.
internal/changelog/changelog.go Defines public Entry, Parse, and Between API.
internal/changelog/changelog_test.go Tests parsing rules, metadata, title extraction, and range filtering.
internal/bundler/versions.go Switches bundler to the consolidated internal/semver.
internal/bundler/semver/CLAUDE.md Removes obsolete docs for the deleted bundler semver subpackage.
internal/bundler/registry/types.go Switches registry types to internal/semver.
internal/bundler/registry/CLAUDE.md Updates dependency notes to reference internal/semver.
internal/bundler/CLAUDE.md Updates docs to remove bundler semver subpackage references.
docs/installation.mdx Documents the “What’s new” teaser behavior and suppression conditions.
CONTRIBUTING.md Adds changelog entry requirements and formatting guidance.
claude-plugin/clawker-support/skills/clawker-support/reference/troubleshooting.md Adds troubleshooting entry for missing “What’s new” note after upgrades.
claude-plugin/clawker-support/.claude-plugin/plugin.json Bumps plugin version.
CHANGELOG.md Adds curated root changelog content and metadata format.
.serena/memories/release-guide.md Updates release guide to describe curated header + auto changelog composition.
.serena/memories/changelog-system.md Adds system memory doc for the changelog design and release flow.
.goreleaser.yaml Adds grouped changelog config and documents --release-header usage.
.github/workflows/release-build.yml Extracts tag section from CHANGELOG.md and passes it via --release-header.
.clawker.yaml Adds contributor-assistant GitHub action allowlist paths.
.claude/rules/storage-schema.md Documents time.Time schema behavior as KindTime.
.claude/rules/dependency-placement.md Updates dependency placement notes for consolidated semver.
.claude/docs/changelog-system-design.md Adds shipped design doc for curated changelog system.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/changelog/loader.go
Comment thread .goreleaser.yaml
Comment thread internal/changelog/CLAUDE.md Outdated
Comment thread internal/cmdutil/CLAUDE.md Outdated
Comment thread .claude/docs/changelog-system-design.md Outdated
Comment thread .serena/memories/changelog-system.md Outdated
schmitthub and others added 4 commits June 15, 2026 13:05
…notes

Add a curated, hand-maintained CHANGELOG.md (Keep a Changelog format with
per-entry HTML-comment metadata) and a `clawker changelog` command so users
learn about user-facing changes after upgrading instead of silently missing
them.

The CLI fetches CHANGELOG.md over the network from `main` (like the update
checker) — not embedded in the binary — so a forgotten entry committed after a
release is picked up without re-releasing; the installed version is the display
ceiling. internal/changelog splits into a pure core (Parse/Between/ForVersion)
and an I/O layer (Fetch/Loader with on-disk cache, TTL gate, silent degrade).
A cursor-driven show-once teaser runs in a non-blocking background goroutine,
TTL-gated and suppressed on CI/non-TTY.

State moves to storage.Store[CliState] (field-merge, no clobber) with a
ChangelogFetchedAt timestamp; new Factory nouns f.State and f.Changelog.

Release notes: the release workflow awk-extracts the tag's section from
CHANGELOG.md and passes it to goreleaser via --release-header (coexists with
the auto commit-group changelog).

Consolidate three semver implementations into one internal/semver leaf package
(moved from internal/bundler/semver). Add v-tolerant CompareStrings and
IsValidLoose; delete the changelog and update copies. update.IsNewer keeps an
IsValidLoose guard to preserve the conservative "unparseable -> not newer"
contract.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The curated changelog surfaces through a single automatic one-time "what's
new" teaser after a command runs — there is no on-demand `clawker changelog`
command. The teaser lists the entry titles gained since the last shown version
with a per-entry docs link; the first run with no catch-up seeds the cursor
silently (no welcome). The network loader, cursor state, and curated
CHANGELOG.md / release-notes flow are unchanged.

Harden both background goroutines in Main(): each now sends on its channel
exactly once from a deferred function that also recovers from panic, so a
panic in the update-check or changelog-load goroutine can neither crash a
command that already succeeded nor deadlock the foreground blocking read.

Also: drop dead code (changelog.ForVersion had no remaining caller), fix
comment-policy violations (historical narration + a hard-spelled state
filename), dedup the DEV sentinel to consts.DevVersion, and close three
coverage gaps (partial-semver header rejection, the tagFromSubsection mapping
table, and the fresh-cache-but-missing-file refetch branch).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…racy, test cleanup

- loader: thread a *logger.Logger so a fetch failure that falls back to the
  on-disk cache is recorded to the file log instead of vanishing (this was the
  one genuine silent swallow — a 404 before CHANGELOG.md lands on main, or a
  DNS/firewall block, now leaves a breadcrumb)
- update: extract an unexported checkForUpdate(...url) core that CheckForUpdate
  delegates to; tests now drive the real gate→fetch→assemble path instead of a
  parallel checkForUpdateWithURL reimplementation. Drop that helper, the dead
  fetchLatestRelease, and the test that only exercised Go channel semantics
- firewall: add the missing leading slash on the raw.githubusercontent.com
  contributor-assistant/github-action path rule — without it the stored path
  never prefix-matched under path_default: deny, so the allow was dead
- docs: correct the maybeShowChangelog signature (5 args, entries third); fix
  the design-doc cursor pseudocode to use the priorCurrentVersion snapshot, not
  a live current_version read that races the update goroutine; add the missing
  entries==nil guard to every pseudocode rendering; retire the Entry.Body
  "rendered verbatim by the CLI" claim (the teaser renders Title, not Body);
  drop legacy / deleted-command narration
- release workflow: strip the clawker metadata HTML comment from the awk-
  extracted release-notes header (invisible on GitHub, but present in the raw
  release body)
- tests: add corrupt-fresh-cache degrade coverage and a nil-entries-with-cursor
  case; drop the redundant persisted-yaml-keys test (the round-trip tests prove
  serialization more strongly)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 15, 2026 13:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 50 changed files in this pull request and generated 4 comments.

Comment thread .goreleaser.yaml
Comment thread internal/changelog/CLAUDE.md
Comment thread internal/cmdutil/CLAUDE.md
Comment thread .serena/memories/release-guide.md Outdated
schmitthub and others added 2 commits June 15, 2026 15:19
CI unit tests failed only under GitHub Actions: newChangelogTestFactory did
not neutralize ambient teaser-suppression env, so CI=true (set by Actions)
tripped changelogSuppressed and silenced every teaser-expecting case. The
helper now clears CI + CLAWKER_NO_UPDATE_NOTIFIER; the opt-out test re-sets
its env after the helper.

Review-round fixes:
- changelog.stale(): honor the documented contract — a non-positive TTL is
  now treated as "always stale" (was: relied on incidental now>last).
- .goreleaser.yaml: de-lazy the changelog.group regexps (.*? -> .*, ?? -> ?).
  Behavior-identical (GoReleaser contains-matches via Go RE2 MatchString);
  drops the non-greedy constructs that read as RE2-invalid.
- docs/memories: correct the NewLoader signature (now takes log *logger.Logger)
  in changelog/CLAUDE.md, cmdutil/CLAUDE.md, changelog-system-design.md, and
  the changelog-system memory; rewrite the stale release-guide release-header
  section to the awk -> release-notes.md -> --release-header flow.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 15, 2026 15:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 48 out of 48 changed files in this pull request and generated 4 comments.

Comment thread internal/changelog/parse.go Outdated
Comment on lines +94 to +97
after = strings.TrimSpace(after)
if sep := strings.Index(after, strings.TrimSpace(dateSeparator)); sep >= 0 {
date = strings.TrimSpace(after[sep+len(strings.TrimSpace(dateSeparator)):])
}
Comment on lines +73 to +78
awk -v ver="## [${VERSION}]" '
index($0, ver) == 1 { collecting = 1; print; next }
collecting && /^## \[/ { exit }
collecting && /^<!-- clawker:/ { next }
collecting { print }
' CHANGELOG.md > release-notes.md
Comment thread .github/workflows/cla.yml
Comment thread .github/workflows/cla.yml
…ata layer

A release spans many merged PRs and mixed change kinds, so a single
tag/title/docs scalar per release was the wrong model — and the Tag field was
parsed but never rendered anywhere. Shrink changelog.Entry to {Version, Date,
Body} and render the whole Keep-a-Changelog section as markdown in the
"what's new" teaser, so multi-section releases (Added + Fixed + ...) surface
fully instead of collapsing to one derived headline.

- Add iostreams.RenderMarkdown (glamour, compact margin-free style, soft-wrap;
  theme/color follow IOStreams detection, ASCII base when color disabled).
- Strip the parser's metadata/tag/title machinery (parseMetaComment,
  tagFromSubsection, titleFromBody, Tag type, section consts); bodies keep all
  ### sections and inline links, with HTML comments and the link-ref block
  stripped.
- Curate root CHANGELOG.md: drop the <!-- clawker: --> lines, inline docs links
  into bullets.
- Generalize the release-notes awk to drop any single-line HTML comment.
- Add glamour deps to NOTICE; sync package docs + clawker-support plugin
  (1.0.15); end-user docs describe the teaser in user terms without naming
  output channels.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ven coverage

The changelog teaser suite asserted only on presence (version strings, entry
counts) and hand-fed Entry structs that bypassed the parser, so the body-render
path the teaser exists for could break without a single failing test.

- Drive maybeShowChangelog tests through real changelog.Parse (teaserChangelogFixture)
  instead of hand-built Entry structs, exercising the parse→cursor seam.
- Add TestMaybeShowChangelog_E2E_RendersParsedBody: real bytes over httptest →
  Loader.Load → maybeShowChangelog → Between → printChangelogTeaser → RenderMarkdown,
  asserting gained release bodies render and non-gained / Unreleased bodies don't.
- Assert parsed Body content (not just len/version) in the loader fetch + cache
  -fallback tests, closing the gap where a dropped Body stayed green.
- Merge the redundant TestParse_HTMLCommentStripped into TestParse_Body (same
  isHTMLComment branch); cover the non-clawker comment flavor via a testdata line.
- Fix the stale loadFixture doc comment and drop a dead fixture comment.

Each new/changed assertion was verified to go red under mutation (deleted body
render, zeroed parse Body, disabled comment stripping, render-all-not-gained).
Comment/link-ref stripping is asserted at the parsed-Body level, not teaser
output, because glamour also strips them on render (a teaser-output check would
stay green if the parser's stripping broke).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 15, 2026 17:20

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 53 out of 54 changed files in this pull request and generated 4 comments.

Comment thread internal/update/update.go
Comment thread internal/clawker/cmd.go
Comment thread .github/workflows/cla.yml
Comment thread CONTRIBUTING.md
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.

2 participants