Gitflow #

Git without an agreed workflow is a recipe for chaos — branches with unclear purposes, merges straight to main out of hurry, hotfixes forgotten to be merged back into develop, and releases that can’t be cleanly rolled back. Gitflow is one answer to this problem: a branching model separating feature development, release preparation, and production fixes into branches with clear purposes and rules. But Gitflow also has costs — it’s complex, verbose, and can become a bottleneck for fast-moving teams. This article covers Gitflow from its fundamental structure, end-to-end workflows, comparisons with alternative workflows, and the often-debated question: when to deploy from a tag vs from the main branch.

Why Gitflow Exists #

Vincent Driessen introduced Gitflow in 2010 when the software world was very different — continuous deployment wasn’t common, teams were larger, and release cycles were longer (weekly or monthly). He needed a model that could:

Problems Gitflow tries to solve:

1. Parallel feature development without disturbing stability
   → Every developer can work on their own branch without fearing
     "breaking" code being tested or about to be released

2. A release process separate from active development
   → The team can keep developing new features while another team
     does final testing and bug fixes for the upcoming release

3. Hotfixes that don't disturb an in-flight sprint
   → Production bugs are fixed from main directly, not from develop
     which might already contain half-finished features

4. Clean, auditable history
   → Every release version is clear — a tag on main shows exactly
     what code was in production at a specific time

The Five Gitflow Branches and Their Roles #

gitGraph
   commit id: "initial"
   branch develop
   checkout develop
   commit id: "dev setup"
   branch feature/login
   checkout feature/login
   commit id: "add login UI"
   commit id: "add login logic"
   checkout develop
   merge feature/login id: "merge login"
   branch feature/payment
   checkout feature/payment
   commit id: "add payment"
   checkout develop
   merge feature/payment id: "merge payment"
   branch release/1.0.0
   checkout release/1.0.0
   commit id: "bump version"
   commit id: "fix minor bugs"
   checkout main
   merge release/1.0.0 id: "release v1.0.0" tag: "v1.0.0"
   checkout develop
   merge release/1.0.0 id: "sync develop"
   checkout main
   branch hotfix/1.0.1
   checkout hotfix/1.0.1
   commit id: "fix critical bug"
   checkout main
   merge hotfix/1.0.1 id: "hotfix v1.0.1" tag: "v1.0.1"
   checkout develop
   merge hotfix/1.0.1 id: "sync hotfix"

The main Branch — Production History #

The main branch (or master) represents the code currently or previously in production. Every commit here is a released version, marked with a version tag.

main rules:
  ✗ No direct commits to main
  ✗ No feature branch merges straight to main
  ✓ Only accepts merges from release/* and hotfix/*
  ✓ Every merge to main must come with a version tag

Why so strict?
  → Main is the "single source of truth" for production
  → If main contains untested or unready code,
    rollback becomes difficult and it's unclear which version is safe

The develop Branch — Integration Branch #

The develop branch is where all finished features gather before release. It’s “production-in-progress” — reflecting what will be in the next release.

develop rules:
  ✗ No direct feature commits to develop
  ✓ Accepts merges from feature/*, release/*, and hotfix/*
  ✓ Must always be in a "buildable and testable" state
  ✓ CI must run on every push to develop

Why is develop separate from main?
  → Separates "in progress" from "production-ready"
  → Developers can integrate and test together without
    affecting main's stability

The feature/* Branches — Feature Development #

Every new feature is worked on in a separate branch, created from develop, and merged back into develop when finished.

# Creating a feature branch
git checkout develop
git pull origin develop
git checkout -b feature/user-profile

# After finishing — merge into develop via PR/MR
git checkout develop
git merge --no-ff feature/user-profile  # --no-ff preserves merge history
git branch -d feature/user-profile
git push origin develop

# Recommended naming convention:
feature/login-google
feature/payment-integration
feature/order-history-export
feature/JIRA-123-add-search       # can include the ticket ID
Feature branch rules:
  ✓ Created from develop (not from main!)
  ✓ One branch per feature or per story
  ✓ Kept short-lived (ideally < 1-2 sprints)
  ✓ Merged into develop via a reviewed Pull Request
  ✓ Deleted after merging

Anti-patterns:
  ✗ Feature branches living for weeks without updating from develop
    → Divergence grows → merge conflicts grow
  ✗ A developer never syncing with the latest develop
    → Integration hell when finally merging

The release/* Branches — Release Preparation #

When develop has all the features for the upcoming release, a release branch is created for final preparation — last bug fixes, version updates, changelog updates — without any new features.

# Creating a release branch
git checkout develop
git checkout -b release/1.3.0

# Activities on the release branch:
# - Fix minor bugs found during testing
# - Update the version in package.json / go.mod / build.gradle
# - Update CHANGELOG.md
# - Update documentation

# NOT allowed: adding new features!

# When ready to release:
git checkout main
git merge --no-ff release/1.3.0
git tag -a v1.3.0 -m "Release v1.3.0"

git checkout develop
git merge --no-ff release/1.3.0   # sync release changes to develop!

git branch -d release/1.3.0
Release branch purpose:
  → Isolate release "polishing" from in-flight feature development
  → The QA team can focus testing on release/1.3.0 while the dev team
    already starts working on 1.4.0 features in develop
  → Release bug fixes don't delay the next sprint

What's allowed on a release branch:
  ✓ Minor bug fixes from testing
  ✓ Version and changelog updates
  ✓ Release documentation updates

What's NOT allowed:
  ✗ Adding new features (new features go into develop, not release)
  ✗ Letting the release branch live too long

The hotfix/* Branches — Emergency Production Fixes #

When there’s a critical production bug that must be fixed now and can’t wait for the next release cycle, a hotfix branch is created directly from main.

# Critical bug found in production v1.3.0
git checkout main
git checkout -b hotfix/1.3.1

# Fix the bug
# Bump the version to 1.3.1
# Update the changelog

# Merge to main and tag
git checkout main
git merge --no-ff hotfix/1.3.1
git tag -a v1.3.1 -m "Hotfix v1.3.1: fix payment gateway timeout"

# MUST also merge into develop!
git checkout develop
git merge --no-ff hotfix/1.3.1   # so the fix isn't lost in the next release

git branch -d hotfix/1.3.1
Merging hotfixes into develop is the most often forgotten step. If a hotfix isn’t merged into develop, the same bug must be fixed again at the next release — because develop doesn’t have the fix. This is known as version drift and can cause “already fixed” bugs to reappear in the next release.

Deployment: Tags vs the main Branch #

This is the most debated topic, especially for engineers moving between teams with different workflows.

flowchart LR
    subgraph WrongWay["A less proper approach for Gitflow"]
        WDev["develop"]
        WMain["main (HEAD)"]
        WProd["Production"]
        WDev -->|"merge"| WMain
        WMain -->|"deploy from branch HEAD"| WProd
        Note1["main HEAD is mutable\ncan change at any time"]
    end

    subgraph RightWay["The correct approach for Gitflow"]
        RDev["develop"]
        RMain["main"]
        RTag["tag v1.4.0\n(immutable)"]
        RProd["Production"]
        RDev -->|"merge via release"| RMain
        RMain -->|"create tag"| RTag
        RTag -->|"deploy from tag"| RProd
        Note2["tags are immutable\nrollback is always consistent"]
    end

    style Note1 fill:#E74C3C,color:#fff
    style Note2 fill:#27AE60,color:#fff
Why tags, not the main branch?

Branches are mutable:
  New commits can be added at any time
  main@HEAD today isn't main@HEAD last week
  → Deploying from HEAD means it can't be consistently reproduced

Tags are immutable:
  v1.4.0 always points to the same commit
  Can't change after creation
  → Deploying from a tag means you can roll back to the exact same version

A concrete example of tag benefits:
  Production incident! Need to roll back to the previous version.

  With tags:
    kubectl set image deployment/api api=gcr.io/project/api:v1.3.0
    → Always gets the same image, guaranteed

  With branch HEAD:
    main at rollback time may differ from main at first deploy
    → Must track the exact commit SHA — harder and error-prone
# GitHub Actions — CI/CD triggered from tags
name: Deploy to Production

on:
  push:
    tags:
      - 'v*.*.*'   # triggers when a new tag matching vX.Y.Z is created

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Extract version from tag
        run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV

      - name: Build Docker image
        run: |
          docker build -t gcr.io/myproject/api:${{ env.VERSION }} .
          docker push gcr.io/myproject/api:${{ env.VERSION }}          

      - name: Deploy to production
        run: |
          kubectl set image deployment/api \
            api=gcr.io/myproject/api:${{ env.VERSION }}          

# Benefits:
# → Deploys only happen on an explicit decision (creating a tag)
# → Every deployment can be reproduced and cleanly rolled back
# → A clear audit trail: which version was deployed when

Gitflow vs GitHub Flow vs Trunk-Based Development #

Gitflow isn’t the only workflow out there. It’s important to know the alternatives to choose the right one.

Gitflow:
  Structure: 5 branch types (main, develop, feature, release, hotfix)
  Best for: Enterprises, large teams, version-based releases
  Pros: Very tight release control, supports multiple versions
  Cons: Complex, lots of merge overhead, not ideal for daily deploys

GitHub Flow:
  Structure: main + feature branches only
  Best for: Startups, small teams, continuous deployment
  Flow: feature branch → PR → merge to main → automatic deploy
  Pros: Very simple, fast, great for CD
  Cons: No QA isolation, not great for multiple versions

Trunk-Based Development:
  Structure: All developers commit to one branch (main/trunk)
  Best for: Very mature teams with high discipline
  Flow: Small commits straight to main, feature flags to hide incomplete features
  Pros: No long-lived branches, no merge hell
  Cons: Needs high discipline, feature flag management gets complex
Workflow selection guidance:

Small teams (< 5 people) + daily deploys:
  → GitHub Flow or Trunk-Based
  → Gitflow is too much overhead

Medium teams (5-20 people) + weekly/bi-weekly releases:
  → Gitflow or GitHub Flow with an added release branch
  → The most common sweet spot in industry

Large teams (> 20 people) + monthly or versioned releases:
  → Gitflow
  → The control and structure Gitflow provides is very valuable

Products with multiple versions to support:
  → Gitflow — supports maintaining multiple release branches
  → GitHub Flow has no mechanism for this

Consistent Naming Conventions #

Consistent naming conventions enable CI/CD automation and make navigation easier.

# Feature branches
feature/<short-description>
feature/user-authentication
feature/payment-integration
feature/TICKET-123-export-report    # include the ticket ID if present

# Release branches
release/<semver>
release/1.3.0
release/2.0.0-beta

# Hotfix branches
hotfix/<semver>
hotfix/1.3.1
hotfix/1.3.2

# Tags
v<semver>
v1.3.0
v1.3.1
v2.0.0

Semantic Versioning in Gitflow #

Gitflow and SemVer (Semantic Versioning) pair together very naturally.

Format: MAJOR.MINOR.PATCH

MAJOR: breaking changes — not backward compatible
  v1.x.x → v2.0.0
  → Usually from a release branch with a major version bump
  → Very rare, usually comes with a migration guide

MINOR: backward-compatible new features
  v1.2.x → v1.3.0
  → From a regular release branch
  → Several new features, nothing breaking

PATCH: backward-compatible bug fixes
  v1.3.0 → v1.3.1
  → From a hotfix branch
  → Bug fixes only, no new features

A real chronology example:
  v1.0.0  → initial release
  v1.0.1  → hotfix: fix login timeout
  v1.1.0  → release: add notification feature
  v1.1.1  → hotfix: fix duplicate notifications
  v1.2.0  → release: add export feature
  v2.0.0  → release: API overhaul (breaking change)

Gitflow Anti-Patterns to Avoid #

Feature Branches Living Too Long #

// ✗ Anti-pattern:
feature/redesign-checkout → lives for 3 months
→ develop has moved much further ahead
→ Massive merge conflicts at final merge
→ "Merge hell" eating days of time

// ✓ Solution: break large features into small features
feature/checkout-step-1-cart-review    → 3 days
feature/checkout-step-2-address       → 3 days
feature/checkout-step-3-payment       → 4 days
feature/checkout-step-4-confirmation  → 2 days
→ Each merged into develop regularly
→ No large divergence
→ Unfinished features hidden with feature flags

Not Merging Hotfixes into Develop #

# ✗ Anti-pattern: hotfix merged into main, but forgotten for develop

git checkout main
git merge hotfix/1.3.1
git tag v1.3.1
# STOP — no merge into develop!
# Result: the same bug reappears in v1.4.0!

# ✓ Solution: always merge into develop too
git checkout main
git merge --no-ff hotfix/1.3.1
git tag -a v1.3.1 -m "Hotfix: fix payment timeout"

git checkout develop
git merge --no-ff hotfix/1.3.1   # MANDATORY!
git push origin develop

Branch Protection Not Configured #

// ✗ Anti-pattern: no branch protection
Anyone can push directly to main or develop
→ One accidental push can break production
→ Code review isn't mandatory

// ✓ Solution: branch protection rules in GitHub/GitLab
For the main branch:
  - Require pull request before merging
  - Require approvals: 1-2 reviewers
  - Require status checks to pass (CI must be green)
  - Restrict pushes: only the CI bot may merge
  - Do not allow bypassing the above settings

For the develop branch:
  - Require pull request before merging
  - Require approvals: 1 reviewer minimum
  - Require status checks to pass

Deploying from develop to Production #

// ✗ Anti-pattern often seen in the field:
developer merges feature into develop
→ CI/CD deploys develop straight to production
→ Even though develop may contain half-finished features!

// ✓ Solution: create a release branch first
develop (stable) → release/x.y.z → testing → merge to main → tag → deploy

// The correct mapping:
develop  → staging environment (for preview, not production!)
main     → production (via tags)

Gitflow Checklist #

INITIAL SETUP:
  □ main and develop branches exist and are configured as protected branches
  □ Branch protection rules enabled: required PRs, required reviews, CI must pass
  □ .gitignore and .gitattributes configured correctly
  □ Branch naming conventions documented and agreed by the team
  □ Semantic versioning agreed (tag format: vX.Y.Z)

FEATURE DEVELOPMENT:
  □ Every feature worked on in a separate feature branch
  □ Feature branches created from develop (not from main)
  □ Feature branches synced with develop regularly (pull/rebase)
  □ Feature branches merged into develop via PR with at least 1 reviewer
  □ Feature branches deleted after merging

RELEASE:
  □ Release branch created from develop when all features are ready
  □ No new features on the release branch — only bug fixes and finalization
  □ Version bumped on the release branch (package.json, go.mod, changelog)
  □ Release branch merged into main AND develop after release
  □ Tag created on main after merging (annotated tag: git tag -a vX.Y.Z)

HOTFIX:
  □ Hotfix branch created from main (not from develop!)
  □ After fixing, merged into main AND develop
  □ Patch version bumped
  □ Tag created on main

CI/CD:
  □ CI runs on every push to feature, develop, release, and hotfix branches
  □ Production deployment triggered from tags (not from branch HEAD)
  □ Staging deployment triggered from develop
  □ Rollback procedure defined and tested

Summary #

  • Gitflow is a release discipline, not just a branching model — its value lies in consistency. Half-heartedly applied Gitflow gives overhead without benefits.
  • Five branches with very different purposes — main (production history), develop (integration), feature (development), release (release finalization), hotfix (emergency fixes). Every branch has rules for where it’s created from and where it merges to.
  • Hotfixes must be merged into develop, not just main — this is the most often forgotten step and causes the same bug to reappear in the next release (version drift).
  • Deploy from tags, not from branch HEAD — tags are immutable and reproducible. Branch HEAD can change at any time. Tags enable clean rollbacks and clear audit trails.
  • CI/CD triggers from tag patternson: push: tags: 'v*.*.*' in GitHub Actions ensures deployments only happen on an explicit release decision, not on every commit to main.
  • Feature branches must be small and short-lived — a feature branch living for weeks is a sign the feature is too big. Break it down into sub-features merged more often.
  • Branch protection is a must — without branch protection, all Gitflow rules can be violated with a single git push --force. Configure required PRs, required reviews, and required CI.
  • Gitflow isn’t for every team — for early-stage startups or teams deploying daily, GitHub Flow is far more pragmatic. Choose a workflow based on team needs, not trends.
  • Release branches enable parallel development — the team can keep working on next-version features in develop while another team does final testing on the release branch.
  • Semantic versioning complements Gitflow — hotfixes produce PATCH bumps, regular releases produce MINOR bumps, breaking changes produce MAJOR bumps. Staying consistent makes changelogs and upgrade paths easier to understand.

← Previous: Postmortem   Next: Trunk Based Development →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact