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
Maintenance branches and their patch tagsMain carries new development. Each supported minor version has a release branch created from its .0 tag. A fix lands on main first and is cherry-picked to each supported branch, where it is tagged as the next patch release.one fix, landed on main, tagged on every supported linemain2.3.02.4.02.5.0fixrelease/2.4fix'2.4.3release/2.3fix'2.3.7patch numbers live in tags; branches are named by minor version only Maintenance branches and their patch tagsMain carries new development. Each supported minor version has a release branch created from its .0 tag. A fix lands on main first and is cherry-picked to each supported branch, where it is tagged as the next patch release.one fix, landed on main, tagged on every supported linemain2.3.02.4.02.5.0fixrelease/2.4fix'2.4.3release/2.3fix'2.3.7patch numbers live in tags; branches are named by minor version only

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
Which branches does this fix go to?A security fix goes to every branch still within its security window. An ordinary bug fix goes to branches within their full support window. A change that is not a fix — a feature or refactoring — goes only to main, and reaches users in the next minor release.What kind of change is it?security fixAll security-supportedincl. oldest linesbug fixFully supported linesnewest firstfeature / refactormain onlynext minor releasewrite the windows down so this is a lookup, not a debate Which branches does this fix go to?A security fix goes to every branch still within its security window. An ordinary bug fix goes to branches within their full support window. A change that is not a fix — a feature or refactoring — goes only to main, and reaches users in the next minor release.What kind of change is it?security fixAll security-supportedincl. oldest linesbug fixFully supported linesnewest firstfeature / refactormain onlynext minor releasewrite the windows down so this is a lookup, not a debate

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
The life of a maintenance branchThe branch is created when the first patch is needed after the minor release. It receives bug and security fixes during full support, then only security fixes once a newer minor version has been out for a while, and is locked at end of life with its tags preserved.Minor releasetag on main2.4.0Branch createdfrom v2.4.0first patchFull support endssecurity only2.5 + 2.6 outEnd of lifebranch lockedpolicy dateillustrative windows — the point is that each stage has a date The life of a maintenance branchThe branch is created when the first patch is needed after the minor release. It receives bug and security fixes during full support, then only security fixes once a newer minor version has been out for a while, and is locked at end of life with its tags preserved.Minor releasetag on main2.4.0Branch createdfrom v2.4.0first patchFull support endssecurity only2.5 + 2.6 outEnd of lifebranch lockedpolicy dateillustrative windows — the point is that each stage has a date

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.