Release Branches: Stabilising and Shipping a Version
A release branch is a branch created at the point a version’s scope is frozen, used to stabilise that version while ordinary development continues elsewhere.
It exists to answer one question: where do the fixes for the version we are shipping go, if main has
already moved on?
If your answer is “we ship whatever is on main right now”, you do not need release branches, and
adding them is pure cost. If your answer involves a testing period, a release candidate, or supporting a
version after the next one starts, you probably do.
The shape
Section titled “The shape”A trunk lane labelled main with commits A through E continuing after the branch point. A release lane labelled release slash 2.4 leaves main after commit B and receives two stabilisation commits R1 and R2, then merges back into main at commit E.
Three properties define it:
Scope is frozen at creation. Whatever is on main at that moment is the release. Features that land
afterwards belong to the next version.
Only fixes go on it. Bug fixes, version bumps, changelog entries, documentation corrections. A new feature on a release branch defeats the purpose.
Fixes must return to main. A fix made only on the release branch disappears when the next version
ships from main, reappearing as a “regression” that was never actually fixed on the development line.
What it buys you
Section titled “What it buys you”Development does not stop. Without a release branch, freezing for testing means freezing main —
everyone else stops or works on branches that pile up.
A stable target for testing. QA tests something that is not moving underneath them. A defect reported against build 2.4-rc2 can be reproduced.
A place for late fixes. A defect found on the last day of testing has an obvious home.
Support after the fact. If 2.4 ships and needs a patch while 2.5 is in development, the branch is still there.
Creating and working on one
Section titled “Creating and working on one”-
Branch when scope freezes.
Terminal window git switch main && git pullgit switch -c release/2.4git push -u origin release/2.4mainis immediately free. Work on 2.5 continues. -
Prepare the version. Bump version files, update the changelog, regenerate anything derived:
Terminal window git commit -am "Prepare 2.4.0" -
Tag release candidates if your process uses them:
Terminal window git tag -a v2.4.0-rc.1 -m "Release candidate 1"git push origin v2.4.0-rc.1 -
Fix defects on the branch as testing finds them.
-
Tag the release when it is accepted:
Terminal window git tag -a v2.4.0 -m "Release 2.4.0"git push origin release/2.4 --follow-tags -
Merge the fixes back to
main.Terminal window git switch main && git pullgit merge --no-ff release/2.4git push
Where to fix first
Section titled “Where to fix first”When a defect affects both the release and ongoing development, there are two orders, and the choice has consequences.
Fix on the release branch, merge forward to main. The usual approach in Git Flow. Natural when
the defect was found during release testing. The merge carries it to main automatically.
Fix on main, cherry-pick to the release branch. Better when the fix is also relevant to work in
progress, or when the release branch has diverged enough that the fix does not apply cleanly in reverse.
git switch release/2.4git cherry-pick -x a1b2c3dWhat it doesApplies the changes introduced by one commit onto the current branch as a new commit with a different ID.
Why we run itIt transplants a single fix between branches that are not going to be merged wholesale.
Expected resultA new commit on the current branch with the same message and content changes. Conflicts are possible if the surrounding code differs.
The -x flag appends a line recording where the change came from, which makes backports auditable later:
Fix crash when config file is empty
(cherry picked from commit 29c4de71dcd298ac5ea1ea5c730140aa1b44845c)[release/2.4 9f8e7d6] Fix crash when config file is empty Date: Sat Aug 22 14:02:11 2026 +0000 1 file changed, 3 insertions(+), 1 deletion(-)Backporting to older versions
Section titled “Backporting to older versions”Once you support more than one released version, fixes have to travel backwards. A security fix in 3.1 may also be needed in 3.0 and 2.9.
The mechanism is cherry-picking onto long-lived maintenance branches:
git switch release/3.0git cherry-pick a1b2c3dgit tag -a v3.0.4 -m "Release 3.0.4"
git switch release/2.9git cherry-pick a1b2c3dgit tag -a v2.9.11 -m "Release 2.9.11"The cost is real and grows with the number of supported versions. Each backport may conflict, needs independent testing, and produces another release. This is why supported-version policies exist: every version you promise to support is ongoing work.
Long-term support branches
Section titled “Long-term support branches”A release branch that outlives the next release becomes a maintenance branch. The mechanics are identical; what changes is the commitment.
A team supporting 3.x and 2.x concurrently keeps release/3.0 and release/2.9 alive indefinitely,
backporting security and critical fixes to each and cutting patch releases from them. Nothing new is
developed there — these branches only ever receive changes that originated elsewhere.
Two policies make this sustainable rather than open-ended:
A written support window. “The current minor version plus the previous one, for twelve months” bounds the work. Without a stated policy, every version you have ever shipped is implicitly supported forever.
A clear bar for what gets backported. Security fixes and data-loss defects, typically — not every bug, and never improvements. Each backport is a cherry-pick, a test cycle and a release, so the bar directly determines the cost.
Delete a maintenance branch when its version leaves support. The tags remain, so the history is not lost, and anyone who needs to inspect that version can branch from a tag again.
Release branches without Git Flow
Section titled “Release branches without Git Flow”Release branches are separable from the model that popularised them, and the reduced form is what many teams actually want:
- Develop on
main— trunk-based or GitHub Flow, nodevelopbranch. - When shipping, cut
release/2.4frommain. - Stabilise on it, tag, ship.
- Merge fixes back to
main. - Delete the branch when the version is no longer supported.
You get a stabilisation window and a home for late fixes without a permanent second integration branch. The branch exists for days or weeks, not forever, and there is only ever one place features land.
This combination — trunk-based development plus release-time branching — is a common shape for teams that deploy continuously internally but ship versioned artefacts externally.
When you do not need them
Section titled “When you do not need them”You deploy from main continuously. The deployed version is a commit on main; a tag records which
one. There is nothing to stabilise because stabilisation happens before merge.
You support exactly one version. No backporting means no maintenance branches.
Your test suite is the stabilisation phase. If automation catches what a manual cycle would, the cycle is redundant.
You ship rarely and can freeze main briefly. A two-hour freeze on a small team is cheaper than
branch machinery.
In these cases a tag on main gives you everything a release branch would, at no ongoing cost:
git tag -a v2.4.0 -m "Release 2.4.0"git push origin --follow-tagsCI/CD implications
Section titled “CI/CD implications”A release branch adds a second thing your pipeline must handle, and the details are worth settling before you cut the branch rather than during a release.
What runs on the release branch? At minimum everything that runs on main, plus whatever slower
checks you normally skip: full integration suites, performance tests, security scanning, packaging and
installer verification. The release branch is where you can afford a forty-minute pipeline.
What does the branch deploy to? A common mapping is that main deploys to a development environment
and the release branch deploys to staging, with production driven by tags. That last point matters:
if production deploys from a tag rather than a branch, your pipeline must trigger on tag pushes, and
git push --follow-tags becomes part of the release procedure rather than an afterthought.
Are release branches protected? They usually should be, and often more strictly than main — a
release branch under test is precisely where an accidental push does the most damage. Requiring review
for every change to a release branch is reasonable even on teams that allow lighter process elsewhere.
How are release branches recognised? Use a consistent prefix (release/*) so pipeline rules,
protection rules and cleanup automation can match on it rather than on a list someone maintains by hand.
Does the merge-back run automatically? Some teams automate it: when a release branch is tagged, open
a pull request merging it into main. That converts the most commonly forgotten step into something
visible.
Naming and version numbers
Section titled “Naming and version numbers”Two small conventions prevent a surprising amount of confusion.
Name the branch for the series, not the patch. release/2.4 rather than release/2.4.0, so that
2.4.1 and 2.4.2 ship from the same branch. Creating a new branch per patch release means recreating it
from a tag each time and re-establishing which fixes it already has.
Let tags carry exact versions. The branch is the line of maintenance; the tags are the points on it:
release/2.4 ──●──────●──────● v2.4.0 v2.4.1 v2.4.2Use annotated tags (git tag -a), which are real objects carrying a tagger, date and message, and can be
signed. Git Objects Explained covers why that differs from a
lightweight tag.
Common mistakes
Section titled “Common mistakes”Letting features onto the release branch. “Just this one small thing” restarts the testing you were trying to finish.
Forgetting the merge back. The single most common release-branch bug.
Treating the release branch as the new mainline. If development migrates to it, you have renamed
main and gained nothing.
Keeping release branches forever. A repository with forty of them is noise. Delete when a version leaves support; the tag preserves the history.
Cherry-picking without tracking. Without a record of what has been backported, you will miss one.
git cherry -v is the audit.
Branching too early. Cutting the release branch a fortnight before you ship means a fortnight of double maintenance. Freeze scope when you actually mean to freeze it.
Mental Model
Section titled “Mental Model”A release branch is a photograph of
mainthat you are allowed to retouch.It captures what the product will be at a moment, then accepts only corrections. Development continues from the original scene; the photograph does not change with it. When the picture is right you frame it — that is the tag — and send the corrections back so the next photograph starts from a better scene.
What You Learned
Section titled “What You Learned”- A release branch freezes scope so a version can be stabilised while development continues.
- Only fixes belong on it; features restart the testing it exists to enable.
- Fixes must be merged back to
main, and this is the step teams most often miss. - Cherry-picking moves individual fixes between branches, creating new commits with new IDs.
- Backporting cost scales with the number of supported versions.
- Release branches work without Git Flow, created at release time and deleted when support ends.
- If you deploy continuously from
main, a tag does everything you need.
Try It Yourself
Section titled “Try It Yourself”- Create a repository with three commits on
mainand tag itv1.0.0. - Add two more commits to
main— these are “1.1 features”. - Cut
release/1.1frommain. - Add a commit to
main— a “1.2 feature” that must not be in 1.1. - Add a fix commit to
release/1.1. - Tag
release/1.1asv1.1.0. - Before merging back, run
git log --oneline mainand confirm the fix is absent. - Merge back:
git switch main && git merge --no-ff release/1.1. - Verify:
git merge-base --is-ancestor release/1.1 main && echo ok. - Confirm the 1.2 feature is not reachable from the
v1.1.0tag:git log --oneline v1.1.0.
Steps 7 and 10 are the two properties that matter: the fix does not reach main by itself, and the
release genuinely excludes work that landed after the freeze.
Next Lesson
Section titled “Next Lesson”You have seen every model this cluster covers. The final lesson compares them systematically.