Migrating from GitFlow to GitHub Flow Jump to heading
GitFlow’s develop, release/*, and hotfix/* branches encode a release cadence that most continuously deployed teams have outgrown. Every change pays a double-merge tax — feature into develop, develop into a release branch, release branch into main, then main back into develop — and each hop is a place for divergence, forgotten back-merges, and “which branch is actually production?” confusion. Moving to GitHub Flow collapses that topology to a single protected trunk with short-lived branches merging directly into it. This page is a staged, reversible migration recipe; for the underlying trade-offs and when the move is justified at all, start from GitFlow vs GitHub Flow: A Comparison.
When to use this approach Jump to heading
Run this migration when:
- Your team already deploys to production multiple times per week, so the batching that
release/*branches provide is friction rather than safety. - Back-merge drift is a recurring incident source — fixes land on
mainviahotfix/*but someone forgets to merge them back todevelop, and the bug resurfaces in the next release. - You are adopting Trunk-Based Development Setup and want GitHub Flow as the pull-request-centric stepping stone toward it.
- Feature flags are available (or can be introduced), so incomplete work can ship to
maindormant instead of being parked on a long-lived integration branch.
This recipe is not the right fit if you ship versioned artifacts to customers who pin specific releases (firmware, SDKs, on-prem installers) — those teams usually need the maintenance-branch model that GitFlow’s release/* lineage supports. In that case, evaluate the sibling scenario in Choosing a Branching Model for a Mobile Release Train before committing.
The diagram below shows the target end-state: the two-branch integration path collapses into one trunk that both feeds deploys and receives short-lived branches.
Step-by-step migration Jump to heading
Step 1 — Freeze new release and hotfix branches Jump to heading
The migration must run against a stationary target. Stop anyone from cutting new release/* or hotfix/* branches so the set of branches you have to reconcile is finite. Announce the freeze, then confirm the current inventory:
# List every long-lived GitFlow branch still on the remote
git fetch --all --prune
git branch -r | grep -E 'origin/(develop|release/|hotfix/)' Existing release branches already in flight may finish their current cycle, but no new ones start. Record the frozen inventory so Step 5 can verify nothing was missed:
# Snapshot the branch list and their tip SHAs for the audit trail
git for-each-ref --format='%(refname:short) %(objectname:short)' \
refs/remotes/origin/develop refs/remotes/origin/release refs/remotes/origin/hotfix \
| tee migration-branch-inventory.txt Step 2 — Reconcile develop into main Jump to heading
develop almost always contains reviewed commits that have not yet reached main. These must land on main before develop can be retired. Create a backup ref first, then merge with a real merge commit so history and authorship survive:
# Immutable backup ref — your recovery anchor if anything goes wrong
git fetch origin
git branch backup/develop-pre-migration origin/develop
git push origin backup/develop-pre-migration
# Reconcile on a dedicated branch, never directly on origin/main
git switch main
git pull --ff-only origin main
git switch -c chore/reconcile-develop
git merge --no-ff origin/develop -m "chore: reconcile develop into main for GitHub Flow migration" Resolve any conflicts, then verify that main now contains every commit develop had — the command below must print nothing:
# Any SHA printed here is a develop commit MISSING from the reconciliation branch
git log --oneline origin/develop --not chore/reconcile-develop Open this reconciliation as a normal pull request into main so it passes the same review and CI gate as any other change. If your team merges with rebase elsewhere, keep this one as a merge commit — see the merge-versus-squash reasoning in the FAQ.
Step 3 — Retarget open pull requests to main Jump to heading
Every open PR currently pointing at develop (or a release/* branch) will become unmergeable the moment its base disappears. Retarget them to main before you delete anything. List them first:
# Requires the GitHub CLI; lists open PRs whose base is develop
gh pr list --base develop --state open \
--json number,title,headRefName --limit 200 Retarget each one. GitHub recomputes the diff against the new base automatically:
# Change the base branch of a single PR
gh pr edit <number> --base main After retargeting, verify no open PR still targets a doomed branch — this list must be empty:
gh pr list --state open --json number,baseRefName \
--jq '.[] | select(.baseRefName == "develop" or (.baseRefName | startswith("release/")))' Authors whose branches were cut from develop should rebase onto main so their PR diff is clean. Branch hygiene during this step is covered in depth by Feature Branch Isolation.
Step 4 — Update branch protection and CI triggers Jump to heading
With main now the single integration target, its protection rules and your pipeline triggers must reflect that. Point required status checks and review rules at main, and remove develop from any ruleset:
# Inspect main's current protection so you know what to preserve
gh api repos/:owner/:repo/branches/main/protection
# Delete the now-obsolete protection on develop
gh api -X DELETE repos/:owner/:repo/branches/develop/protection Rewrite CI triggers so pipelines fire on main and pull requests, not on develop or release branches. A typical GitHub Actions change:
# .github/workflows/ci.yml
on:
push:
branches: [main] # was: [develop, main, 'release/*']
pull_request:
branches: [main] # validate every PR targeting the trunk Verify the workflow parses and no lingering job still references the old branches:
# Should return no matches once the migration is complete
grep -rnE 'develop|release/|hotfix/' .github/workflows/ Coordinating what runs on push versus pull request is the subject of Trunk-Based Development Setup, which this pipeline shape moves you toward.
Step 5 — Retire the develop branch safely Jump to heading
Only after Steps 2–4 are green — develop fully reconciled into main, every PR retargeted, protection and CI moved — do you delete develop. This ordering is not optional.
SAFETY WARNING: Deleting
developbefore it is fully reconciled intomainpermanently orphans any commit that lived only ondevelop— once the branch ref is gone, those commits are unreachable and will eventually be garbage-collected. If you deleted too early, recover immediately from the backup ref created in Step 2 withgit push origin backup/develop-pre-migration:refs/heads/develop, or, if you skipped the backup, from the local reflog withgit reflog show origin/developfollowed bygit branch develop <sha>before GC runs (defaultgc.reflogExpireUnreachableis 30 days).
Run the reconciliation-completeness check one final time, then delete:
# MUST print nothing — proves no develop-only commits remain
git fetch origin
git log --oneline origin/develop --not origin/main
# Delete the remote branch, then prune local tracking refs
git push origin --delete develop
git remote prune origin Keep backup/develop-pre-migration for at least one release cycle before removing it. It is cheap insurance and the only thing standing between you and unrecoverable history if a missed commit surfaces later.
Step 6 — Adopt deploy-from-main with feature flags Jump to heading
The workflow is only truly migrated once main is deployable at every commit. Wire deployment to main so a merge is a release, and gate anything not ready to be seen behind a flag rather than parking it on a branch:
# Deploy pipeline reacts to main; a merged PR is a candidate release
# Tag the release from main so history stays linear and auditable
git switch main && git pull --ff-only origin main
git tag -a "v$(date +%Y.%m.%d)" -m "Release from main"
git push origin --tags Wrap incomplete features so they can merge to trunk dormant:
// Ship to main behind a flag; enable per-environment when ready
if (flags.isEnabled('checkout-v2')) {
renderCheckoutV2();
} else {
renderCheckoutV1();
} Verify main is genuinely release-ready by confirming the deploy pipeline runs green on the current tip:
# Latest run for the main branch must be a success
gh run list --branch main --limit 1 --json conclusion,status Validation checklist Jump to heading
Confirm every item before declaring the migration complete:
Frequently asked questions Jump to heading
Do we have to squash-merge develop into main, or should we merge with history? Jump to heading
Merge with a true merge commit, not a squash. develop typically holds dozens of already-reviewed commits, and squashing them into a single node destroys authorship attribution and git bisect granularity across a large slice of history. Use git merge --no-ff so the reconciliation appears as one clean, revertible topology point while every underlying commit is preserved intact.
How do we handle a production hotfix during the migration window? Jump to heading
Once main is the single source of truth, a hotfix is just a short-lived branch off main that merges straight back and deploys — there is no separate hotfix branch class and no back-merge to develop, because develop no longer exists. During the transition window itself, cut the fix from main and, while develop is still live, cherry-pick it forward with git cherry-pick <sha> until develop is retired in Step 5.
What if long-running feature branches cannot merge before we retire develop? Jump to heading
Rebase them onto main and hide the incomplete work behind a feature flag so the branch can merge to trunk unfinished but dormant. GitHub Flow assumes short-lived branches; a feature that cannot land within a few days is a signal to decompose it into smaller increments, not a reason to keep develop alive as a staging area. If several such branches exist, sequence their landing before Step 5 rather than blocking the whole migration on them.
Related Jump to heading
- GitFlow vs GitHub Flow: A Comparison — the parent overview of both models, their trade-offs, and the decision criteria that justify this migration.
- Choosing a Branching Model for a Mobile Release Train — the sibling scenario for teams whose store-review cadence may still warrant release branches instead.
- Trunk-Based Development Setup — where deploy-from-main and short-lived branches lead next, with CI and merge-frequency guidance.
- Feature Branch Isolation — keeping the now-short-lived branches clean, rebased, and conflict-free as they merge directly to trunk.