Development record
Releases are changelog-driven — rolling `[Unreleased]` is the release decision, and automation tags exactly that commit
Context
abcd has shipped one tag (v0.1.0) via a manual tag push into
.github/workflows/release.yml, which verifies the pushed commit (build,
gofmt, vet, tests, race leg, record/docs/reviews gates) and publishes a GitHub
Release with cross-compiled binaries and a checksums manifest. What the repo
lacked was a policy: what event constitutes the release decision, where that
decision is recorded, and what prevents a tag from drifting away from the code
it claims to describe. CHANGELOG.md already follows Keep a Changelog +
SemVer and already carries the dated ## [v0.1.0] - 2026-07-07 heading — the
release record has always lived in the tree, distinct from the artefact
version stamp that ADR-19/ADR-31 keep out of it.
Decision
Adopt changelog-driven releases, a policy proven in a sibling project's release automation:
- The CHANGELOG is the release instrument. Rolling the accumulated
## [Unreleased]section into a dated## [X.Y.Z] - <date>heading — in an ordinary, reviewed PR — is the release decision. Nothing else declares a release. The dated heading is the one sanctioned in-tree carrier of a released version: it is the record of the decision, not a version stamp (the binaries and manifests keep getting their version only from the tag at build time, per ADR-19/ADR-20). - Automation follows the record. On every push to the default branch,
auto-release.ymlreads the newest dated CHANGELOG version; if it has no tag, the workflow creates an annotatedvX.Y.Ztag at exactly that commit and invokesrelease.ymlas a reusable workflow. Only the newest dated version is ever tagged (tagging an older heading at HEAD would mis-point an immutable tag). An ordinary push with no new dated heading is a no-op. - Tags are immutable; publishes are idempotent and self-healing. An existing tag is never moved. If a tag exists but its Release is missing (a transient publish failure), the workflow re-releases from the tagged commit's resolved SHA, never from a moved-on HEAD.
- Token model. The whole path runs on the built-in
GITHUB_TOKEN, scoped per job; the tag push is the automation's only write and targets a new tag ref. No personal access token exists to leak. - Choosing the number. ADR-31 stands: the version is to be derived
from the shipped intents'
impact, with the surface-diff guardrail — machinery tracked by itd-73 and not yet built. Until it ships, the roll-up PR proposes the bump and maintainer review is the check (pre-1.0: a minor may break, called out under Breaking; a patch is fixes-only). When itd-73 lands, its derived number feeds the roll; the changelog mechanism here is the recording and cutting instrument either way, not a second source of the number.
Amendment (2026-08-26). itd-73 has landed: the ingest derives the number and renders the six closed Keep a Changelog sections, so a pre-1.0 break is signalled by the minor bump and the section its record belongs to, not by a Breaking heading — the derived renderer has no such section. The "called out under Breaking" clause above described the interim hand-rolled regime, whose last cut was v0.6.0.
Alternatives Considered
- Manual tag push as the decision. The status quo. Rejected as policy: the decision lives in someone's shell history, is unreviewable before it fires, and cannot be reverted-before-merge.
- Tagging from CI on every merge (continuous release). Rejected: abcd's releases are curated cuts (ADR-28); most merges are not releases, and the no-op-by-default detect gives exactly that.
- A release bot with a PAT. Rejected: a standing elevated secret for something the built-in token can do with narrower scope.
Consequences
- Cutting a release is reviewable, revertable-before-merge, and leaves the decision in the record (the CHANGELOG diff), not in shell history.
release.ymlgains aworkflow_callentry point (tag, optional resolved-SHAref); the tag-push trigger remains as the manual escape hatch. Both entry points build from the immutable commit SHA, never a re-resolvable ref name.- The detect step tolerates both heading styles in the historical record
(
[v0.1.0]and[0.2.0]); new headings use the Keep-a-Changelog plain form, and the tag always carries the leadingv. - The first release under this policy is
v0.2.0, rolled and tagged by the automation itself as its acceptance test.
Frontmatter
| id | adr-37 |
|---|---|
| type | adr |
| status | accepted |
| date | 2026-07-17 |
.abcd/development/decisions/adrs/0037-changelog-driven-releases.md
open on GitHub ↗ · commit history ↗
References6
- adr-19The plugin version lives ONLY in the released artifact; the working tree stays unversioned, and the version location is chosen by a schema-validated decision, not hard-codedrelated
- adr-20The two release manifests stay version-consistent by an anti-drift checker over a pinned path list; the source view stays unversioned; `--allow-dirty` must never bypass itrelated
- adr-28One repository, a curated release artifact — no dev→public mirrorrelated
- adr-31The release version is derived from the intents in it, never authoredrelated
- itd-72Publishing A Release Is One Atomic, Refusable `ship`supersedes
- itd-73abcd Cuts the Version — You Never Type Onerelated
Referenced by5
- adr-47abcdev.app is rendered from this repository alonerelated
- adr-48The website deploys on release, not on mergerelated
- adr-52The semantic gate sits on the wrong side of the tagrelated
- adr-2610281718537125A release publishes with no approval steprelated
- itd-93abcd Scaffolds a Release Gate That Works on the First Tryrelated
mentions21
- dcn-26071700000000052026-07-17 — Release burst (maintainer-directed): adr-37 adopts
- dcn-26072100000000032026-07-21 — `launch ship` does not tag. It writes the dated `## [X.Y.Z] - <date>`
- dcn-26080600000000022026-08-06 — v0.4.2 release cut: content commit `3377980` (changelog roll; eight
- dcn-26082200000000102026-08-22 — Website build rulings, from the slice reviews (the interview's
- dcn-26082600000000072026-08-26 — Bug-hunt round 9 (bughunt-b): baseline green on the unshallowed
- iss-108iss-35 semantic release gate was self-referential under changelog-driven auto-release (adr-37): it armed receipt_gate with the tagged commit but read receipts from that commit's own tree, which can never hold a receipt naming itself. Dormant while private, it fail-closed the first public release (v0.3.0).
- iss-2608220150157510CHANGELOG.md is the repo's most contended file (205 changes since July): every user-facing change appends an entry near the top, so any two in-flight branches conflict there — and it keeps biting on real merges. The SOTA fix is per-change fragments (the towncrier/changesets pattern, which is also the shape the ledger already uses): each change commits one fragment file with a collision-proof name, and the release cut — which already owns CHANGELOG writes via launch ship (adr-37) — assembles the dated section and deletes the fragments. Zero contention by construction; the changelog-driven release flow is unchanged, only its input format widens
- iss-2609100507439414A managed repository's shared append-only files conflict on nearly every merge, and abcd propagates neither of the two remedies it has already adopted for itself.
- iss-2609300751191426Rulings CF1 and CF2 of 2026-09-30 are not built: the blocked pre-start check still refuses a blocker whose supersession chain ends at an accepted ADR, and a blocker reclassified as a discipline. The person ruled, through the run's interviewer on 2026-09-30: 'CF1 blocker replaced by an accepted ADR: COUNTS AS SETTLED (the waiting intent may start)' and 'CF2 blocker reclassified as a discipline: COUNTS AS SETTLED (the waiting intent may start)'. At base 85d0bb8eb, followBlocker in internal/core/intent/startcheck.go stops an ADR successor with the problem 'adr-N is not an intent: a decision replaced the blocker, and nothing on the record says that settles the edge', and startBlockedRow counts only a final record in shipped/ as settled, so a chain ending in disciplines/ blocks its dependants for ever. Lane supersededBlocker asked both questions rather than building them; no live edge hits either shape today (itd-72 is superseded by adr-37 and blocks nothing), so nothing is refused wrongly yet.
- iss-2609301000153822Two ADR files claiming one id are read first-wins: recordid's resolver kept the first path in name order for an id two files claim, and record-lint had unique-id rules for intents, issues and specs only, so an accepted 0037-a.md beside a proposed 0037-x.md settled every reader of adr-37 (abcd adr-37 rendered the twin; a start-blocker check reading the status would settle on it).
- iss-261025221426017904-launch.md:64, 179-187, 704-707 and 814-821 call commit/tag/publish and the release commit-message format a design target of itd-72, but itd-72 is superseded by adr-37 (.abcd/development/intents/superseded/itd-72-launch-ship-tier-b-publishing.md:20,28), so no decided target stands behind them
- iss-2610262033189165itd-67 sends change-narration stripped from a doc into the changelog; 04-launch says rephrase the doc and never move the sentence into the changelog
- iss-2610262038066422itd-67 has `/abcd:launch ship` tag the repo; 37-reserve has the release workflow push the release tag after the merge
- itd-65abcd Refuses To Publish A Payload Until The Full Ship-Time Pre-Flight Gate Suite Passes, Not Just The Secret Scan
- itd-67abcd Is An Installable, Versioned Claude Code Plugin Whose Repo Is Its Own Marketplace And Whose Every Launch Bumps, Tags, And Publishes A New Version
- itd-2609150819432059A managed repository that is not a plugin gets the same release gate
- one-writer-per-fileOne writer per file
- spc-10derived-versioning
- spc-11installable-versioned-plugin
- spc-14abcd-scaffolds-a-hardened-changelog-driven-release-gate-into
- spc-2609201955279019launch-preflight-gate-suite