GitHub Flow: A Practical Guide for Modern Teams
GitHub Flow is a lightweight workflow with one long-lived branch. You branch from main, commit, open
a pull request, get review and automated checks, merge, and delete the branch. There is no develop
branch, no release branch, and no separate integration stage.
It is designed for teams that deploy frequently from a single mainline. That design assumption is the whole story: where it holds, GitHub Flow is hard to beat for simplicity; where it does not, it fits badly.
The six steps
Section titled “The six steps”GitHub documents GitHub Flow as a short sequence:
-
Create a branch. From
main, with a short descriptive name so colleagues can see what is in progress at a glance. -
Make changes. Commit with descriptive messages, and push the branch.
-
Create a pull request. This asks collaborators for feedback and is where review and automated checks happen.
-
Address review comments. Push more commits to the same branch; the pull request updates.
-
Merge the pull request once it is approved.
-
Delete your branch. GitHub keeps deleted branches recoverable for a period afterwards.
A trunk lane labelled main with commits A, B, a merge commit M1 and a merge commit M2. Two short branch lanes each leave main and merge back after two commits. There is no develop branch and no release branch.
What is Git and what is GitHub
Section titled “What is Git and what is GitHub”This distinction matters more here than in any other workflow, because the model is named after the platform.
| Step | Git or platform? | Underlying operation |
|---|---|---|
| Create a branch | Git | git switch -c |
| Commit | Git | git commit |
| Push | Git | git push |
| Open a pull request | Platform | No Git equivalent exists |
| Review and comment | Platform | No Git equivalent exists |
| Run automated checks | Platform | Triggered by push events |
| Require approvals before merge | Platform | Branch protection / rules |
| Merge the pull request | Both | A platform button that performs a Git merge, squash or rebase server-side |
| Delete the branch | Git or platform | git push origin --delete, or the button |
Three of the six steps have no Git equivalent at all. There is no git pull-request command. If you
moved this repository to a different host tomorrow, steps 1, 2, 3 and 6 would work unchanged and the
review workflow would need replacing.
Why it suits continuous delivery
Section titled “Why it suits continuous delivery”GitHub Flow makes one strong assumption: main is always deployable, and merging is the release
decision.
That collapses a lot of process. There is no separate “integrate to develop, then cut a release, then
promote to production” pipeline, because merging is the promotion. A change goes from a developer’s
branch to production through exactly one gate.
The properties that fall out:
- One integration target. No question about which branch a fix belongs on.
- Short branch lifetime by construction. Nothing accumulates on the way to
main, so there is little reason to keep a branch open. - A single history to reason about.
git log mainis the product’s history. - Fast feedback. Small changes merged frequently means problems surface while the context is fresh.
Those properties depend entirely on the assumption holding. Which brings us to the requirements.
What GitHub Flow requires
Section titled “What GitHub Flow requires”The workflow is simple. Making it safe is not.
Automated tests good enough to gate a merge. If merging means deploying, the test suite is the last
line of defence. A team that merges to main on the strength of a suite that misses most regressions is
running an unsafe workflow with a simple diagram.
Branch protection on main. Requiring status checks and reviews, and preventing direct pushes, is
what makes “main is always deployable” structural rather than aspirational.
Fast, reliable rollback. Merging frequently is only reasonable if a bad change can be reverted
quickly. git revert plus an automated redeploy is the usual answer.
A way to hide incomplete work. If a feature takes three weeks but branches should live for a day, the work has to ship disabled — behind a feature flag or as inert code nothing calls yet.
Small changes. Everything above assumes a pull request is reviewable in one sitting.
Merge methods and the history you get
Section titled “Merge methods and the history you get”When merging a pull request, GitHub offers three methods. They produce meaningfully different histories.
| Method | What it does | main history |
|---|---|---|
| Create a merge commit | Merges with --no-ff, preserving every branch commit plus an explicit merge point | Branch structure visible |
| Squash and merge | Combines all branch commits into one commit on main | One commit per pull request |
| Rebase and merge | Adds each branch commit onto main individually, with no merge commit | Linear |
Two details are worth knowing precisely, because they surprise people:
GitHub’s “Rebase and merge” always creates new commits. GitHub’s implementation always updates
committer information and produces new commit SHAs — unlike native git rebase, which can preserve
committer data when replaying onto an ancestor. It also drops commits that were empty to begin with.
“Squash and merge” is not git merge --squash. They achieve comparable results by different routes,
and one consequence is that after either squash or rebase integration, git branch -d will refuse to
delete your local branch — the commits on main are new objects, so Git cannot see the original branch
as merged. That refusal is expected, not a sign the change failed to land.
Squash Merging and Rebase and Merge cover both properly.
Branch protection and repository rules
Section titled “Branch protection and repository rules”GitHub Flow’s central claim — that main is always deployable — is only true if something enforces it.
On GitHub that enforcement comes from branch protection settings or the newer repository rulesets. Both
are platform features; Git itself has no equivalent.
The controls that matter most for this workflow:
| Control | What it prevents |
|---|---|
| Require a pull request before merging | Direct pushes to main bypassing review entirely |
| Require status checks to pass | Merging a change CI has already flagged |
| Require branches to be up to date | Merging a change never tested against current main |
| Require approving reviews | One person merging their own unreviewed work |
| Require linear history | Merge commits, if the team wants a linear main |
| Require signed commits | Unsigned commits reaching main — see Signed Commits |
| Restrict who can push | Everyone except a release role touching main |
GitHub’s documentation notes that protection settings may block merging when a pull request does not
meet the configured requirements — which is exactly the point. The rules turn “we agreed not to push to
main” into something that cannot happen by accident.
“Require branches to be up to date before merging” deserves particular attention. Without it, a pull
request can pass CI against a main from three days ago, then merge into a main that has moved. The
merge itself may be conflict-free while the combination is broken — each change is fine alone and they
interact badly. Requiring an up-to-date branch closes that window at the cost of asking contributors to
sync more often, which on a busy repository is why merge queues exist: the platform serialises merges
and tests each combination before it lands.
Deployment implications
Section titled “Deployment implications”“Merging is shipping” has consequences worth being explicit about.
Deploying from main only. In its simplest form, every merge to main triggers a deployment. This
is the model the workflow was designed for, and it makes the commit that is running in production
trivially identifiable.
Deploying before merging. Some teams deploy the pull request branch to a staging or preview
environment first, verify it, then merge. This keeps main clean while still testing the real artefact,
at the cost of an extra step.
Tagging releases anyway. Continuous deployment does not preclude tags. Tagging main at each
deployment gives you a stable name for “what shipped on Tuesday” without introducing a release branch.
Release Branches covers when a tag is insufficient.
The one arrangement that does not work is deploying from main while treating main as an integration
area where things are allowed to be broken. That combination is how teams end up unable to ship a fix
because main currently contains someone’s half-finished refactor.
What GitHub Flow does not answer
Section titled “What GitHub Flow does not answer”The workflow is deliberately narrow. It has no opinion on several questions that eventually arise:
- Supporting more than one released version. If customers run 2.x while you develop 3.x, GitHub Flow offers no place for 2.x fixes to live. You need a maintenance branch, which is a Git Flow idea.
- Stabilising a release over days. With no release branch, a stabilisation period has nowhere to
happen except on
main, which blocks everyone else. - Coordinating a change across several repositories. Nothing in the model addresses it.
- Regulated environments requiring segregation of duties. “Merge equals deploy” may be incompatible with a required separate approval before production.
None of these are flaws — they are the scope boundary. When you hit one, the answer is usually to add the smallest piece of another model rather than to abandon this one.
GitHub Flow versus Git Flow
Section titled “GitHub Flow versus Git Flow”| GitHub Flow | Git Flow | |
|---|---|---|
| Long-lived branches | main only | main and develop |
| Release branches | None | Yes, per release |
| Hotfix branches | Not a distinct concept | Yes, a defined branch type |
| Release cadence | Continuous | Discrete, versioned |
| Deploy trigger | Merge to main | Tagged release |
| Complexity | Low | Substantially higher |
| Fits | SaaS, web services, continuous delivery | Versioned or packaged software with parallel maintenance |
The honest summary: Git Flow solves problems GitHub Flow does not address — supporting several released versions at once, stabilising a release while development continues — and charges for that in complexity. If you do not have those problems, you are paying for nothing. Git Flow covers the model on its own terms.
GitHub Flow versus trunk-based development
Section titled “GitHub Flow versus trunk-based development”These two are far closer to each other than either is to Git Flow, and the difference is often overstated.
Both keep one long-lived branch and emphasise short branch lifetime. The differences are of degree and of emphasis:
| GitHub Flow | Trunk-based development | |
|---|---|---|
| Branch lifetime | Hours to days | Hours, sometimes minutes |
| Branch always used? | Yes | Sometimes commits go directly to trunk |
| Review | Pull request before merge | Pull request, or pair/post-commit review |
| Emphasis | The review workflow | Integration frequency |
| Feature flags | Useful | Effectively required |
In practice, a team doing GitHub Flow with branches that live a few hours and merge behind flags is practising trunk-based development. The labels describe emphasis more than mechanism. Trunk-Based Development covers the discipline it demands.
Signalling work in progress
Section titled “Signalling work in progress”A pull request opened early is useful — CI runs, colleagues can see the direction, and feedback arrives while changing course is still cheap. The risk is that an unfinished proposal looks ready to merge.
GitHub’s answer is the draft pull request: opened normally but explicitly marked as not ready, so it
cannot be merged until it is marked ready for review. Teams also use labels, a WIP: title prefix, or
simply not requesting reviewers yet.
The underlying practice matters more than the mechanism. Opening a pull request on the first commit and letting it accumulate work for a fortnight is a long-lived branch regardless of how it is labelled. Opening one early because you intend to merge it within a day or two is the workflow behaving as designed.
Common mistakes
Section titled “Common mistakes”Treating main as deployable without testing that claim. The property has to be maintained by
automation, not assumed.
Long-lived pull requests. A pull request open for three weeks is a long-lived feature branch with a web page attached, and it carries every cost of one.
Merging without deleting. The workflow assumes branches disappear. Enable automatic deletion.
Using develop “as well”. Adding a second long-lived branch to GitHub Flow gives you the
integration cost of Git Flow without its release discipline. Pick a model.
Assuming the merge button and the Git command are identical. They are not, particularly for rebase and squash. Know which one your repository is configured for.
Skipping branch protection because the team is small and trusts each other. Protection is not about trust; it is about the change nobody meant to push at 6pm on a Friday.
Mental Model
Section titled “Mental Model”GitHub Flow says:
mainis production, and merging is shipping.A branch is a proposal that has not shipped yet. A pull request is that proposal under review. Merging accepts it — and because merging means shipping, everything that must be true before shipping must be checked before merging.
That framing explains the requirements. Comprehensive tests, branch protection and feature flags are not add-ons; they are what makes “merge equals ship” survivable.
What You Learned
Section titled “What You Learned”- GitHub Flow is: branch, commit, pull request, review, merge, delete — with
mainthe only long-lived branch. - Three of its six steps are platform features with no Git equivalent.
- It assumes
mainis always deployable and that merging is the release decision. - It requires good automated tests, branch protection, fast rollback and a way to hide incomplete work.
- GitHub’s three merge methods produce different histories; rebase-and-merge always rewrites commits.
- It differs from Git Flow in scope, and from trunk-based development mostly in degree.
Try It Yourself
Section titled “Try It Yourself”You can practise the Git half without any account, using a local “remote”.
- Create a bare repository to act as the remote:
git init --bare ~/tmp/origin.git. - Clone it:
git clone ~/tmp/origin.git demo && cd demo. - Commit a file on
mainand push. - Create
feat/add-readme, commit, andgit push -u origin feat/add-readme. - Switch to
mainand merge withgit merge --no-ff feat/add-readme— the same shape as GitHub’s “Create a merge commit”. - Run
git log --oneline --graphand identify the merge commit. - Delete the branch locally and on the “remote”:
git branch -dandgit push origin --delete.
Steps 5 and 7 are exactly what the merge button and the delete button do. What you cannot reproduce locally is steps 3 and 4 of GitHub Flow — review and required checks — which is precisely the part that belongs to the platform.
Next Lesson
Section titled “Next Lesson”Git Flow is the model GitHub Flow was a reaction against. It is more complex, frequently criticised, and still the right answer for some products.