Files
agentic-dev/docs/decisions/adr-021-versioning-and-changelog.md
Danijel Martinek f77e6ea881 chore(template): clean-slate template snapshot from bb4a0c7
Curated, product-agnostic snapshot of the post-story-04 tree: demo
content deleted, auth-only reference feature, web-next shell, all gates
green. Product-specific docs, ADRs 027-029, PRDs/epics/archive, editor
library traces, and product naming are curated out; generic template
repairs (coverage provider devDeps, root test:coverage script, live
lint fixes, root-only release-please) are kept. See TEMPLATE.md for
provenance, curation list, and usage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016j8z4VHjedXDTjEDNg7qHK
2026-07-12 20:40:54 +02:00

9.6 KiB
Raw Permalink Blame History

ADR-021 — Hybrid versioning + automated changelog via release-please

Status: Accepted Date: 2026-05-13 Builds on: Conventional Commits convention (CLAUDE.md Key Conventions), ADR-019 (sandcastle agent orchestration)

Context

Until this ADR the template had no versioning + no changelog. Every package shipped at 0.0.0; there was no tag history, no "what changed since I forked this" answer, and no formal cadence for surfacing meaningful state changes.

Two pressures motivated wiring this up now:

  1. Conventional Commits had just been mandated (visible across CLAUDE.md, AGENTS.md, session-start, and prompt-context hooks). Conventional commits are the substrate that automated versioning tools consume — leaving them unused would waste a free signal.
  2. The template is a fork target. Downstream consumers need a version they can pin against ("I forked at v0.3.0, what's changed?") and a changelog they can diff.

The available tools fall in three families:

  • Changesets (@changesets/cli) — contributors run pnpm changeset per PR; explicit semver intent. Higher friction; not needed when conventional commits are already mandated.
  • semantic-release — fully automated from conventional commits; publishes on merge. Less control; monorepo support requires extra plumbing.
  • release-please (Google) — parses conventional commits, opens a rolling release PR with bumps + CHANGELOG entries; merging the PR cuts tags + GitHub releases.

release-please is the natural fit: conventional commits are already enforced, and the "release PR" model gives a human approval gate without requiring per-commit changeset files.

The remaining design choice is versioning scope:

  • Single root version — one CHANGELOG.md at the root; the whole template moves together. Simplest.
  • Per-package versions — every @repo/* package versions independently; one CHANGELOG.md per package. Useful for published packages; we publish nothing today.
  • Hybrid — root template version (for cross-cutting changes: docs, scripts, ci, generators, core packages) + per-feature versions (for packages/<feature>/** changes). One root CHANGELOG.md + one per feature. Decision boundary follows commit-path scoping.

Decision

1. Adopt release-please (Google) as the versioning + changelog substrate. Configuration in release-please-config.json and .release-please-manifest.json at the repo root. The GitHub Action lives at .github/workflows/release-please.yml and runs on every push to main.

2. Hybrid versioning scope. Six tracked packages:

Path Package name Component (tag prefix) Initial version
. template-vertical template 0.1.0
packages/auth @repo/auth auth 0.1.0
packages/blog @repo/blog blog 0.1.0
packages/media @repo/media media 0.1.0
packages/marketing-pages @repo/marketing-pages marketing-pages 0.1.0
packages/navigation @repo/navigation navigation 0.1.0

Each gets its own CHANGELOG.md. Tags use the per-package component prefix to avoid collisions: template-v0.2.0, auth-v0.1.1, etc.

Core packages (core-shared, core-cms, core-api, core-eslint, core-typescript, core-testing) and optional cores (core-events, core-realtime, etc., when scaffolded) are NOT independently versioned. Cross-cutting changes to those land in the root template version. Rationale: those packages cascade — bumping core-shared would functionally invalidate every feature anyway; surfacing them as separate versions creates noise without information. If a future consumer publishes individual core packages downstream, they can add per-package tracking then.

Apps (web-next, web-tanstack, cms, storybook) stay at 0.0.0. They're not consumable artifacts.

3. Pre-1.0 bump policy. While each tracked package is <1.0.0:

  • feat: commits bump patch (not minor), per bump-patch-for-minor-pre-major: true
  • fix: commits bump patch
  • feat!: (or any BREAKING CHANGE: footer) bumps minor (not major)
  • chore, ci, build, style, test commits don't bump

Rationale: the conservative pre-1.0 default means surface area can change without exhausting the version space. When a package crosses 1.0.0, standard semver kicks in.

4. Conventional-commit type → changelog section mapping. From release-please-config.json:

Type Section Hidden
feat Features no
fix Bug Fixes no
perf Performance no
refactor Refactoring no
docs Documentation no
revert Reverts no
test, chore, ci, build, style (omitted) yes

Hidden sections still drive version bumps where applicable (none do, by current policy) but don't clutter the changelog.

5. Bump targeting is by commit-path, not commit-scope. release-please decides which package(s) to bump based on the files changed in a commit, NOT the conventional-commit scope. A commit changing packages/auth/** bumps @repo/auth; a commit changing docs/** or scripts/** or CLAUDE.md bumps the root template; a commit changing both bumps both. The conventional-commit scope field is for human readability in the changelog — it does not drive routing.

6. Release PR is a rolling document. Every push to main re-evaluates the open release PR. Merging it cuts tags + creates GitHub releases for each affected package. No manual edits to the PR — the changelog content is reproducible from the commit history.

7. CHANGELOG files are committed and edited only by release-please. Manual edits are discouraged because they will be overwritten the next time release-please assembles the rolling PR. Initial baseline content for each 0.1.0 entry is the only exception.

Alternatives considered

  • Changesets (@changesets/cli) — rejected primarily because Conventional Commits already capture the necessary intent. Per-PR changeset files would duplicate information. Could revisit if release-please ever fails to handle a release-shape edge case (e.g. needing a manual bump that's larger than commits imply).
  • semantic-release — rejected for monorepo friction. Per-package support requires semantic-release-monorepo or multi-semantic-release; release-please handles this natively.
  • Single root version — rejected because cross-cutting commits ARE a different kind of change from feature commits. A fix(media): off-by-one in upload shouldn't churn the root template version; a refactor(coverage): unify L0 thresholds shouldn't churn @repo/auth's version. Separating gives consumers a finer-grained "what changed for me" signal.
  • Per-package for every workspace member (apps, core, tooling) — rejected. Core packages cascade; bumping core-shared is effectively a template-wide change. Apps aren't consumable. Tooling churn is mostly mechanical. Adding versions for these adds bookkeeping without information value.
  • Tag prefix template-vertical-v... vs template-v... — chose template-v for tag economy (shorter; consistent length with auth-v, blog-v, etc.). The package name template-vertical is still authoritative in package.json.
  • Auto-merge the release PR — rejected. Human approval is the gate that catches the rare case where a commit's content doesn't match its conventional type (e.g. a chore: commit that actually shipped a feature).

Consequences

Positive:

  • Every merge to main produces a tracked, dated record of what changed for downstream consumers.
  • Conventional commits become load-bearing: their type + body shape the changelog directly.
  • The "what version did I fork at" question has a real answer per tracked package.
  • Tagged releases enable git diff vX.Y.Z..HEAD -- <path> for narrow "what changed in feature X since I last looked" questions.
  • Release cadence is implicit (merge the PR when ready); no separate release planning required.

Negative:

  • Six packages × independent versions means six CHANGELOG.md files to navigate. Mitigated by the ## Cross-references section in each pointing at the relevant ADRs + this one.
  • release-please-action is GitHub-Actions-coupled. A migration to a different CI provider would need a different runner (the release-please core CLI runs anywhere, but the orchestration around the rolling PR is Action-specific).
  • The first release PR after the initial baseline could be large (covers everything merged after 0.1.0). This is one-time; subsequent PRs are scoped to the work between releases.
  • Apps stuck at 0.0.0 is mildly confusing if a contributor expects every package to be versioned. Mitigated by documentation here + in docs/guides/releasing.md.
  • ADR-019 — sandcastle agent orchestration (the dispatch loop that ships these commits)
  • ADR-020 — coverage architecture (one of the systems shipped at the 0.1.0 baseline)
  • CLAUDE.md Key Conventions — Conventional Commits requirement (the substrate this ADR consumes)
  • docs/guides/releasing.md — day-to-day cookbook