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 main without passing through develop or 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
GitFlow's branch topologyMain holds only released commits, each tagged. Develop integrates finished features. A release branch is cut from develop, stabilised, then merged into main and back into develop. A hotfix branch is cut from main, fixed, then merged into main and develop.two permanent branches, three kinds of temporary onesmainv2.3v2.4v2.4.1hotfix/2.4.1fixrelease/2.4rcv2.4developddmergemergefeature/xf1f2every arrow into main is from a release or hotfix branch — never from a feature GitFlow's branch topologyMain holds only released commits, each tagged. Develop integrates finished features. A release branch is cut from develop, stabilised, then merged into main and back into develop. A hotfix branch is cut from main, fixed, then merged into main and develop.two permanent branches, three kinds of temporary onesmainv2.3v2.4v2.4.1hotfix/2.4.1fixrelease/2.4rcv2.4developddmergemergefeature/xf1f2every arrow into main is from a release or hotfix branch — never from a feature

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 } ]
Rules per GitFlow branch typeFeature branches get fast CI and no protection. Develop requires one approval and unit tests. Release branches require release-manager approval, the full suite and deploy to staging. Main accepts only release and hotfix merges with two approvals, and deploys to production from tags.Who merges / approvalsCI and deploysfeature/*authorfast testsdevelop1 approvalbuild + testrelease/*release managersfull suite, stagingmain2 approvals, release/hotfix onlytag → productionthe stricter the branch, the closer it is to production Rules per GitFlow branch typeFeature branches get fast CI and no protection. Develop requires one approval and unit tests. Release branches require release-manager approval, the full suite and deploy to staging. Main accepts only release and hotfix merges with two approvals, and deploys to production from tags.Who merges / approvalsCI and deploysfeature/*authorfast testsdevelop1 approvalbuild + testrelease/*release managersfull suite, stagingmain2 approvals, release/hotfix onlytag → productionthe stricter the branch, the closer it is to production

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.

Is this pull request allowed into main?A pull request into main from a release branch is allowed once release checks pass. One from a hotfix branch is allowed after its checks. Any other source, such as a feature branch or develop, is rejected by the source-branch check with a message explaining the flow.Pull request into main — from which branch?release/*Allowafter full suitehotfix/*Allowafter checksanything elseRejectuse developthis one check prevents the most common GitFlow mistake Is this pull request allowed into main?A pull request into main from a release branch is allowed once release checks pass. One from a hotfix branch is allowed after its checks. Any other source, such as a feature branch or develop, is rejected by the source-branch check with a message explaining the flow.Pull request into main — from which branch?release/*Allowafter full suitehotfix/*Allowafter checksanything elseRejectuse developthis one check prevents the most common GitFlow mistake

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.