Handling hotfixes in GitFlow Jump to heading

In GitFlow, develop carries unreleased work, so a production bug cannot be fixed there and shipped β€” the fix would drag every unreleased feature along with it. The model’s answer is a hotfix branch: cut from main, which holds exactly what is in production, fixed, released as a patch version, and then merged back so the fix also reaches the next release. The procedure is simple; the failure modes are in the details. A hotfix merged into main but not develop reappears in the next release. A hotfix merged into develop while a release branch is open skips that release. A hotfix that diverges from develop produces conflicts at merge-back that someone resolves badly under pressure. This page runs the procedure with those pitfalls covered, within GitFlow vs GitHub Flow comparison.

When to use this approach Jump to heading

  • Your team uses GitFlow, as set up in setting up GitFlow branches and protections.
  • A bug in production needs fixing before the next scheduled release.
  • Past hotfixes have gone missing from later releases, or caused painful merges into develop.
  • For GitHub Flow or trunk-based models, a hotfix is simply a fast pull request to main; this page does not apply.

Step 1 β€” Branch from the production tag Jump to heading

Cut the hotfix branch from main β€” or, more precisely, from the tag of the version in production. Name it after the patch version it will produce.

git fetch origin --tags
git describe --tags --abbrev=0 origin/main          # v2.4.0 β€” what production runs
git switch -c hotfix/2.4.1 v2.4.0
git push -u origin hotfix/2.4.1
A hotfix's path through GitFlowThe hotfix branch starts at the v2.4.0 tag on main. The fix is committed and tested there, merged into main and tagged v2.4.1, then merged into develop so the next release includes it. If a release branch is open, it merges there instead, and that branch carries it to develop.from production, back to production and forward to developmainv2.4.0v2.4.1hotfix/2.4.1v2.4.0fixdevelopd1d2d3mergetwo merges out of every hotfix β€” skip the second and the bug comes back A hotfix's path through GitFlowThe hotfix branch starts at the v2.4.0 tag on main. The fix is committed and tested there, merged into main and tagged v2.4.1, then merged into develop so the next release includes it. If a release branch is open, it merges there instead, and that branch carries it to develop.from production, back to production and forward to developmainv2.4.0v2.4.1hotfix/2.4.1v2.4.0fixdevelopd1d2d3mergetwo merges out of every hotfix β€” skip the second and the bug comes back

Step 2 β€” Keep the fix minimal and test it like a release Jump to heading

A hotfix ships to production quickly with less review time than a normal release, so it should contain the smallest change that fixes the bug, plus a regression test. Run the same release pipeline the hotfix will go through β€” the full suite and staging deploy β€” on the hotfix branch.

# The fix and its test, nothing else
git add src/billing/totals.py tests/billing/test_credit_notes.py
git commit -m "fix(billing): stop double discount on credit notes (PAY-907)"
git push
gh pr create --base main --title "Hotfix 2.4.1: double discount on credit notes" --fill

Resist the temptation to include β€œwhile we are here” fixes. Every extra line is untested in the context of an urgent release.

Step 3 β€” Merge into main and tag the patch release Jump to heading

Merge the hotfix into main through its pull request, with a merge commit so the hotfix is identifiable in history, then tag the result.

gh pr merge --merge --delete-branch=false
git fetch origin
git tag -s v2.4.1 -m "Release 2.4.1 (hotfix PAY-907)" origin/main
git push origin v2.4.1

Keep the hotfix branch until the merge-back is done; it is the clean source for merging the fix forward.

Step 4 β€” Merge back to develop, or to the open release Jump to heading

Where the fix goes next depends on whether a release branch is currently open.

Where does the hotfix merge back to?If no release branch is open, merge the hotfix into develop. If a release branch is open, merge it into the release branch, which will carry it into develop when the release finishes. If the release branch has diverged heavily, merge into both, resolving conflicts on each.Is a release branch open right now?noMerge into developnext release has ityesMerge into release/X.Yrelease carries it onyes, diverged a lotMerge into bothresolve eachmerging only into develop while a release is open ships that release without the fix Where does the hotfix merge back to?If no release branch is open, merge the hotfix into develop. If a release branch is open, merge it into the release branch, which will carry it into develop when the release finishes. If the release branch has diverged heavily, merge into both, resolving conflicts on each.Is a release branch open right now?noMerge into developnext release has ityesMerge into release/X.Yrelease carries it onyes, diverged a lotMerge into bothresolve eachmerging only into develop while a release is open ships that release without the fix
# No open release: merge the hotfix into develop
git switch develop && git pull --ff-only
git merge --no-ff hotfix/2.4.1 -m "Merge hotfix/2.4.1 into develop"
git push origin develop

# Open release: merge into the release branch instead
git switch release/2.5 && git merge --no-ff hotfix/2.4.1 && git push

If the merge into develop conflicts β€” because develop has changed the same code β€” resolve it with care: develop’s version is the future, and the fix’s intent must be reapplied to it, as described in resolving cherry-pick conflicts on older branches in reverse.

Step 5 β€” Verify the fix is everywhere, then delete the branch Jump to heading

Confirm the fix is on main and on the branch that will become the next release, then delete the hotfix branch.

fix=$(git log --format=%H -1 --grep='PAY-907' origin/hotfix/2.4.1)
for b in origin/main origin/develop origin/release/2.5; do
  git merge-base --is-ancestor "$fix" "$b" 2>/dev/null && echo "βœ“ $b" || echo "βœ— $b missing the fix"
done
git push origin --delete hotfix/2.4.1
A hotfix from report to cleanupThe bug is reported and a hotfix branch is cut from the production tag within the hour. The fix and its test are reviewed and pass the release pipeline. It merges to main and is tagged, deploys, and is then merged back to develop or the open release branch before the hotfix branch is deleted.Reportedproduction bug09:10hotfix/2.4.1from v2.4.009:30Merged + taggedv2.4.1 deployed11:00Merged backdevelop or release11:20Branch deletedverified everywhere11:25the merge-back is part of the hotfix, not a follow-up task for later A hotfix from report to cleanupThe bug is reported and a hotfix branch is cut from the production tag within the hour. The fix and its test are reviewed and pass the release pipeline. It merges to main and is tagged, deploys, and is then merged back to develop or the open release branch before the hotfix branch is deleted.Reportedproduction bug09:10hotfix/2.4.1from v2.4.009:30Merged + taggedv2.4.1 deployed11:00Merged backdevelop or release11:20Branch deletedverified everywhere11:25the merge-back is part of the hotfix, not a follow-up task for later

⚠️ SAFETY WARNING: Deleting the hotfix branch before the merge-back means the fix exists only on main, and the next release from develop will regress. Keep the branch until the verification above shows the fix on every branch that needs it. If deleted too early, the fix is still reachable on main; merge main into develop, or cherry-pick the fix commit with -x.

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Can we cherry-pick instead of merging the hotfix back? Jump to heading

Yes, and some teams prefer it for a tidier develop. Use cherry-pick -x so the link is recorded. Merging is the GitFlow default because it records the relationship in the graph and avoids duplicate commits.

What if two hotfixes are needed at once? Jump to heading

Run them sequentially if possible: finish and tag 2.4.1, then cut 2.4.2 from it. Two parallel hotfix branches from the same tag both need merging into main and conflict with each other’s version bumps.

Should the CI check for missing merge-backs? Jump to heading

Yes. A scheduled git cherry comparison between main and develop, as in the GitFlow setup guide, catches forgotten merge-backs within a day.