Skip to main content
CI/CD·7 min read

GitHub Branching Strategy — Trunk-Based, GitHub Flow, or Git Flow?

A practical comparison of trunk-based development, GitHub Flow, and Git Flow: short-lived branches, pull requests, required checks, protected branches, release tagging, and how branching ties into CI/CD.

DT

DevOps Engineer & Technical Writer

The Problem

Teams spend hours debating branching models and then adopt whichever one a tutorial showed them. The wrong choice is expensive: long-lived branches drift from main, merges turn into daylong conflict-resolution sessions, and "it works on my branch" becomes a weekly phrase. The branching model is not an aesthetic preference — it directly controls how often you can ship and how painful integration is.

GITHUB FLOW (SHORT-LIVED BRANCHES) vs GIT FLOW (HEAVIER, LONG-LIVED BRANCHES) GitHub Flow / Trunk-Based main feature/login (PR) fix/cart (PR) branches live hours to days, merge via PR, deploy from main Git Flow main (releases) develop feature (long-lived) release branch hotfix more branches, more coordination, slower integration — fits scheduled releases

The trade-off is simple: fewer, shorter-lived branches mean faster integration and continuous deployment; more, longer-lived branches mean more control but slower, more painful merges.

The Three Models

ModelBranch structureIntegration cadenceBest for
<strong>Trunk-based</strong>Everyone commits to <code class="inline-code">main</code> behind short-lived branches or feature flagsMany times a dayHigh-velocity teams, continuous deployment
<strong>GitHub Flow</strong><code class="inline-code">main</code> + short-lived feature branches merged via PRPer PR, several times a dayMost teams, web apps, SaaS
<strong>Git Flow</strong><code class="inline-code">main</code>, <code class="inline-code">develop</code>, plus <code class="inline-code">feature</code>, <code class="inline-code">release</code>, <code class="inline-code">hotfix</code> branchesPer scheduled releaseVersioned software, multiple supported releases

Trunk-Based Development

Everyone integrates into main constantly — branches live hours, not days. Unfinished work hides behind feature flags rather than long-lived branches. This keeps integration pain near zero because nothing drifts far from trunk.

GitHub Flow

A lightweight middle ground: main is always deployable, and every change happens on a short-lived branch that opens a pull request. Once reviewed and green, it merges and deploys. This is the default for most teams and the one GitHub itself popularized.

Git Flow

A heavier model with permanent develop and main branches plus dedicated release and hotfix branches. It gives fine control over versioned releases but adds coordination overhead and slows integration. It fits software with scheduled releases and multiple supported versions — less so a continuously deployed web app.

The Short-Lived Branch Workflow

For GitHub Flow and trunk-based, the day-to-day loop is the same:

# start from an up-to-date main

git checkout main

git pull origin main

# create a short-lived, well-named branch

git checkout -b feature/add-password-reset

# work, commit, push

git add .

git commit -m "Add password reset endpoint"

git push -u origin feature/add-password-reset

# open a PR, get review + green checks, merge, delete the branch

The discipline that makes this work: keep the branch alive for hours or a day, not weeks. The longer it lives, the more main moves underneath it and the harder the merge becomes.

Pull Requests, Review, and Required Checks

A pull request is where branching meets CI/CD. Protect main so that nothing merges without passing checks and review.

In GitHub, branch protection rules on main typically require:

  • At least one approving review before merge.
  • Status checks to pass — your CI build and tests must be green.
  • Branch up to date with main before merging.
  • No direct pushes to main — changes arrive only through PRs.

# .github/workflows/pr-checks.yml — the required check behind the rule

name: PR Checks

on:

pull_request:

branches: [main]

jobs:

verify:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- uses: actions/setup-node@v4

with:

node-version: 20

cache: "npm"

- run: npm ci

- run: npm run lint

- run: npm test

Once this workflow is marked as a required status check, GitHub blocks the merge button until it passes. That is how a branching strategy becomes enforceable rather than a wiki page people ignore.

Release Tagging

Even with trunk-based or GitHub Flow, you still want a durable marker for what shipped. Tags give you that without a long-lived release branch.

git checkout main

git pull origin main

git tag -a v1.4.0 -m "Release 1.4.0"

git push origin v1.4.0

A pushed tag can itself trigger a release workflow:

on:

push:

tags:

- "v*" # fires on v1.4.0, v2.0.0, etc.

This keeps releases traceable while leaving your branch model simple.

How Branching Ties Into CI/CD

The branching model and the CI/CD pipeline are two halves of the same system:

  • PR to main runs lint, build, and test as a required check — nothing merges red.
  • Merge to main triggers deploy to staging (and often production) automatically.
  • Tag push cuts a versioned release.

Short-lived branches make this flow safe because main stays continuously deployable. Long-lived branches fight it: by the time a two-week branch merges, the pipeline is validating code against a main that has changed hundreds of times.

Recommendation

For most teams deploying continuously — web apps, APIs, SaaS — use GitHub Flow, and move toward trunk-based as your test coverage and feature-flagging mature. Both keep integration cheap and main shippable. Reserve Git Flow for software with real scheduled releases and multiple supported versions, where its extra branches earn their keep.

Common Mistakes to Avoid

  • Long-lived feature branches. The longer a branch lives, the worse the merge. Keep them to hours or a day and integrate often.
  • No branch protection on main. Without required reviews and checks, your strategy is a suggestion. Enforce it with protection rules.
  • Adopting Git Flow by default. Its develop/release/hotfix overhead rarely pays off for a continuously deployed app. Start simpler.
  • Treating main as unstable. main should always be deployable. If it is routinely broken, your required checks are too weak.
  • Skipping release tags. Even with simple branching, tags give you a traceable record of exactly what shipped and when.

FAQ

Which branching strategy should a new team pick?

Start with GitHub Flow: main plus short-lived feature branches merged through reviewed pull requests. It is simple, keeps main deployable, and maps cleanly onto GitHub's protection rules and CI — you can always graduate to trunk-based later.

What makes trunk-based development different from GitHub Flow?

Trunk-based pushes integration even further: developers merge into main many times a day and hide unfinished work behind feature flags rather than branches. GitHub Flow still uses short-lived branches per change, which is a gentler on-ramp to the same continuous-integration mindset.

How do protected branches enforce a strategy?

Branch protection rules on main can require approving reviews, passing status checks, and an up-to-date branch before any merge, while blocking direct pushes. That turns your intended workflow into something GitHub enforces automatically instead of relying on everyone remembering the rules.

Do I still need release branches with GitHub Flow?

Usually not. Pushing an annotated tag like v1.4.0 from main gives you a durable release marker and can trigger a release workflow, without the overhead of maintaining a separate long-lived release branch.