Skip to main content

GitLab Flow: The Middle Ground Between GitFlow and GitHub Flow

Environment branches, release branches, or a production branch: how to pick the GitLab Flow variant that matches the way you deploy.

Updated 6 min readMarkdown
Part of seriesGit Branching Strategies: A Practical Guide for Teams

Here's a problem I keep running into. GitHub Flow is lovely, but the moment someone says "we need a staging environment" or "we can't push to production until Thursday," it starts to wobble. GitFlow handles those cases, but it brings so much ceremony that nobody's happy.

GitLab Flow sits in between. It keeps the simplicity of GitHub Flow, adds just enough structure for real-world release constraints, and ties tightly into issue tracking and CI/CD. GitLab introduced it as a response to exactly the pain points above.

Worth knowing up front: GitLab Flow isn't one rigid workflow. It's a family of three patterns that all share the same base: feature branches, merge requests, and main as the integration branch. You pick the variant that matches your deployment reality.

The shared foundation

  • Work happens on short-lived feature branches.
  • Changes land via merge requests (MRs) into main.
  • main is the source of truth, and everything flows downstream from it.
  • Upstream first: a fix goes into main first, then gets cherry-picked or merged downstream. Never the reverse. This single rule prevents the "fixed in prod but not in main" mess.

Variant 1: Environment branches

Use this when you deploy to multiple environments and want branches to represent what's running where.

Code only moves one direction: main → staging → production. A commit reaches production only after it's proven itself in the earlier stages.

gitGraph
  commit id: "initial"
  branch staging
  branch production
  checkout main
  branch feature/invoice
  checkout feature/invoice
  commit id: "add invoice"
  commit id: "tests"
  checkout main
  merge feature/invoice
  checkout staging
  merge main
  checkout production
  merge staging tag: "deployed"

Typical commands:

# Promote main to staging
git checkout staging
git merge --ff-only main
git push origin staging

# After verification, promote to production
git checkout production
git merge --ff-only staging
git push origin production

Using --ff-only guarantees that the environment branch is exactly what you tested, with no surprise merge commits.

Good for: web apps with dev/staging/prod pipelines, or teams that need an explicit promotion step and an audit trail of what went where.

Variant 2: Release branches

Use this when you must support multiple released versions, such as mobile apps, packaged software, or self-hosted products.

Release branches are cut from main when a version is ready. Bug fixes land in main first, then get cherry-picked into the affected release branches:

# fix lands on main via MR first, grab its SHA
git checkout release/1.0
git cherry-pick -x <sha-of-fix-on-main>
git tag -a v1.0.1 -m "Patch release 1.0.1"
git push origin release/1.0 --tags

The -x flag appends "(cherry picked from commit …)" to the message, which is extremely helpful when you're auditing later.

Good for: products where customers run different versions and you backport security and critical fixes.

Variant 3: Production branch (with or without pre-production)

This is the lightest option. You add one extra branch, production, that you merge into whenever you want to release. It's GitHub Flow plus a deliberate deploy gate.

You merge into production on your own schedule: nightly, weekly, after a sign-off, whatever. Tagging commits on production gives you release markers.

Good for: teams that want continuous integration into main but need control over when things go live.

Choosing a variant

What GitLab brings to the party

GitLab Flow was designed alongside GitLab's tooling, so a few things come almost for free:

  • Issue-driven branches. Create a branch from an issue and its name gets linked automatically. Merging the MR can close the issue with Closes #123.
  • Merge request pipelines. Run CI on the MR source branch, or on the merged result, so you're testing what will actually land.
  • Protected branches & environments. Restrict who can push to production and who can approve deployments.
  • Review apps. Spin up a temporary environment per MR for reviewers to click through.

A minimal .gitlab-ci.yml for the environment-branch variant:

stages: [test, deploy]

test:
  stage: test
  script:
    - npm ci
    - npm test

deploy_staging:
  stage: deploy
  script: ./deploy.sh staging
  environment: staging
  rules:
    - if: $CI_COMMIT_BRANCH == "staging"

deploy_production:
  stage: deploy
  script: ./deploy.sh production
  environment: production
  when: manual
  rules:
    - if: $CI_COMMIT_BRANCH == "production"

Note when: manual on production, which gives you a human approval button without any extra branching.

Pros

  • Flexible. You choose the amount of structure you need.
  • Simpler than GitFlow while still handling environments and versions.
  • Upstream-first discipline keeps main authoritative and avoids drift.
  • Strong CI/CD integration if you're on GitLab.
  • Clear promotion path with an auditable record of what's in each environment.

Cons

  • "Which variant are we using?" needs an explicit team decision and documentation. Without it, you get a hybrid nobody understands.
  • Cherry-picking at scale is tedious and can cause duplicate commits and subtle conflicts if you're not careful.
  • Environment branches can drift if someone commits directly to them. Protect them.
  • Slower than trunk-based since changes wait to be promoted.

Best practices

  • Write down which variant you're using in your CONTRIBUTING.md, including the promotion steps.
  • Protect every long-lived branch (main, staging, production, release/*).
  • Always fix upstream first. Make it a hard rule in code review.
  • Use --ff-only merges for environment promotion so you deploy exactly what was tested.
  • Use git cherry-pick -x for backports so the origin is traceable.
  • Keep MRs small and short-lived, same as any other flow.
  • Squash on merge into main to make cherry-picking a single commit instead of a series.
  • Delete merged feature branches, but obviously keep the environment and release ones.

Wrapping up

GitLab Flow is the "grown-up GitHub Flow." If your team has outgrown a single main branch but doesn't need the full GitFlow machinery, it's probably where you'll end up. Pick the variant that matches how you really deploy, write it down, and automate the promotion steps.


If you enjoyed this, consider supporting me with a coffee: buymeacoffee.com/vatul16 ☕

Thanks for reading!

About the author

  • Hi, I'm Atul! I am a software engineer with a strong focus on DevOps, Cloud Infrastructure, and automation. I spend my time building scalable CI/CD pipelines, mastering tools like Docker, Terraform, and AWS, and breaking down complex cloud concepts into accessible, hands-on tutorials. I believe the best way to learn is to build, break, and document the process.

No comments yet. Be the first to comment.

Git Branching Strategies: A Practical Guide for Teams

Part 3 of 3

A five-part, hands-on series on Git branching strategies. We go through GitFlow, GitHub Flow, GitLab Flow and Trunk-Based Development, with diagrams, real commands, pipeline examples, pros, cons and best practices. The final post helps you decide which strategy fits your team, release style and level of automation.

Start from the beginningGitFlow Workflow: The Traditional Branching Model, and When It Still Makes SenseA practical guide to main, develop, release and hotfix branches, with commands, diagrams, and the mistakes that trip teams up.

More from this blog