Setting up GitFlow branches and protections Jump to heading
GitFlow is a branching model with rules: features branch from develop and merge back to it; releases branch from develop, stabilise, and merge into both main and develop; hotfixes branch from main and merge into both. Teams adopt it for products with scheduled releases and several versions in flight. Where it goes wrong is almost never the model itself but its enforcement — a feature merged straight to main, a release branch nobody merged back to develop, a hotfix that reached production but not the next release. Those are prevented by configuration: branch protection per branch type, CI that runs the right checks for each, and a merge-back check. This page sets that up, within GitFlow vs GitHub Flow comparison.
When to use this approach Jump to heading
- Your team has chosen GitFlow — typically for versioned, scheduled releases — and wants it enforced.
- Changes sometimes reach
mainwithout passing throughdevelopor a release branch. - Release or hotfix changes sometimes fail to reach
develop, and regressions reappear. - If you are still choosing a model, read trunk-based vs GitFlow for SaaS teams first.
Step 1 — Create the long-lived branches Jump to heading
GitFlow has two permanent branches: main, which only ever holds released code, and develop, where the next release is integrated. Create develop from main and make it the default branch, so pull requests target it unless someone chooses otherwise.
git switch main && git pull --ff-only
git switch -c develop && git push -u origin develop
gh repo edit --default-branch develop Step 2 — Protect each branch type differently Jump to heading
Each branch type has different rules about who may push and what must pass. Express them as protection rules or rulesets keyed by name pattern.
[
{ "name": "main", "include": ["refs/heads/main"],
"rules": ["pull_request (2 approvals)", "required checks: build, test, e2e", "no force-push", "no deletion",
"restrict merges to release/* and hotfix/* sources"] },
{ "name": "develop", "include": ["refs/heads/develop"],
"rules": ["pull_request (1 approval)", "required checks: build, test", "no force-push", "no deletion"] },
{ "name": "release", "include": ["refs/heads/release/*"],
"rules": ["pull_request (1 approval, release managers)", "required checks: build, test, e2e", "no force-push"] },
{ "name": "hotfix", "include": ["refs/heads/hotfix/*"],
"rules": ["required checks: build, test", "no force-push"] }
] Forges cannot always express “merges into main may only come from release/* and hotfix/*” natively. A required check that inspects the pull request’s head branch enforces it:
# Required check on pull requests targeting main
jobs:
source-branch:
if: github.base_ref == 'main'
runs-on: ubuntu-latest
steps:
- run: |
case "${{ github.head_ref }}" in release/*|hotfix/*) exit 0 ;; esac
echo "::error::Only release/* and hotfix/* branches may merge into main"; exit 1 Step 3 — Map CI triggers to branch types Jump to heading
Different branch types deserve different pipelines. Feature branches need fast feedback; release branches need the full suite and packaging; main triggers production deploys from tags.
on:
pull_request:
branches: [develop, "release/**", main]
push:
branches: [develop, "release/**"]
tags: ["v*.*.*"]
jobs:
test: { runs-on: ubuntu-latest, steps: [ { uses: actions/checkout@v4 }, { run: make test } ] }
e2e:
if: startsWith(github.ref, 'refs/heads/release/') || github.base_ref == 'main'
runs-on: ubuntu-latest
steps: [ { uses: actions/checkout@v4 }, { run: make e2e } ]
deploy-staging:
if: startsWith(github.ref, 'refs/heads/release/') && github.event_name == 'push'
runs-on: ubuntu-latest
steps: [ { run: ./deploy.sh staging } ] Step 4 — Check that releases and hotfixes merge back Jump to heading
The most common GitFlow failure is a fix made on a release or hotfix branch that never reaches develop, so it reappears in the next release. A scheduled check compares the branches by patch content.
# Commits on main with no equivalent on develop — should be empty after every release/hotfix
git fetch origin
git cherry -v origin/develop origin/main | grep '^+' || echo "develop contains everything on main" Run it after every merge into main, and fail loudly if anything is missing. Patch-based comparison is explained in finding unported commits with git cherry.
Step 5 — Tag every release on main Jump to heading
Every commit on main is a release, so every merge into main should be tagged. Tagging in the pipeline, from the release branch’s version, keeps tags consistent.
version=${RELEASE_BRANCH#release/}
git tag -s "v$version.0" -m "Release $version.0" origin/main
git push origin "v$version.0" Signing and verifying release tags is covered in signing release tags from a pipeline.
Step 6 — Document the flow where people work Jump to heading
A one-page summary with the diagram above, the branch rules and the commands for starting a feature, a release and a hotfix should be in the repository’s contributing guide. GitFlow’s complexity is manageable when everyone sees the same picture.
## Branching (GitFlow)
- Features: branch from `develop`, PR back to `develop`.
- Releases: `release/X.Y` from `develop`; PR to `main`; then merge `main` back to `develop`.
- Hotfixes: `hotfix/X.Y.Z` from `main`; PR to `main`; then merge to `develop` (or the open release). Validation checklist Jump to heading
Frequently Asked Questions Jump to heading
Do we need the git-flow command-line extension? Jump to heading
No. It automates branch creation and merges but adds no enforcement. The branch protections, checks and merge-back verification above matter more than the tool used to create branches.
Should feature pull requests be squash-merged into develop? Jump to heading
Many GitFlow teams squash features into develop for a readable history, and use merge commits for release and hotfix merges into main so those branches’ histories remain traceable.
How long should a release branch live? Jump to heading
Days to a couple of weeks — long enough to stabilise, short enough that develop and the release branch do not drift far. Only fixes go onto a release branch; new work continues on develop.
Related Jump to heading
- GitFlow vs GitHub Flow Comparison — the parent topic.
- Handling Hotfixes in GitFlow — the hotfix path in detail.
- Migrating from GitFlow to GitHub Flow — if the model stops fitting.
- Designing Branch Protection Rulesets for an Org — expressing these rules as rulesets.