Files
agentic-dev-template/docs/decisions/adr-021-versioning-and-changelog.md
Danijel Martinek b96cce5d74 feat: hybrid versioning + automated CHANGELOG via release-please
Closes the user's ask: versioning + a changelog generated on merging
to main, building on the just-mandated Conventional Commits substrate
(CLAUDE.md Key Conventions).

Architecture: ADR-021. Cookbook: docs/guides/releasing.md.

Initial state — six tracked packages at v0.1.0:
  - .                          -> template-vertical  (tag: template-v...)
  - packages/auth              -> @repo/auth         (tag: auth-v...)
  - packages/blog              -> @repo/blog         (tag: blog-v...)
  - packages/media             -> @repo/media        (tag: media-v...)
  - packages/marketing-pages   -> @repo/marketing-pages (tag: marketing-pages-v...)
  - packages/navigation        -> @repo/navigation   (tag: navigation-v...)

Core packages, tooling, and apps are NOT independently versioned
(ADR-021 rationale: core bumps cascade; apps aren't consumables;
surfacing them would create noise without information).

Configuration:
  - release-please-config.json   - 6 tracked packages, hybrid scope,
                                   pre-1.0 conservative bump policy
                                   (feat: -> patch, feat!: -> minor),
                                   conventional-commit type mapping
  - .release-please-manifest.json - baseline 0.1.0 for all 6 packages
  - .github/workflows/release-please.yml - googleapis/release-please-
                                   action@v4 on push to main,
                                   concurrency-gated, write
                                   permissions for the rolling PR

Workflow: on every push to main, release-please scans commits since
the last release tag PER PACKAGE (using commit-path, not the
conventional-commit scope), updates a single rolling release PR with
version bumps + per-package CHANGELOG entries. Merging that PR cuts
per-package tags + GitHub releases.

CHANGELOG files seeded at v0.1.0 baseline:
  - CHANGELOG.md (root)
  - packages/<feature>/CHANGELOG.md (5 features)
Subsequent versions are appended by release-please from commit
history. Do not edit manually.

Visibility surfaces updated (every agent entry point):
  - CLAUDE.md Read First + new "Versioning is hybrid" Key Conventions
    bullet (with bump policy summary)
  - AGENTS.md preamble - new "Releases:" callout alongside Commits
  - docs/glossary.md - new Releasing section with 8 terms (Conventional
    Commits, release-please, Hybrid versioning, Tag prefix, Rolling
    release PR, Bump targeting, Pre-1.0 bump policy, Release-As trailer,
    CHANGELOG.md)
  - docs/README.md - guides tree updated with releasing.md
  - .claude/hooks/session-start.sh - one-line release reminder
  - .claude/hooks/prompt-context.sh - new keyword group for
    release/version/bump/semver/tag prompts

Package.json version bumps:
  - root: name "template" -> "template-vertical", version "0.1.0"
  - packages/auth, blog, media, marketing-pages, navigation: "0.0.0" -> "0.1.0"

Root rename rationale: release-please tags use the package-name + the
component prefix; "template-vertical" matches the repo identity (and
the user's question preview).

First release-please PR after this lands will sweep all subsequent
post-baseline commits into 0.1.1 / 0.2.0 bumps as appropriate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 17:17:16 +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