Cutting release branches from trunk Jump to heading
Pure trunk-based development deploys trunk directly. Many teams cannot: mobile apps go through store review, installed software ships on a schedule, enterprise customers need a stable build for a week of acceptance testing. Trunk-based development handles this with release branches that are deliberately limited: cut from trunk as late as possible, never developed on, receiving only fixes cherry-picked from trunk, and deleted or locked once the release is done. They exist so trunk can keep moving while a release stabilises, not as a second place where work happens. This page sets that pattern up and enforces its limits, within trunk-based development setup.
When to use this approach Jump to heading
- You practise trunk-based development but cannot deploy trunk continuously to every target.
- A release needs a stabilisation period while trunk continues to change.
- You ship to app stores, on-premise customers, or on a scheduled release train.
- If you deploy every merge to production, you probably need no release branches; see running releases with GitHub Flow.
Step 1 โ Cut the branch as late as possible Jump to heading
Cut the release branch from a green trunk commit just before stabilisation starts. Cutting early means more fixes to cherry-pick and more time for the branch and trunk to drift.
git fetch origin
# Choose the latest trunk commit with a green pipeline
sha=$(gh run list --branch main --workflow ci.yml --status success --limit 1 --json headSha --jq '.[0].headSha')
git switch -c release/2026.10 "$sha"
git push -u origin release/2026.10
git tag -a v2026.10.0-rc.1 -m "Release candidate 1" && git push origin v2026.10.0-rc.1 Step 2 โ Fix on trunk first, then cherry-pick Jump to heading
Every fix for the release goes into trunk through a normal pull request, then is cherry-picked to the release branch. This guarantees the next release, cut from trunk, already contains it.
# After the fix merges to main as abc1234
git switch release/2026.10 && git pull --ff-only
git cherry-pick -x abc1234
git push Recording the source of each cherry-pick is covered in recording backports with cherry-pick -x, and automation with labels in automating backport pull requests with labels.
Step 3 โ Enforce โno development on the release branchโ Jump to heading
The rule that makes this work is also the easiest to break: someone needs a quick change for the release and commits it to the release branch directly. Enforce that every commit on a release branch carries a cherry-pick reference to trunk.
# CI on pull requests targeting release/*: every commit must be a recorded cherry-pick from main
base=$(git merge-base "origin/$BASE_REF" HEAD)
for c in $(git rev-list --no-merges "$base..HEAD"); do
src=$(git log -1 --format=%B "$c" | sed -n 's/.*cherry picked from commit \([0-9a-f]*\).*/\1/p')
if [ -z "$src" ] || ! git merge-base --is-ancestor "$src" origin/main; then
echo "::error::$(git log -1 --format='%h %s' "$c") is not a cherry-pick of a commit on main"; exit 1
fi
done Step 4 โ Tag candidates and the final release on the branch Jump to heading
Each build sent for testing gets a release-candidate tag; the final build gets the release tag. Tags on the release branch record exactly what was shipped, even though the branch itself is temporary.
git tag -s v2026.10.0 -m "Release 2026.10.0" origin/release/2026.10
git push origin v2026.10.0 Tagging conventions for candidates are in tagging pre-releases and release candidates.
Step 5 โ Retire the branch quickly Jump to heading
Once the release ships and no further patch is expected, lock or delete the branch. The tags preserve everything needed to rebuild or patch it later โ a patch release can always start a new branch from the release tag.
# Delete the branch; the tag keeps the history reachable
git push origin --delete release/2026.10
# Later, if a patch is needed:
git switch -c release/2026.10 v2026.10.0 If customers need long-term patches, keep the branch within a support window instead, as in supporting multiple maintained versions.
Step 6 โ Keep trunk releasable regardless Jump to heading
Release branches can become a crutch: if trunk is often broken, teams cut release branches early to escape it. The cure is to keep trunk green, so a release can be cut from any recent commit.
# How often was main green over the last 50 runs?
gh run list --branch main --workflow ci.yml --limit 50 --json conclusion \
--jq '(map(select(.conclusion=="success")) | length) as $g | "\($g)/\(length) green"' Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Isnโt a release branch a step away from trunk-based development? Jump to heading
Not if it is short-lived and receives only cherry-picked fixes. Trunk remains the only place where development happens; the release branch is a stable snapshot with a few patches.
What if a fix cannot be cherry-picked cleanly? Jump to heading
Resolve the conflict on the release branch, keeping the cherry-pick reference, and note the adaptation. The analysis is in resolving cherry-pick conflicts on older branches.
Should release branches have their own CI? Jump to heading
Yes โ the full release pipeline, including packaging and any long suites. Trunkโs pre-merge pipeline can stay fast because releases are tested thoroughly on their branch.
Related Jump to heading
- Trunk-Based Development Setup โ the parent topic.
- Running a Scheduled Release Train โ release branches on a fixed schedule.
- Keeping Trunk Green โ what makes late cuts possible.
- Cherry-Picking Hotfixes Across Release Branches โ the fix flow in practice.