Skip to content

GitHub Flow: A Practical Guide for Modern Teams

Lesson 3 of 8Beginner → Intermediate11 min readModern Git Workflows · BranchingVerified: Git 2.43.0; GitHub behaviour checked against GitHub's published GitHub Flow documentation

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.

GitHub documents GitHub Flow as a short sequence:

  1. Create a branch. From main, with a short descriptive name so colleagues can see what is in progress at a glance.

  2. Make changes. Commit with descriptive messages, and push the branch.

  3. Create a pull request. This asks collaborators for feedback and is where review and automated checks happen.

  4. Address review comments. Push more commits to the same branch; the pull request updates.

  5. Merge the pull request once it is approved.

  6. Delete your branch. GitHub keeps deleted branches recoverable for a period afterwards.

GitHub Flow: one long-lived branch, many short ones

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.

ABM1CM2XYmainfix/parserfeat/searchmain is always deployable. Branches are short and there is only ever one integration target.

This distinction matters more here than in any other workflow, because the model is named after the platform.

StepGit or platform?Underlying operation
Create a branchGitgit switch -c
CommitGitgit commit
PushGitgit push
Open a pull requestPlatformNo Git equivalent exists
Review and commentPlatformNo Git equivalent exists
Run automated checksPlatformTriggered by push events
Require approvals before mergePlatformBranch protection / rules
Merge the pull requestBothA platform button that performs a Git merge, squash or rebase server-side
Delete the branchGit or platformgit 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.

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 main is 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.

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.

When merging a pull request, GitHub offers three methods. They produce meaningfully different histories.

MethodWhat it doesmain history
Create a merge commitMerges with --no-ff, preserving every branch commit plus an explicit merge pointBranch structure visible
Squash and mergeCombines all branch commits into one commit on mainOne commit per pull request
Rebase and mergeAdds each branch commit onto main individually, with no merge commitLinear

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.

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:

ControlWhat it prevents
Require a pull request before mergingDirect pushes to main bypassing review entirely
Require status checks to passMerging a change CI has already flagged
Require branches to be up to dateMerging a change never tested against current main
Require approving reviewsOne person merging their own unreviewed work
Require linear historyMerge commits, if the team wants a linear main
Require signed commitsUnsigned commits reaching main — see Signed Commits
Restrict who can pushEveryone 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.

“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.

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 FlowGit Flow
Long-lived branchesmain onlymain and develop
Release branchesNoneYes, per release
Hotfix branchesNot a distinct conceptYes, a defined branch type
Release cadenceContinuousDiscrete, versioned
Deploy triggerMerge to mainTagged release
ComplexityLowSubstantially higher
FitsSaaS, web services, continuous deliveryVersioned 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 FlowTrunk-based development
Branch lifetimeHours to daysHours, sometimes minutes
Branch always used?YesSometimes commits go directly to trunk
ReviewPull request before mergePull request, or pair/post-commit review
EmphasisThe review workflowIntegration frequency
Feature flagsUsefulEffectively 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.

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.

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.

GitHub Flow says: main is 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.

  • GitHub Flow is: branch, commit, pull request, review, merge, delete — with main the only long-lived branch.
  • Three of its six steps are platform features with no Git equivalent.
  • It assumes main is 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.

You can practise the Git half without any account, using a local “remote”.

  1. Create a bare repository to act as the remote: git init --bare ~/tmp/origin.git.
  2. Clone it: git clone ~/tmp/origin.git demo && cd demo.
  3. Commit a file on main and push.
  4. Create feat/add-readme, commit, and git push -u origin feat/add-readme.
  5. Switch to main and merge with git merge --no-ff feat/add-readme — the same shape as GitHub’s “Create a merge commit”.
  6. Run git log --oneline --graph and identify the merge commit.
  7. Delete the branch locally and on the “remote”: git branch -d and git 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.

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.