Choosing a branching model for a mobile release train Jump to heading

Web teams can deploy trunk on every green merge; mobile teams cannot. An iOS or Android build must clear app-store review — a queue you do not control, measured in hours to days — before a single user sees it. That review latency is the reason a purely trunk-based mobile team still needs release branches: the commit you submit must stay frozen while review runs, even as trunk keeps moving. This page works through the hybrid model that reconciles those two facts, building on the broader GitFlow vs GitHub Flow comparison for teams deciding how much branching structure they actually need.

The problem: review latency freezes a commit you can’t stop moving Jump to heading

On the web, “release” is an event that lasts seconds. On mobile it is a window. Between the moment you submit a binary and the moment it is approved, three things are simultaneously true:

  • The exact commit that produced the submitted binary must not change — reviewers may reject it and ask you to resubmit that build with one fix.
  • Trunk must keep accepting merges, because blocking twenty engineers for a two-day review queue is untenable.
  • If review surfaces a bug, you need to ship a fix against the submitted commit, not against a trunk that has since absorbed a dozen unrelated features.

Pure GitHub Flow — branch, PR, merge to main, deploy — has no place to park a frozen build while trunk advances. That single missing capability is what forces a release branch onto otherwise trunk-based mobile teams. Full GitFlow supplies release branches but also drags in a permanent develop branch, hotfix/* conventions, and a merge topology that most mobile teams do not need. The right answer is neither: it is a narrow hybrid.

The model: trunk stays trunk, release branches are per-train and disposable Jump to heading

Keep main as a single, always-releasable integration branch — every feature merges there via short-lived branches, exactly as in trunk-based development. The only addition is a release train: at feature freeze for version x.y, cut a release/x.y branch. That branch is the frozen line the store reviews against. Trunk never stops.

The rules that make this work:

  • One branch per train. release/8.4, release/8.5, and so on. Never a shared long-lived release branch.
  • Trunk-first fixes. Every fix lands on main first, then is cherry-picked onto the release branch — never the reverse. This guarantees no fix is lost when the train is deleted.
  • Backport only what’s approved. A commit reaches the release branch only if a release owner signs off. The default answer to “can this go in the release?” is no; the branch stays minimal so each store re-review diff is small.
  • Tag every build. Every binary submitted to review gets an annotated tag. Tags are the permanent record; the branch is disposable.
Release train topology: trunk keeps moving while a release branch is frozen and patched by cherry-pickHorizontal trunk line labeled main with several commits continuing rightward. At the feature-freeze point a release/8.4 branch splits downward. On the release branch three tagged builds are shown: v8.4.0-rc1, rc2, and the final v8.4.0. Two dashed cherry-pick arrows run from fix commits on main down to the release branch.main (trunk)fix Afix Bfeature freeze — cut release/8.4release/8.4v8.4.0-rc1v8.4.0-rc2v8.4.0 (shipped)cherry-pickapproved fixes

When pure GitHub Flow is enough — and when you need the hybrid Jump to heading

Not every mobile team needs release branches. Reach for the hybrid only when the failure modes above are real:

SignalPure GitHub FlowRelease-train hybrid
Store review latencyTolerable — you can freeze trunk briefly or use phased rollout onlyMulti-day queues force a parked, frozen commit
Release cadenceContinuous / weekly, low ceremonyFixed trains (e.g. every 2–4 weeks)
Post-submission fixesRare; you just resubmit from trunkExpected; reviewers reject and you patch the exact build
Parallel versions in reviewNever more than oneSometimes two trains overlap (n in review, n+1 stabilizing)
Team sizeSmall enough to pause mergesLarge enough that freezing trunk is unacceptable

If your review turnaround is fast and you rarely patch a submitted build, stay on GitHub Flow and resubmit from main — the branch overhead buys you nothing. The moment two versions can be in flight at once, or a rejection forces a surgical fix against a frozen commit, cut the release branch.

Step-by-step recipe Jump to heading

Step 1 — Cut the release branch at feature freeze Jump to heading

Branch directly from the trunk commit you have decided to ship. Do not branch from a stale local main; fetch first.

git fetch origin
# Cut the train from the exact freeze point on trunk
git switch -c release/8.4 origin/main
git push -u origin release/8.4

Verify the branch points at the same commit as trunk right now:

# Both SHAs must match immediately after the cut
git rev-parse release/8.4
git rev-parse origin/main

Step 2 — Tag the first build submitted for review Jump to heading

Tag the exact commit you hand to the build pipeline. Use an annotated tag so it carries author, date, and message — a lightweight tag records none of that.

# Annotated, per-build tag on the current release-branch HEAD
git tag -a v8.4.0-rc1 -m "Build submitted to App Store review 2026-07-05"
git push origin v8.4.0-rc1

Verify the tag is annotated and resolves to the submitted commit:

# 'tag' object type confirms it is annotated, not lightweight
git cat-file -t v8.4.0-rc1
git rev-list -n1 v8.4.0-rc1

Tagging conventions here are worth standardizing across trains — see Release Tagging & Versioning for the semantic-version and tag-message policy this recipe assumes.

Step 3 — Backport an approved fix with cherry-pick Jump to heading

A reviewer rejects the build for a crash. Fix it on main first, get it reviewed and merged, then move only that commit onto the release branch. Working trunk-first guarantees the fix survives when the train is deleted.

git fetch origin
git switch release/8.4
# Move exactly the approved fix commit; -x records the source SHA in the message
git cherry-pick -x <fix-sha-from-main>

If the cherry-pick conflicts, resolve, then continue — never abandon it half-applied:

# After editing the conflicted files
git add -A
git cherry-pick --continue
# Or, to back out entirely and reassess:
# git cherry-pick --abort

Verify the fix — and nothing else — landed on the branch:

# The '-x' line should name the original main commit
git show --stat HEAD
# Confirm the branch still contains only approved changes vs the cut point
git log --oneline v8.4.0-rc1..HEAD

The full mechanics of moving individual commits across long-lived lines — including how to handle commits that touch since-refactored code — live in Cherry-Pick & Backporting.

Step 4 — Tag the resubmitted build and ship the final one Jump to heading

Every resubmission is a new build and gets its own tag. When review finally passes, tag the shipped version.

git tag -a v8.4.0-rc2 -m "Resubmitted after crash fix (cherry-pick a1b2c3d)"
git push origin v8.4.0-rc2
# Once approved and released to production:
git tag -a v8.4.0 -m "Approved and released to App Store 2026-07-08"
git push origin v8.4.0

Verify the full build history of the train is intact and ordered:

# Lists every tagged build for this train, newest first
git tag --list 'v8.4.*' --sort=-creatordate

Step 5 — Reconcile the release branch back to trunk Jump to heading

Because every fix originated on main, reconciliation is usually a no-op — the commits are already there. Confirm nothing is stranded before you retire the train.

git fetch origin
# Any commit on the release branch NOT reachable from main is a stranded fix
git log --oneline origin/main..release/8.4

If that command prints commits, cherry-pick each back onto main (or open a PR) before deleting the branch. Once it prints nothing, the tags hold the permanent record and the branch is safe to remove:

git push origin --delete release/8.4

SAFETY WARNING: Never git push --force a shared release/x.y branch. Other engineers, CI, and the store-submission pipeline all build from its published commits; a force-push rewrites SHAs out from under them and can orphan the exact commit a submitted binary was built from. If you must correct a shared branch, add a new commit or a git revert instead. If a force-push already happened, recover the prior tip from the reflog on a machine that still has it — git reflog show release/8.4 — or reset the branch back to a preserved build tag: git switch release/8.4 && git reset --hard v8.4.0-rc2 && git push --force-with-lease, coordinating with everyone who tracks the branch first.

Validation checklist Jump to heading

Before treating the release-train model as adopted, confirm each item:

Frequently asked questions Jump to heading

Do I need a develop branch like GitFlow prescribes? Jump to heading

No. A mobile release train needs only trunk plus a short-lived release/x.y branch per version. GitFlow’s develop branch adds a permanent integration layer that duplicates trunk’s role, doubles the merge surface, and slows velocity for no benefit when your real constraint is store review latency. Keep main as your always-releasable integration branch and cut release branches only at feature freeze.

How long should a release branch live? Jump to heading

Only until that version is fully rolled out in production, plus a short tail for emergency patches. Once the next train’s release/x.y+1 is cut and the previous version is no longer receiving fixes, delete the branch. Its annotated tags preserve every shipped and submitted build permanently, so the branch itself is disposable — keeping it around only invites accidental commits to a dead line.

Why cherry-pick fixes instead of merging trunk into the release branch? Jump to heading

Merging trunk pulls in every unrelated change made since the freeze, which reintroduces unreviewed features into a build that must stay minimal for store re-review. Cherry-pick moves exactly the approved commits and nothing else, keeping the release branch auditable and the resubmission diff small enough for a reviewer — human or automated — to reason about. It also keeps each train’s history a clean superset of the frozen cut point.