ADR 011: Release branches are lazily cut, downstream-only, and never silently diverge
Status
Accepted
Context
The v2 launch left main (v1) and release/v2 significantly drifted, because
new development landed on release/v2 while main stayed on v1. Go's
semantic-import-versioning module paths make this especially expensive to
recover from: every touched file carries /v1 vs /v2 import suffixes, so
cross-line merges conflict in nearly every file and effectively become
rewrites rather than merges. Drift between major lines therefore compounds
fast and doesn't heal on its own.
Tooling reinforces the problem today: release-please fires only on main
(.github/workflows/stable-release.yml), so there is no supported path for
patching — or extending — a shipped major once main has moved on. That gap
becomes acute whenever a new ServiceNow API surface ships mid-cycle that
consumers still pinned to the previous major want now, not at the next
major release.
Alternatives considered:
- GitFlow-style per-major development — develop v2 work directly on
release/v2, as happened before. Rejected: this is the drift incident; the failure mode is structural, not incidental. - Tags-only, no maintenance branches — cut an ephemeral branch per patch, tag, delete. Rejected: once more than one patch accumulates, the line needs a durable target for PRs, required CI, and a release-please config; repeated ad-hoc cuts invite inconsistency.
- Preemptive branch per minor (
release/vX.Y.0at every minor) — rejected: most minors never receive a patch, and idle long-lived branches are exactly how silent drift happens again.
Decision
Adopt trunk-first development with lazily-cut, downstream-only maintenance branches (maintainer decision, 2026-08-21):
mainis always the tip of the next major's development.- Maintenance branches are named
release/vX.Y(no patch segment) and are created lazily from the lastvX.Y.*tag at first need —git branch release/v2.4 v2.4.5 && git push origin release/v2.4. Tags are immutable, so when the branch is cut is irrelevant; never cut one preemptively. - Downstream-only within a major: fixes land on
mainfirst, then move down via a label-triggered backport action (backport release/v2.xopens the cherry-pick PR). Direct pushes torelease/*are blocked; all changes arrive by PR with required CI. - Cross-major features (a new ServiceNow capability arriving while
maindevelops the next major):- Useful for both majors and small/portable (typical new API module) —
build on
main, ship in the next major, then open a matching PR againstrelease/vX.Y, rewriting the import suffix (/vN+1→/vN). - Useful for both but touching diverged core — build on
main; backport only if consumer demand justifies the port cost, otherwise document the change as next-major-only in its PR. - Old-major-only by design (depends on prior-major model shapes) — PR
targets
release/vX.Ydirectly and ships as a minor bump (feat:→vX.(Y+1).0), but triggers rule 5 below.
- Useful for both majors and small/portable (typical new API module) —
build on
- Drift is tracked debt, never silent: any merge into
release/v*without backport provenance automatically opens aneeds-forward-portissue ("assess porting #N tomain"). It closes only by porting the change up or by recording an explicit "won't port" rationale. - Releases off maintenance lines are batched and demand-driven, driven
by Conventional Commits through a second release-please configuration
scoped to
release/v*refs. Weekly preview releases remainmain-only.VERSIONandCHANGELOG.mdstay release-please-owned on every branch — never hand-edited. - EOL policy: vocabulary — current means the highest tagged major,
prior any tagged major below it. The current major's latest minor
receives fixes and features until the successor major tags its first
stable release; from that moment every prior major drops to
critical/security fixes only. Its
release/*branch is deleted at EOL with an announcement in the release notes. Forgotten long-lived branches are how the v2 incident happens twice.
The contributor-facing walkthrough of these rules — where changes land,
how to manage backports and forward-ports, and worked examples — lives on
the docs site at website/docs/contributing/release-branches.md.
Follow-up tooling work implied by this decision: a stable-release job
keyed on release/v* refs with an alternate release-please config, the
backport label action, the forward-port tracker workflow, and
branch-policy.yml updates covering release/*.
Consequences
- Pros: drift becomes visible, tracked debt instead of silent divergence; consumers on shipped majors get a predictable path to fixes and selected features; maintenance releases gain real CI, changelogs, and tagging via release-please instead of hand-run tags.
- Cons: cross-major ports stay semi-manual — import-suffix differences conflict in nearly every touched file, so ports must be kept small; two release-please configs and several new workflows add maintenance surface; rules 4–5 depend on reviewer discipline that automation only partially enforces.
- Rule for future release questions: don't create preemptive
release/*branches, never land feature work on a maintenance branch without completing the forward-port assessment, and don't treat a maintenance branch as a development trunk — if v2-era work starts feeling like active development again, that's the signal to re-evaluate the EOL clock, not to grow the branch.