Maintaining release branches for patch versions Jump to heading
Once a project has users who cannot upgrade immediately, a fix on main is not enough: users on 2.3 need 2.3.5, not a jump to 2.5 with new features and new risk. Maintenance branches make that possible — one branch per supported minor version, receiving only fixes, each fix tagged as a patch release. The pattern is simple to describe and easy to let sprawl: branches created ad hoc, fixes landed on the old branch and never forward-ported, support windows that never end. This page sets up maintenance branches with a consistent name, a clear fix flow, patch tagging and an explicit end of support, within release tagging and versioning.
When to use this approach Jump to heading
- You support more than one minor version of a library, product or API at a time.
- Customers or downstream projects pin to a minor version and need security fixes.
- Fixes sometimes reach old versions but not
main, or the reverse. - You want a written policy for how long each version receives patches.
Step 1 — Name branches by minor version Jump to heading
Use one branch per minor version, named predictably so automation can find them: release/2.3, release/2.4. The branch name carries no patch number, because patches are tags on the branch.
git branch -r --list 'origin/release/*' --sort=-version:refname
# origin/release/2.5
# origin/release/2.4
# origin/release/2.3 Step 2 — Create the branch from the release tag Jump to heading
Create a maintenance branch only when the first patch for that version is needed, starting from its .0 tag (or the latest patch tag). Creating them lazily avoids branches nobody uses.
git fetch --tags origin
git switch -c release/2.4 v2.4.0
git push -u origin release/2.4 Protect the branch so only pull requests with passing checks can change it, using the same ruleset pattern as main.
Step 3 — Land fixes on main first, then cherry-pick down Jump to heading
A fix lands on main through a normal pull request, then is cherry-picked to each supported branch, newest first. Fixing main first guarantees no future release regresses.
fix=abc1234 # merged to main
for b in release/2.5 release/2.4 release/2.3; do
git switch "$b" && git pull --ff-only
git cherry-pick -x "$fix" || { echo "conflict on $b — resolve, then continue"; break; }
git push
done The exception is a fix for code that no longer exists on main. Land it on the newest branch that has the code, and record why in the commit message. Automation is covered in automating backport pull requests with labels.
Step 4 — Tag each patch release Jump to heading
When a branch has accumulated the fixes for a patch, tag the branch tip with the next patch number. Derive it from the existing tags rather than typing it.
git switch release/2.4 && git pull --ff-only
last=$(git describe --tags --abbrev=0 --match 'v2.4.*') # v2.4.2
next=$(echo "$last" | awk -F. '{printf "%s.%s.%d", $1, $2, $3+1}') # v2.4.3
git tag -s "$next" -m "Release ${next#v}"
git push origin "$next" Step 5 — Check that no fix is missing from a newer line Jump to heading
The most common maintenance failure is a fix that reached release/2.3 but not release/2.4, so upgrading reintroduces the bug. Check that every cherry-picked source on an older branch also reached each newer one.
srcs() { git log --format=%B "origin/$1" --not "v$2.0" | sed -n 's/.*cherry picked from commit \([0-9a-f]*\).*/\1/p' | sort -u; }
srcs release/2.3 2.3 > /tmp/old.txt
srcs release/2.4 2.4 > /tmp/new.txt
comm -23 /tmp/old.txt /tmp/new.txt | while read -r c; do
git merge-base --is-ancestor "$c" v2.4.0 || echo "fix $c is on 2.3 but not on 2.4"
done Step 6 — Publish and enforce a support window Jump to heading
Decide how many minor versions receive fixes, and for how long, and publish it. When a version leaves its window, archive its branch: protect it against all pushes rather than deleting it, so the history stays visible.
# Lock an end-of-life branch with a ruleset that blocks all updates
gh api -X POST "repos/$OWNER/$REPO/rulesets" --input - <<'JSON'
{"name":"eol-2.2","target":"branch","enforcement":"active",
"conditions":{"ref_name":{"include":["refs/heads/release/2.2"],"exclude":[]}},
"rules":[{"type":"update"},{"type":"deletion"}]}
JSON Several maintained lines are compared with simpler alternatives in supporting multiple maintained versions.
Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Why not merge the old branch forward instead of cherry-picking down? Jump to heading
Merging forward (2.3 into 2.4 into main) works when fixes are made on the oldest branch first, and some projects prefer it. It drags any branch-specific adaptation forward too. Fix-on-main with cherry-picks keeps main authoritative and is easier to automate.
What if a cherry-pick conflicts on an old branch? Jump to heading
Resolve it on that branch, keep the -x reference, and note the adaptation in the message. See resolving cherry-pick conflicts on older branches.
Do maintenance branches need full CI? Jump to heading
Yes. Old branches often use older toolchains, so keep their pipeline definitions on the branch itself and run them on every pull request.
Related Jump to heading
- Release Tagging & Versioning — the parent topic.
- Cherry-Picking Hotfixes Across Release Branches — the fix flow in detail.
- Cutting Release Branches from Trunk — short-lived release branches.
- Deriving Versions from git describe — finding the latest patch tag.