docs(release): CHANGELOG + operator-visible-change convention (LC-209) #275

Merged
longjacksonle merged 1 commit from docs/lc-209-changelog-convention into main 2026-05-30 04:13:15 +02:00

LC-209: the repo has no operator-facing changelog; default-on behavior changes and security fixes were documented only in PR descriptions and git log. This investigates the release reality, decides the channel deliberately, and seeds a tight backfill.

Release reality (item 0 - this gated the decision)

Fact Finding
Git tags Zero. Ships the latest OCI image off main (1060 commits, ~7 weeks, no CI gate).
Release process Built but dormant. just create-release bumps the workspace semver, opens a release/v* PR; on merge create-release.yml tags + creates a Forgejo Release (body = auto git log --oneline between tags); v* tag fires publish-release.yml (desktop self-updater + latest.json). Never invoked (still v0.1.0).
Changelog seam The release PR body uses a tempfile "so the changelog can grow later" - the infra was built to carry curated release content.
Operator channel today The auto-generated Forgejo release body (raw commit subjects) - exactly the "operator has to read git log" gap.

Decision: hybrid (not per-PR CHANGELOG, not marker-only)

  • (a) per-PR CHANGELOG rejected as sole mechanism: the discipline that won't hold at this velocity with no gate; a stale changelog is worse than none.
  • (b) marker-only rejected: an operator on the repo sees no changelog, and a commit-subject firehose doesn't surface "set this env var" / "upgrade promptly" with the action text.
  • Hybrid chosen, sustainable here because: there is a release trigger to anchor to (just create-release); per-PR friction is zero (only the release-cutter touches CHANGELOG); the update is mechanical (grep markers since the last tag, not from memory); and CHANGELOG.md records tagged releases only, so it cannot go stale - the between-release delta lives in immutable git markers, reconstructable via git log --grep.

Deliverables (docs only)

  • CHANGELOG.md - Keep-a-Changelog + a Pre-release seed backfill (operator-visible changes to date, not every PR). Leads with Security (LC-152 unified outbound SSRF guard, closed unguarded Web Push SSRF, upgrade-promptly) and the default-on / destructive Changed items (LC-78-AVATAR-PROXY default-ON proxy + opt-out env; LETS_CHAT_RETENTION_SWEEP_ENABLED destructive default-OFF; LC-205 email-ingress now fires webhooks), then Added (LC-77 ingress + reply; LC-207 admin page). LC-204/206/208 omitted (not operator-visible).
  • docs/releasing.md - the release process, the weighed decision, the operator-visible definition list, the marker convention, responsibilities, and a cut-a-release checklist.
  • Marker convention - [operator-action] in PR title + commit subject (rides the auto git log release body; greppable git log --grep='\[operator-action\]' <last-tag>..HEAD) + a recommended Operator-Action: trailer carrying the one-line instruction.
  • CLAUDE.md - contributor-facing summary + marker rule. README.md - "Upgrading? Read CHANGELOG.md first" pointer.

Out of scope (named in the doc)

Enhancing create-release.yml to auto-grep markers into the release body (modifies dormant CI that can't be exercised until a first tag; revisit then). No changelog tooling/bots - the written convention is cheaper for a no-CI-gate project.

🤖 Generated with Claude Code

LC-209: the repo has no operator-facing changelog; default-on behavior changes and security fixes were documented only in PR descriptions and git log. This investigates the release reality, decides the channel deliberately, and seeds a tight backfill. ## Release reality (item 0 - this gated the decision) | Fact | Finding | |---|---| | Git tags | **Zero.** Ships the `latest` OCI image off `main` (1060 commits, ~7 weeks, no CI gate). | | Release process | **Built but dormant.** `just create-release` bumps the workspace semver, opens a `release/v*` PR; on merge `create-release.yml` tags + creates a Forgejo Release (body = auto `git log --oneline` between tags); `v*` tag fires `publish-release.yml` (desktop self-updater + `latest.json`). Never invoked (still `v0.1.0`). | | Changelog seam | The release PR body uses a tempfile "so the changelog can grow later" - the infra was built to carry curated release content. | | Operator channel today | The auto-generated Forgejo release body (raw commit subjects) - exactly the "operator has to read git log" gap. | ## Decision: hybrid (not per-PR CHANGELOG, not marker-only) - **(a) per-PR CHANGELOG** rejected as sole mechanism: the discipline that won't hold at this velocity with no gate; a stale changelog is worse than none. - **(b) marker-only** rejected: an operator on the repo sees no changelog, and a commit-subject firehose doesn't surface "set this env var" / "upgrade promptly" with the action text. - **Hybrid** chosen, sustainable *here* because: there **is** a release trigger to anchor to (`just create-release`); per-PR friction is **zero** (only the release-cutter touches CHANGELOG); the update is **mechanical** (grep markers since the last tag, not from memory); and CHANGELOG.md records **tagged releases only**, so it **cannot go stale** - the between-release delta lives in immutable git markers, reconstructable via `git log --grep`. ## Deliverables (docs only) - **CHANGELOG.md** - Keep-a-Changelog + a `Pre-release` seed backfill (operator-visible changes to date, not every PR). Leads with **Security** (LC-152 unified outbound SSRF guard, closed unguarded Web Push SSRF, upgrade-promptly) and the default-on / destructive **Changed** items (LC-78-AVATAR-PROXY default-ON proxy + opt-out env; `LETS_CHAT_RETENTION_SWEEP_ENABLED` destructive default-OFF; LC-205 email-ingress now fires webhooks), then **Added** (LC-77 ingress + reply; LC-207 admin page). LC-204/206/208 omitted (not operator-visible). - **docs/releasing.md** - the release process, the weighed decision, the operator-visible definition list, the marker convention, responsibilities, and a cut-a-release checklist. - **Marker convention** - `[operator-action]` in PR title + commit subject (rides the auto `git log` release body; greppable `git log --grep='\[operator-action\]' <last-tag>..HEAD`) + a recommended `Operator-Action:` trailer carrying the one-line instruction. - **CLAUDE.md** - contributor-facing summary + marker rule. **README.md** - "Upgrading? Read CHANGELOG.md first" pointer. ## Out of scope (named in the doc) Enhancing `create-release.yml` to auto-grep markers into the release body (modifies dormant CI that can't be exercised until a first tag; revisit then). No changelog tooling/bots - the written convention is cheaper for a no-CI-gate project. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs(release): CHANGELOG + operator-visible-change convention (LC-209)
All checks were successful
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 5s
check-secrets / TruffleHog (push) Successful in 4s
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / Kingfisher (push) Successful in 5s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 2m39s
5110788638
Investigated the release reality before choosing, per the ticket. Findings: the repo has zero git tags and ships the `latest` OCI image off `main` (1060 commits in ~7 weeks, no CI gate), BUT a full semver, tag-driven release process is built and dormant: `just create-release` bumps the workspace version and opens a `release/v*` PR; on merge `create-release.yml` tags and creates a Forgejo Release whose body is an auto-generated `git log --oneline` between tags; the `v*` tag fires `publish-release.yml` (desktop self-updater artifacts + latest.json). The release PR body already uses a tempfile seam "so the changelog can grow later." So there IS a release trigger to anchor changelog discipline to; it has just never been invoked.

Decision: hybrid, not per-PR CHANGELOG and not marker-only. Reasoning from the release reality: per-PR CHANGELOG edits are the discipline that won't hold at this velocity with no gate (rejected as sole mechanism); marker-only leaves an operator landing on the repo with no changelog and a raw commit-subject firehose that does not surface "set this env var" / "upgrade promptly" with action text. The hybrid is sustainable HERE because (1) there is a release trigger (`just create-release`) to anchor to, (2) per-PR friction is zero - only the release-cutter touches CHANGELOG, (3) the update is mechanical (grep the markers since the last tag) not from memory, and (4) CHANGELOG.md records tagged releases only, so it cannot go stale - the between-release delta lives in immutable git markers and is always reconstructable via `git log --grep`.

Deliverables (docs only, no code):
- CHANGELOG.md: Keep-a-Changelog format, with a Pre-release seed backfill of operator-visible changes to date (NOT every PR). Leads with Security (LC-152 unified outbound SSRF guard + closed unguarded Web Push SSRF, upgrade-promptly) and the default-on/destructive Changed items (LC-78-AVATAR-PROXY foreign-avatar proxy default-ON + the opt-out env var; LETS_CHAT_RETENTION_SWEEP_ENABLED destructive default-OFF; LC-205 email-ingress now fires outgoing webhooks), then Added (LC-77 email ingress + reply-by-email gating; LC-207 admin diagnostic page). Internal-only work (LC-204/206/208) intentionally omitted.
- docs/releasing.md: the release process, the decision + why each option was weighed, the operator-visible definition list, the marker convention, responsibilities, and a cut-a-release checklist.
- The marker convention: operator-visible PRs carry `[operator-action]` in the title + commit subject (rides the auto `git log` release body; greppable via `git log --grep='\[operator-action\]' <last-tag>..HEAD`) plus a recommended `Operator-Action:` commit trailer carrying the one-line instruction.
- CLAUDE.md: a contributor-facing summary of the convention + the marker rule.
- README.md: an "Upgrading? Read CHANGELOG.md first" pointer in Quick Start.

Out of scope (named in docs/releasing.md): enhancing create-release.yml to auto-grep the markers into the release body - it modifies dormant CI that cannot be exercised until a first real tag; revisit when the first release is cut. No tooling (conventional-commits bots, release-please) - the written convention is cheaper for a no-CI-gate project.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch docs/lc-209-changelog-convention 2026-05-30 04:13:15 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!275
No description provided.