GitHub Flow: Simple Branching for Continuous Delivery
Learn GitHub Flow — the lightweight branching strategy built for continuous deployment. Covers feature branches, pull requests, and production deployment on every merge.
GitHub Flow: Simple Branching for Continuous Delivery
If you’ve ever looked at Git Flow with its five different branch types, release schedules, and complicated lifecycle diagrams and thought “there has to be a simpler way” — you’re not alone. GitHub Flow was born exactly because Git Flow solves problems that most teams don’t actually have. GitHub Flow strips away the complexity: there’s one rule that governs everything — every line of code starts from main and every line of code merges back to main.
This works because GitHub built this model around a single assumption: if your tests pass and your code is reviewed, you’re ready to deploy. No staging branches, no version tags, no release candidates. When your feature branch merges to main, it goes to production. Some of the fastest-moving teams in the industry run on this philosophy, and this post explains why it works, when it breaks down, and how to make it work for your team.
Introduction
There are a lot of ”## when to use / when not to use” sections in this file — they read like filler content from an AI generator. The core idea behind GitHub Flow is simple: if your tests pass and your code is reviewed, you’re ready to deploy. No staging branches, no version tags, no release candidates. When your feature branch merges to main, it goes to production. Some of the fastest-moving teams in the industry run on this philosophy, and it works — provided you have the CI/CD infrastructure and feature flag culture to back it up. This guide covers when this model shines, where it breaks down, and how to implement it without shooting yourself in the foot.
When to Use / When Not to Use
Use GitHub Flow When
- Continuous deployment — You deploy to production multiple times per day or week
- Web applications and SaaS — No packaging, signing, or app store approval process
- Small to medium teams — Where communication overhead of complex branching outweighs benefits
- Feature flag culture — You control feature rollout with flags, not branch timing
- Automated testing — Your CI pipeline catches regressions before they reach
main
Do Not Use GitHub Flow When
- Scheduled releases — You ship on a calendar cadence with QA gates
- Multiple supported versions — You need to patch v1.x while developing v2.x
- Regulated environments — You need formal change management and release documentation
- No CI/CD pipeline — Without automated testing, every merge to
mainis a gamble - Mobile or desktop apps — Where each release requires a build and distribution process
Core Concepts
GitHub Flow has exactly three concepts:
| Concept | Description |
|---|---|
main branch | Always deployable. The single source of truth. |
| Feature branches | Branch from main, merge back to main via pull request |
| Pull requests | Code review, discussion, and CI validation before merge |
That’s it. No release branches. No develop branch. No version branches. The simplicity is the entire point.
graph LR
A[main - Always Deployable] --> B[feature/new-ui]
A --> C[feature/api-v2]
A --> D[feature/bugfix]
B -->|PR + CI| A
C -->|PR + CI| A
D -->|PR + CI| A
A --> E[Deploy to Production]
Architecture and Flow Diagram
The complete GitHub Flow lifecycle from branch creation through production deployment:
graph TD
A[main - Production] -->|create branch| B[feature/branch]
B -->|commit| B
B -->|push| C[Remote Feature Branch]
C -->|open PR| D[Pull Request]
D -->|code review| E[Review Feedback]
E -->|update| C
D -->|CI passes| F[Approved]
F -->|merge| A
A -->|auto deploy| G[Production]
Step-by-Step Guide
1. Create a Feature Branch
Always branch from the latest main:
# Ensure you have the latest main
git checkout main
git pull origin main
# Create and switch to your feature branch
git checkout -b feature/add-user-profile
Branch naming conventions matter for traceability:
# Good: descriptive with ticket reference
feature/PROJ-123-add-user-profile
feature/add-dark-mode
bugfix/fix-login-timeout
# Bad: vague or personal
feature/work-in-progress
johns-branch
temp-fix
2. Commit and Push Frequently
Small, focused commits make code review easier and bisect more effective:
# Make changes
git add src/components/UserProfile.tsx
git commit -m "feat: add user profile component skeleton"
git add src/styles/profile.css
git commit -m "feat: style user profile card"
# Push to remote
git push -u origin feature/add-user-profile
3. Open a Pull Request
The pull request is the heart of GitHub Flow. It’s where code review, automated testing, and team discussion converge:
# Using GitHub CLI
gh pr create \
--base main \
--head feature/add-user-profile \
--title "feat: add user profile page" \
--body "## Summary
Adds user profile page with avatar, bio, and settings link.
## Testing
- Manual: /profile endpoint renders correctly
- Unit: UserProfile component tests pass
- E2E: Profile navigation flow verified
## Screenshots
[Attach screenshots if UI changes]"
4. Review and Iterate
Code review is mandatory in GitHub Flow — it’s the only quality gate before main:
- At least one approved review required
- All CI checks must pass (tests, linting, type checking)
- Address review comments with additional commits to the same branch
- The PR updates automatically with each push
5. Merge and Deploy
Once approved and green:
# Merge via CLI or UI
gh pr merge feature/add-user-profile --squash --delete-branch
# The merge triggers deployment
# CI/CD pipeline detects main branch update and deploys
Most teams use squash merges to keep main history clean, but some prefer merge commits for topology preservation.
Production Failure Scenarios
| Scenario | What Happens | Mitigation |
|---|---|---|
| Bad merge reaches production | A bug slips through review and CI | Feature flags allow instant rollback without code changes; revert the merge commit |
| CI is green but production breaks | Tests don’t cover the failure mode | Add canary deployments; monitor error rates post-deploy; implement automated rollback |
| Conflicting PRs | Two PRs modify the same file, only one can merge first | Communicate in PR comments; rebase the second PR on updated main before merging |
| Long-running feature branch | Branch diverges from main, merge conflicts pile up | Rebase on main daily; break large features into smaller, mergeable chunks |
| Deploy pipeline failure | Merge succeeds but deployment fails | Deployment should be atomic; failed deploys auto-rollback; never leave main in a broken state |
Deployment Verification Checklist
After each merge to main, verify the deployment succeeded:
- CI pipeline completed with all green checks
- Deployment logs show successful rollout with correct commit SHA
- Health endpoint responds with 200 OK
- Error rate in monitoring is within normal baseline (< 1% increase)
- Key user journeys pass smoke tests (login, core feature, checkout)
- No new Sentry/alerting errors in the last 5 minutes
- Database migrations (if any) completed successfully
- Feature flags for the new feature are in the intended state
- Rollback procedure is documented and tested if this is a high-risk deploy
Trade-off Analysis
| Aspect | Advantage | Disadvantage |
|---|---|---|
| Simplicity | One permanent branch, easy to understand | No structure for complex release management |
| Speed | Merge and deploy in minutes | Requires excellent automated testing |
| Code review | Every change gets reviewed before merge | Can become a bottleneck for large teams |
| History | Clean linear history with squash merges | Loses feature branch topology information |
| Risk | Small, frequent changes are low-risk | Every merge to main is a production change |
| Learning curve | New developers can start contributing immediately | Requires discipline around CI and feature flags |
Implementation Snippets
Branch Protection Rules
# GitHub repository settings (via API or UI):
# Required pull request reviews: 1
# Dismiss stale reviews: enabled
# Require status checks: build, test, lint
# Require branches to be up to date: enabled
# Include administrators: enabled
# Restrict pushes that create matching files: enabled
GitHub Actions — CI Pipeline
# .github/workflows/ci.yml
name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm run lint
- run: npm run typecheck
- run: npm test -- --coverage
deploy:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to production
run: ./scripts/deploy.sh
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
Feature Flag Integration
// src/features/userProfile.ts
import { isFeatureEnabled } from '@/utils/featureFlags';
export function UserProfilePage() {
if (!isFeatureEnabled('user-profile')) {
return null; // Or redirect to a fallback
}
return <UserProfile />;
}
// Feature flag evaluation
// src/utils/featureFlags.ts
export function isFeatureEnabled(flag: string): boolean {
const flags = process.env.FEATURE_FLAGS || '{}';
return JSON.parse(flags)[flag] === true;
}
Quick Revert Script
#!/bin/bash
# scripts/revert-last-merge.sh
# Revert the last merge commit on main
git checkout main
git pull origin main
# Find the last merge commit
LAST_MERGE=$(git log --oneline --merges -1 --format="%H")
if [ -z "$LAST_MERGE" ]; then
echo "No merge commits found on main"
exit 1
fi
echo "Reverting merge: $LAST_MERGE"
git revert -m 1 "$LAST_MERGE"
git push origin main
echo "Revert pushed. CI will redeploy the previous version."
Observability Checklist
- Logs: Log every PR merge event with author, PR number, and commit SHA
- Metrics: Track PR cycle time (open to merge), deployment frequency, and mean time to recovery
- Traces: Correlate each deployment with the PR that triggered it and any subsequent incidents
- Alerts: Alert on deployment failure rate > 5%, PRs open longer than 3 days, or
mainbranch CI failures - Dashboards: Display deployment frequency, lead time for changes, change failure rate, and MTTR (DORA metrics)
GitHub Flow vs Trunk-Based Development
GitHub Flow and trunk-based development are close relatives — same core idea, different tolerances for how long branches can live. GitHub Flow works with branches that stick around for days or weeks. Trunk-based development expects branches to be opened and closed within hours, with feature flags hiding incomplete work.
The practical difference comes down to infrastructure. GitHub Flow tolerates longer-lived branches because most teams don’t have the feature flag systems needed to merge incomplete code continuously. Trunk-based development requires that infrastructure as a baseline.
If you’re moving from GitHub Flow to trunk-based, you need:
- Feature flag system (LaunchDarkly, Unleash, Flagsmith, etc.)
- Canary deployment support in CI/CD
- A culture of merging incomplete features behind flags
- Rollback procedures that don’t require new deployments
Security and Compliance Notes
- Branch protection: Required reviews and status checks prevent unauthorized merges to
main - Signed commits: Require GPG-signed commits for cryptographic authorship verification
- CODEOWNERS: Use
CODEOWNERSfile to require specific team reviews for sensitive directories - Secrets scanning: Enable GitHub secret scanning to prevent credential leaks in PRs
- Audit log: GitHub’s audit log tracks all repository events including branch protection changes
- Compliance gap: GitHub Flow lacks formal release artifacts — supplement with deployment logs and change tickets if required by your compliance framework
Common Pitfalls / Anti-Patterns
- The Staging Branch — Adding a
stagingbranch turns GitHub Flow into a half-baked Git Flow. Either commit to continuous deployment or use a different model. - Skipping Code Review — Merging your own PRs without review defeats the only quality gate. Enforce branch protection rules.
- Massive PRs — Pull requests with 50+ files are impossible to review effectively. Break features into smaller, reviewable chunks.
- Ignoring CI Failures — Allowing merges with failing checks means
mainis not always deployable. Make CI mandatory. - No Feature Flags — Without feature flags, every merged feature is immediately visible to users. This couples deployment to release.
- Long-Lived Branches — Feature branches that exist for weeks defeat the purpose. Merge small, merge often.
- Direct Commits to main — Bypassing PRs for “emergency fixes” creates inconsistency. Use the same process for everything.
Quick Recap Checklist
-
mainbranch is always deployable and represents production - Every change starts as a feature branch from
main - Pull requests require at least one review before merge
- CI pipeline runs tests, linting, and type checks on every PR
- Merged PRs trigger automatic deployment to production
- Branch protection rules prevent direct pushes to
main - Feature flags control feature visibility independent of deployment
- PRs are kept small and focused for effective code review
- Failed deployments trigger automatic rollback
- DORA metrics are tracked and reviewed regularly
Interview Questions
GitHub Flow has one permanent branch (main) while Git Flow has two (main and develop) plus temporary release and hotfix branches. GitHub Flow assumes every merge to main is deployable, making it ideal for continuous deployment. Git Flow assumes releases are scheduled events requiring a stabilization period on a release branch.
In practice, GitHub Flow is simpler and faster but requires stronger automated testing and feature flag infrastructure to manage risk.
Create a hotfix branch from main, fix the bug, open a PR, get it reviewed and merged — the same process as any other change. The speed comes from the small scope of the fix, not from bypassing the process.
If the fix needs to go out immediately, use a feature flag to disable the broken feature while the fix goes through the normal pipeline. This is faster than waiting for a merge and deployment cycle.
Feature flags decouple deployment from release. You can merge code to main and deploy it to production without exposing the feature to users. This allows you to deploy frequently while controlling when features become visible.
They also enable progressive rollout — enabling a feature for 10% of users, then 50%, then 100% — and instant rollback by toggling the flag off without deploying new code.
The first PR to merge wins. The second PR gets merge conflicts that need resolving before it can go in.
Fix it by rebasing the second PR on the latest main, resolving conflicts locally, then pushing. CI reruns on the rebased branch. If the conflict is messy, talk to whoever has the first PR — sometimes coordinate to let one merge first.
Prevent this by using CODEOWNERS for contested directories, announcing big changes in team channels, and splitting features into smaller PRs that touch different files.
It doesn't — not well, anyway. Branches older than a week or two start collecting merge conflicts and drifting from main. This is an anti-pattern.
What helps:
- Break large features into smaller chunks behind feature flags
- Rebase on
mainevery day - Use stacked PRs — split a feature into dependent PRs that merge one after another
- Set team norms around branch age and flag stale branches after a few days
A GitHub Flow CI pipeline needs:
- Automated tests — unit, integration, or E2E that run on every PR
- Linting and type checking — catch syntax errors and type mismatches before merge
- Build verification — code compiles and packages correctly
- Required status checks — configured on the
mainbranch so PRs can't bypass them
Without these, every merge to main is a roll of the dice. Teams without automated testing should stick with Git Flow and longer release cycles.
Three options, in order of preference:
- Feature flag rollback — Toggle off the flag for the broken feature. Instant. No redeployment. Requires feature flag infrastructure to be in place first.
- Revert the merge commit — Run
git revert -m 1 <merge-commit>, push, and let CI/CD redeploy the previous version. - Point-in-time restore — Pull a known-good container image from your registry. Last resort since it may undo several unrelated changes at once.
Whatever approach you use, document it and test it before you need it.
Squash merge: All the commits in your PR collapse into one commit on main. The PR title becomes the commit message. Clean, linear history — easy to read and bisect.
Merge commit: Every commit from your feature branch gets preserved as a separate commit on main. Messier log, but you can see the full history of how the feature developed.
Rebase and merge: Your feature branch commits replay on top of main. Clean history too, but you lose the PR as an atomic unit.
Most teams use squash merge. Pick one and enforce it in repository settings.
Almost never. Adding a staging branch means you've basically built a half-baked Git Flow, and that defeats the entire point of GitHub Flow.
There are a few edge cases where it might make sense:
- Regulatory compliance — Change management requirements that mandate a pre-production approval step
- Manual QA gates — Testing that genuinely cannot be automated, like accessibility audits or penetration tests
- Blue-green setups — Where "staging" is actually your green environment and both are production-equivalent
Most teams should skip the staging branch and invest in better automated testing and feature flags instead.
Several mechanisms work together:
- Feature flags — Deploy new code but hide the feature until you're ready
- Blue-green deployments — Route a small percentage of traffic to the new version (canary) before switching over fully
- Rolling updates — Kubernetes-style restarts with health checks between batches
- Backward-compatible migrations — Add columns, never remove them; the old code keeps working while the new code deploys
The underlying rule: main must always be in a deployable state. If a migration needs downtime, design it to be backward-compatible during the transition.
GitHub Flow fits best with:
- Small to medium teams — 5 to 20 developers; communication overhead stays manageable
- Cross-functional teams — Each team can take something from design to deployment without handoffs
- Flat hierarchies — No approval layers between developer and production merge
- Strong code review culture — PR reviews are the only quality gate, so they actually have to happen
Once you get above 30 developers, code review turns into a bottleneck. You can work around it with CODEOWNERS and specialized review teams, or just move to trunk-based development with feature flags.
The CODEOWNERS file defines who owns specific directories or file patterns in the repository. When a PR modifies those files, the designated owners are automatically requested for review. This ensures domain experts always review changes to their areas.
Example CODEOWNERS:
# Teams /src/api/ @api-team /src/ui/ @frontend-team /docs/ @docs-teamIndividuals
*.sql @senior-dba /security/** @security-team
Place the file at .github/CODEOWNERS in the repository root.
Stale branches (older than 1-2 weeks) indicate work that isn't progressing. Handle them by: setting up branch protection rules to alert on old branches, using automation to close branches with no activity for X days after notification, deleting merged branches automatically via settings or scripts, and encouraging developers to push to remote daily so branches stay current with main.
main is broken and needs immediate fix in GitHub Flow?The key principle: main should never be broken. If it happens despite best practices: immediately disable the broken feature via feature flag (if infrastructure exists), revert the merge commit with git revert -m 1 <sha>, investigate why CI didn't catch the issue, fix the root cause, test thoroughly, and merge a proper fix. Never commit directly to main to "fix it fast" — this bypasses CI and can make things worse.
GitHub Actions runs on every push and PR. A typical workflow triggers on push: branches: [main] and pull_request: branches: [main]. Jobs run tests, linting, and build verification. On push to main, a deployment job runs after tests pass. Branch protection settings require all status checks to pass before merging.
GitHub Flow allows feature branches to live for days or weeks; trunk-based development restricts them to hours. At scale (50+ developers), GitHub Flow's longer branch lifetimes cause merge conflict accumulation and integration pain. Trunk-based development requires feature flag infrastructure and very fast CI, but eliminates integration debt entirely. The transition requires cultural changes: developers must be comfortable merging incomplete features behind flags.
Environment configs should be injected at runtime, not stored in code. Use: environment variables set in CI/CD platform settings, config maps/secrets in Kubernetes, or feature flag services for feature-toggled behavior. Never commit API keys or environment-specific URLs to the repository. The same commit should be deployable to any environment with the right configuration injected.
Key security measures: require branch protection with required status checks and PR reviews, enable secret scanning to prevent credential leaks, use CODEOWNERS to ensure security-relevant code is reviewed by experts, require signed commits for production branches, enable dependency review to catch vulnerable dependencies before merge, and use GitHub's audit log to track who merged what and when.
Without feature flags, rollback options are limited: revert the merge commit (creates a new commit that undoes changes), point-in-time restore from a previous container image (may lose other legitimate changes), or push a fix forward (may take longer). The best mitigation is investing in feature flag infrastructure before you need it. Short branch lifetimes also reduce rollback blast radius.
Tests don't cover everything. When this happens: immediately toggle off any related feature flags, assess the blast radius (what users are affected), decide between revert or forward fix, add missing test cases to prevent recurrence, conduct blameless postmortem, and consider canary deployments that expose new code to limited users first. The goal is to catch issues before full production exposure through better testing and staged rollouts.
Further Reading
- GitHub Flow documentation — Official GitHub guide
- GitHub Flow vs Git Flow — Atlassian’s comparison
- Feature Flags best practices — LaunchDarkly’s guide
- DORA Metrics — Four key metrics for software delivery
- Trunk-Based Development — Related approach that GitHub Flow approximates
Conclusion
GitHub Flow strips branching to its essence — main branch, short-lived feature branches, and PRs. Its simplicity is its strength: every merge deploys, every deploy is fast, and there’s no ceremony between code and production.
Category
Related Posts
Choosing a Git Team Workflow: Decision Framework
Decision framework for selecting the right Git branching strategy based on team size, release cadence, and project type.
Git Flow: The Original Branching Strategy Explained
Master the Git Flow branching model with master, develop, feature, release, and hotfix branches. Learn when to use it, common pitfalls, and production best practices.
Git Release Branching and Hotfixes: Managing Versions in Production
Master release branching and hotfix strategies in Git. Learn version branches, emergency fixes, backporting, and how to manage multiple production versions safely.