DevOps · Release Engineering
Release Trains, Cut Points, and Hotfix Branches
How a release branch is cut from a moving mainline, what the cut point guarantees, and how a hotfix reaches both the released line and the mainline without duplicating the change.
A branching strategy is not a workflow preference. It is a set of rules about which commits are eligible to become a release, and every rule has a failure mode attached. The common arrangements differ in one variable: how much of the history between two releases is shared with the line that produces the next one.
The distinction that matters operationally is between the line where integration happens continuously and the line that a released artifact was built from. Everything else, including naming, cadence, and branch count, follows from how the two are kept in sync.
The cut point is a snapshot, not a merge
A release branch is created at a commit on the mainline. That commit is the cut point, and what it fixes is the content of the release: everything merged before it is in, everything merged after it is not. A branch created with git branch release-1.4 <sha> is a name pointing at a frozen tree; the mainline keeps moving and the branch does not.
The cut point is the source of a class of defects that no test finds. A commit merged to mainline an hour after the cut, containing the fix for a defect found during release validation, is not in the release. The people who wrote it believe it shipped. The branch’s commit graph says otherwise, and the graph is correct.
The mitigation is to make the cut point explicit and visible: tag it, and make the release notes generated from the range between the previous release tag and this tag: the mechanism described in deriving a changelog from commit messages. When the range is wrong, the notes and the artifact disagree, and that disagreement is detectable.
A second property of a branch as a snapshot: it is not immutable. Commits can be added to a release branch, which is exactly what happens during a hardening window. What a release branch guarantees is not that its content equals the cut point, but that its content is a superset of the cut point and a subset of what the release process admits. The set of admissions is the policy.
Freeze windows and what they actually freeze
Projects speak of a code freeze before a release. A freeze is rarely about the code; it is about the eligibility rule. During a hardening window on a release branch, only certain classes of change are admitted: fixes for defects blocking the release, documentation corrections that ship with the artifact, and changes to build configuration that reduce risk. Feature work continues on the mainline throughout.
The reason for a freeze is concentration of risk, not process aesthetics. Each additional change to the release branch adds a validation cost proportional to the surface it touches, and a hardening window has a fixed budget of validation. Without an admission rule, the branch converges to the mainline’s instability while carrying the expectation of a stable artifact.
The cost is the double maintenance that admission rules create. A fix admitted to the release branch also has to land on the mainline, or the next release reintroduces the defect. This is the forward-port problem, and its mirror is the backport problem, and neither is solvable in general: a fix written against an old tree does not apply to a tree that has since refactored the code it touches.
A freeze also interacts with the version number. A branch that keeps accepting patches after its cut produces a sequence of patch releases on the same minor, each differing from the cut by a small and declared set of changes. That is exactly the situation semantic versioning as a compatibility contract requires: no behaviour change outside the declaration, and never a re-publish of a coordinate that already exists.
Forward-port and backport are different operations
A backport copies a change from a newer line to an older one. A forward-port copies a change from a release line to the mainline. Git’s cherry-pick does the mechanical part: compute the diff between a commit and its parent, apply it to the target. Three things make it not a copy.
The first is context. The diff’s hunks carry surrounding lines, and a target whose surrounding lines changed will reject the hunk. This is a genuine merge problem, and the right response is a manual resolution that preserves the intent, which may involve rewriting the change against the new code.
The second is identity. A cherry-picked commit gets a new hash and does not record any link to its source, so a later attempt to cherry-pick the same commit again will re-apply it. Git’s -x flag appends a line to the message naming the source commit, which makes the relationship visible to a human and to tooling; without it, the provenance of a backport is invisible.
The third is partiality. A commit is often one step in a change spread over several commits, and picking it alone produces a tree that never existed. The symptom is a build that fails on the release branch after a hotfix that compiled fine everywhere else.
The safest arrangement is that fixes are authored on the oldest line that needs them and merged forward, because changes designed against old code apply forward trivially, while the reverse requires rewriting. Where that is impossible, because the defect exists only in the newer code, the fix is authored on the mainline and backported deliberately, with the backport reviewed as a change in its own right rather than as a mechanical replay.
Hotfix path versus deferred release
When a released version has a defect serious enough to warrant a release outside the cadence, the branch model decides how much work that is. Two shapes are common.
In the first, the hotfix is committed directly on the release branch, tagged, built, and then merged or cherry-picked to the mainline. The released line is the authority; the mainline catches up. This is fast and it keeps the fix identical to what was validated, but it means the mainline temporarily lacks a fix that production has, and any test on the mainline written against the fixed behaviour fails until the merge lands.
In the second, the fix is made on the mainline, validated there, and backported. The mainline is always the authority. The cost is that the fix passes through a full mainline validation cycle before reaching production, which may not fit an incident timeline, and the backport must be re-validated on the old tree regardless.
Neither is universally right. The determining factor is how long the two lines stay apart: a release line that receives one hotfix and is then closed can afford the first shape, because the divergence is short-lived and bounded. A release line that must be maintained for a long support window requires the second, because the divergence persists for months and a growing set of forward-ports would otherwise be needed.
Long-lived maintenance lines and the merge cost
Support windows create more than one live release branch. With two maintained minors plus a mainline, a single fix may need to reach three or four trees. Each tree’s history diverges further with time, so the applicability of any given change degrades.
The standard mitigation is to keep the change small and to keep the reasoning in the commit message rather than in the diff’s context: a message that explains why a value was wrong lets a maintainer re-derive the fix against rewritten code, while a message that says “change 500 to 200” does not. This is the same principle that makes a changelog entry auditable, since the reader of a release note has no access to the diff either.
A second mitigation is structural: keep the code paths that receive hotfixes on older lines as small and as isolated as possible, so the surface a backport has to touch is bounded. Code that has been repeatedly refactored between the maintained lines is a backport liability, and that liability is a real cost of choosing to maintain those lines.
Reading the topology
Two queries answer most questions about what a branch model is doing. git describe --tags on a commit reports the nearest reachable tag plus the number of commits since, which tells whether a candidate build sits on a released point or beyond it. git log --first-parent --oneline <tagA>..<tagB> shows the merges that constitute a release when the mainline is merge-based, which is the list a release manager actually reviews.
The rule of thumb that follows from all of this: a change should be authored on the line where it will be validated longest, and travel in the direction that keeps the set of differences monotonically shrinking. Every branch model that works obeys that; every one that fails to, produces backports that stop applying, hotfixes that never reach the next release, and changelogs that describe commits the artifact does not contain.
References
- Git documentation — git-branch — Git project
- Trunk Based Development — trunkbaseddevelopment.com
- Git documentation — git-describe — Git project
- Version Control with Git — Loeliger & McCullough, O'Reilly Media