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

112 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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`.
## Related
- 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