ADR-0024: Git Branching Model — dev for Integration, main for Releases
Date: 2026-07-18 Status: Accepted
Context
Until now the repository used a single long-lived branch, main. Feature work,
bug fixes, chores, and release-preparation commits all landed directly on
main, interleaved. Two forces make this arrangement increasingly awkward:
- Releases are tag-driven. ADR-0020
publishes a stable release only when a
v*.*.*tag is pushed, andrelease.ymlalready verifies that the tag points at a commit onmain. Nothing else aboutmaindistinguishes "released" commits from "work-in-progress" commits — the branch is a mix of both. mainalso drives the rollingunstablesnapshot. A job inbuild.ymlre-points theunstablepre-release on every push tomain. Somainis simultaneously the integration branch and the release branch, and theCHANGELOG.md[Unreleased]section accumulates directly on the same branch that is supposed to represent shipped software.
There is no branch whose state answers the question "what is the latest
released version?" without inspecting tags, and no branch that represents
"everything merged since the last release" other than main itself.
Decision
Adopt a two-tier long-lived branch model.
mainis release-only. Every commit onmaincorresponds to a released, tagged version.mainmoves only when a release is cut. ItsHEADalways equals the most recent published release. No feature or fix is committed tomaindirectly.devis the integration branch. It is the default target for day-to-day work and always contains "everything merged since the last release." TheCHANGELOG.md[Unreleased]section lives and grows ondev.- Feature branches (
feat/…,fix/…,chore/…,docs/…) branch fromdevand merge back intodev, normally via pull request. Merging a branch intodevis what adds its entry to[Unreleased]. - Releasing is
dev→main+ tag. To release,devis merged intomain, the release-preparation commit renames[Unreleased]to## [X.Y.Z] — YYYY-MM-DD(see the changelog policy inCLAUDE.md), and avX.Y.Ztag is pushed onmain. The tag triggersrelease.yml(ADR-0020). After the release,mainis merged back intodevso the two branches share the release commit. - Hotfixes for an already-released version branch from
main(fix/…), merge intomain, are released via a patch tag, and are then merged back intodev.
Version numbering (semantic versioning, ADR-0001) and the tag-triggered pipeline (ADR-0020) are unchanged; this ADR only changes which branch carries integration versus release state.
CI consequences (applied in this change)
- The
unstablepre-release job inbuild.ymlis removed. Withdevin place, the tip ofdevis the rolling "latest integrated" state, so a separateunstableGitHub pre-release (and its force-pushedunstabletag) is redundant. This supersedes theunstablepre-release introduced by ADR-0020; per-run fat-JAR artifacts remain available on each Actions run. sonar.ymlruns its push analysis on bothmainanddevso the integration branch is quality-gated.docs.ymldeploys documentation fromdev, so ADRs and design docs publish when they are integrated rather than only at release time.release.ymlis unchanged: it still triggers onv*.*.*tags and still requires the tag to be an ancestor ofmain.
Rationale
mainbecomes a truthful release ledger.git log mainis exactly the release history;main@HEADis exactly what users get from Homebrew / the PPA. This mirrors the intent already encoded inrelease.yml's "tag must be on main" check, and makes it structural rather than incidental.[Unreleased]gets a natural home. The changelog's unreleased section is, by definition, "merged but not released" — precisely the contents ofdev. Keeping it offmainremoves the oddity of a shipped branch carrying an[Unreleased]heading between releases.- Deliberate releases, continuous integration. Work still flows
continuously into
dev(whoseHEADis the latest integrated code), while releasing stays an explicit, low-frequency act (adev→mainmerge plus a tag), consistent with ADR-0020's "releases are deliberate" rationale. - Low ceremony. Two long-lived branches and short-lived topic branches is the smallest model that separates integration from release; it does not require release branches or a full git-flow.
Consequences
- A
devbranch is created from the currentmainand becomes the default branch for development; branch protection should require PRs intodevand forbid direct pushes tomain. - Contributors branch from
dev, notmain. The default branch on the remote should be set todevso clones and PRs target it by default. - The release checklist gains a first step: merge
devintomainbefore tagging (and mergemainback intodevafterwards). The existing ADR-0020 / ADR-0023 release steps follow unchanged. - The
unstableGitHub pre-release and itsunstabletag are retired; users who want the latest build take the tip ofdevor a per-run Actions artifact. - Published documentation tracks
dev; a doc fix is visible once merged, not only after the next release. - Existing open work branched from
main(e.g. the branch introducing this ADR) is retargeted ontodev.