GitLab Flow: Environment and Release-Based Branching

Master GitLab Flow — the branching strategy that combines Git Flow simplicity with deployment pipelines. Learn environment-based and release-based branching patterns.

published: reading time: 12 min read updated: March 31, 2026

GitLab Flow: Environment and Release-Based Branching

GitLab Flow sits between GitHub Flow and Git Flow. It keeps the simplicity of a single main branch but adds environment-based branches for teams that need deployment stage separation. Created by GitLab, it reflects the reality that many teams deploy through staging environments before reaching production.

The model comes in two variants: environment-based branching for teams with staging environments, and release-based branching for teams with scheduled releases. Both variants share the same core principle: code flows downstream from main through increasingly stable branches.

This post covers both GitLab Flow variants, when each makes sense, and how to implement them without creating the complexity trap that Git Flow is known for.

When to Use / When Not to Use

Use GitLab Flow When

  • Staging environment required — You deploy to staging before production for validation
  • Multiple deployment targets — Different branches map to different environments
  • Scheduled releases with CI/CD — You want release branches but with automated pipelines
  • GitLab users — Native integration with GitLab CI/CD and environment management
  • Compliance requirements — You need environment separation for audit purposes

Do Not Use GitLab Flow When

  • Pure continuous deployment — If you deploy every merge directly to production, use GitHub Flow
  • No staging environment — If you only have production, environment branches add no value
  • Complex release management — If you need hotfixes, support branches, and version maintenance, Git Flow may be more appropriate
  • Small teams — The environment branch overhead may not be justified

Core Concepts

GitLab Flow has two variants, each with distinct branch structures:

Environment-Based Branching

BranchPurposeDeployment Target
mainDevelopment and stagingStaging environment
productionProduction-ready codeProduction environment

Code flows from main to production via merge. Every merge to main deploys to staging. When staging is validated, main merges to production.


graph LR
    A[feature branch] -->|merge| B[main - Staging]
    B -->|merge| C[production - Live]
    B --> D[Auto Deploy to Staging]
    C --> E[Auto Deploy to Production]

Release-Based Branching

BranchPurposeDeployment Target
mainActive developmentPre-release / Nightly
release/X.YStable release branchProduction

Similar to Git Flow but simpler: main is always the cutting edge, and release branches are cut for production versions.


graph LR
    A[feature] -->|merge| B[main]
    B -->|cut| C[release/1.0]
    B -->|cut| D[release/2.0]
    C --> E[Production 1.0]
    D --> F[Production 2.0]
    C -->|cherry-pick| G[release/1.0.1]

Architecture and Flow Diagram

The complete GitLab Flow with environment-based branching and CI/CD pipeline:


graph TD
    A[Developer] -->|push| B[feature branch]
    B -->|MR + CI| C{CI Pass?}
    C -->|No| D[Fix and retry]
    D --> B
    C -->|Yes| E[Merge to main]
    E --> F[Deploy to Staging]
    F --> G[QA Validation]
    G -->|Approved| H[Merge main to production]
    H --> I[Deploy to Production]
    H --> J[Create Release Tag]

Step-by-Step Guide

1. Environment-Based Setup

Start with the simplest variant — two branches mapping to two environments:


# Initialize repository with main branch
git checkout -b main
git push -u origin main

# Create production branch
git checkout -b production
git push -u origin production

# Set up branch protection
# main: requires MR approval, CI must pass
# production: requires MR from main only, manual approval

2. Feature Development

Features branch from main and merge back through merge requests:


# Create feature branch
git checkout main
git checkout -b feat/user-dashboard

# Develop and push
git add .
git commit -m "feat: add user dashboard with analytics"
git push -u origin feat/user-dashboard

# Open merge request
gitlab mr create \
  --source-branch feat/user-dashboard \
  --target-branch main \
  --title "Add user dashboard" \
  --description "Implements the user dashboard with analytics widgets"

3. Deploy to Staging

Every merge to main triggers staging deployment:

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy-staging
  - deploy-production

test:
  stage: test
  script:
    - npm ci
    - npm run lint
    - npm test
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'

build:
  stage: build
  script:
    - npm ci
    - npm run build
  artifacts:
    paths:
      - dist/
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

deploy-staging:
  stage: deploy-staging
  script:
    - ./scripts/deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

4. Promote to Production

When staging is validated, merge main into production:


# Create merge request from main to production
git checkout production
git merge main
git push origin production

# Or via GitLab CLI
gitlab mr create \
  --source-branch main \
  --target-branch production \
  --title "Promote to production" \
  --description "Staging validated. Ready for production deployment."

5. Release-Based Variant

For teams that need versioned releases:


# Cut a release branch from main
git checkout main
git checkout -b release/2.0
git push -u origin release/2.0

# Deploy release branch to production
# .gitlab-ci.yml deploys release/* branches to production

# Hotfix for release
git checkout release/2.0
git checkout -b hotfix/2.0.1
# Fix the bug
git commit -am "fix: resolve payment processing error"
git checkout release/2.0
git merge hotfix/2.0.1
git push origin release/2.0

# Merge hotfix back to main
git checkout main
git merge release/2.0
git push origin main

Production Failure Scenarios + Mitigations

ScenarioWhat HappensMitigation
Staging passes, production failsEnvironment difference causes production-only bugUse identical infrastructure for staging and production; run smoke tests post-deploy
Production branch divergesHotfixes to production aren’t merged back to mainEnforce merge-back policy; CI checks that production is an ancestor of main
Release branch stalenessOld release branches accumulate without cleanupArchive release branches after 2 versions; automate branch lifecycle management
Merge conflict during promotionmain to production merge has conflictsResolve conflicts in main first; never resolve in production
Pipeline bottleneckLong CI pipelines delay staging deploymentsParallelize test suites; use incremental builds; cache dependencies

Trade-offs

AspectAdvantageDisadvantage
Environment mappingClear branch-to-environment relationshipExtra merge step for production
SimplicityFewer branch types than Git FlowMore complex than GitHub Flow
Staging validationDedicated environment for QARequires maintaining staging infrastructure
Release managementRelease branches for versioned softwareManual branch cutting and maintenance
CI/CD integrationNative GitLab CI/CD pipeline supportLess portable to other CI systems
Audit trailClear promotion path from staging to productionAdditional merge commits in history

Implementation Snippets

GitLab CI/CD — Full Pipeline

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy-staging
  - deploy-production
  - verify

variables:
  NODE_VERSION: "20"

.test-template: &test-config
  image: node:${NODE_VERSION}
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
  before_script:
    - npm ci

unit-test:
  <<: *test-config
  stage: test
  script:
    - npm run test:unit
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

integration-test:
  <<: *test-config
  stage: test
  script:
    - npm run test:integration
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

build:
  <<: *test-config
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_BRANCH =~ /^release\/.*/'

deploy-staging:
  stage: deploy-staging
  script:
    - ./scripts/deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

deploy-production:
  stage: deploy-production
  script:
    - ./scripts/deploy.sh production
  environment:
    name: production
    url: https://example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "production"'
  when: manual

verify-production:
  stage: verify
  script:
    - ./scripts/smoke-test.sh https://example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "production"'

Branch Protection via GitLab API


# Protect main branch
curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/protected_branches/main" \
  --data "allowed_to_push[]=access_level:40" \
  --data "allowed_to_merge[]=access_level:30" \
  --data "allowed_to_push[]=access_level:30"

# Protect production branch (only merges from main)
curl --request POST --header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
  "https://gitlab.example.com/api/v4/projects/$PROJECT_ID/protected_branches/production" \
  --data "allowed_to_push[]=access_level:40" \
  --data "allowed_to_merge[]=access_level:40"

Environment Promotion Script


#!/bin/bash
# scripts/promote-to-production.sh
set -euo pipefail

echo "Promoting main to production..."

# Verify staging is healthy
STAGING_STATUS=$(curl -s -o /dev/null -w "%{http_code}" https://staging.example.com/health)
if [ "$STAGING_STATUS" != "200" ]; then
  echo "ERROR: Staging health check failed (HTTP $STAGING_STATUS)"
  exit 1
fi

# Create merge request
MR_URL=$(gitlab mr create \
  --source-branch main \
  --target-branch production \
  --title "Promote $(git rev-parse --short HEAD) to production" \
  --description "Staging validated. Commit: $(git log -1 --format='%h %s')" \
  --json | jq -r '.web_url')

echo "Merge request created: $MR_URL"
echo "Awaiting approval for production deployment."

Observability Checklist

  • Logs: Log every environment deployment with commit SHA, branch, and deployer identity
  • Metrics: Track staging-to-production promotion time, deployment success rate per environment, and pipeline duration
  • Traces: Trace each production deployment through staging validation to the original merge request
  • Alerts: Alert when staging is unhealthy for > 1 hour, production deployment fails, or promotion queue exceeds 24 hours
  • Dashboards: Display environment health, deployment frequency, promotion success rate, and pipeline lead time

Security and Compliance: GitLab Flow

  • Environment separation: Production branch should have stricter access controls than main
  • Manual approval: Require manual approval for production deployments in CI/CD pipeline
  • Audit trail: GitLab’s merge request history provides a complete audit trail of promotions
  • Secret management: Use GitLab CI/CD variables with environment scoping for environment-specific secrets
  • Compliance: Environment-based branching naturally supports compliance requirements for staging validation before production

Environment-Based CI/CD Pipeline

# .gitlab-ci.yml — Full environment promotion pipeline
stages:
  - test
  - build
  - deploy-staging
  - deploy-production
  - verify

variables:
  NODE_VERSION: "20"

.test-template: &test-config
  image: node:${NODE_VERSION}
  cache:
    key: ${CI_COMMIT_REF_SLUG}
    paths:
      - node_modules/
  before_script:
    - npm ci

unit-test:
  <<: *test-config
  stage: test
  script:
    - npm run test:unit
  rules:
    - if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
    - if: '$CI_COMMIT_BRANCH == "main"'

integration-test:
  <<: *test-config
  stage: test
  script:
    - npm run test:integration
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

build:
  <<: *test-config
  stage: build
  script:
    - npm run build
  artifacts:
    paths:
      - dist/
    expire_in: 1 week
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
    - if: '$CI_COMMIT_BRANCH =~ /^release\/.*/'

deploy-staging:
  stage: deploy-staging
  script:
    - ./scripts/deploy.sh staging
  environment:
    name: staging
    url: https://staging.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'

deploy-production:
  stage: deploy-production
  script:
    - ./scripts/deploy.sh production
  environment:
    name: production
    url: https://example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "production"'
  when: manual

verify-production:
  stage: verify
  script:
    - ./scripts/smoke-test.sh https://example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "production"'

Common Pitfalls and Anti-Patterns

  1. The Staging Quagmire — Staging becomes a dumping ground for half-tested features. Only merge to main when features are complete and tested.
  2. Production Branch Divergence — Hotfixes applied directly to production without merging back to main cause the bug to reappear. Always merge back.
  3. Manual Deployment Drift — Manual deployments outside the pipeline create inconsistency. All deployments must go through CI/CD.
  4. Environment Configuration Drift — Staging and production configurations diverge over time. Use infrastructure as code to keep them identical.
  5. Release Branch Proliferation — Too many active release branches create confusion. Limit to 2-3 active releases maximum.
  6. Skipping Staging Validation — Auto-promoting from staging to production without validation defeats the purpose. Require explicit approval.
  7. Pipeline Coupling — Tying deployment to pipeline success without manual gates for production is risky. Use when: manual for production.

Quick Recap Checklist

  • main branch deploys to staging automatically
  • production branch receives merges from main only
  • Every merge request requires CI validation
  • Production deployments require manual approval
  • Hotfixes merge back to both production and main
  • Environment-specific secrets are scoped correctly
  • Staging and production infrastructure are identical
  • Deployment logs include commit SHA and deployer identity
  • Release branches are archived after 2 versions
  • Pipeline stages are parallelized for speed

Interview Q&A

What is the difference between environment-based and release-based GitLab Flow?

Environment-based branching maps branches to deployment environments: main deploys to staging, production deploys to production. Code flows downstream from main through merge. This is ideal for web applications with continuous deployment.

Release-based branching maps branches to software versions: main is cutting-edge development, release/X.Y branches are stable versions deployed to production. This is ideal for versioned software with scheduled releases.

How do you handle hotfixes in GitLab Flow?

In environment-based GitLab Flow, hotfixes branch from production, fix the issue, merge back to production, and then merge to main to prevent regression.

In release-based GitLab Flow, hotfixes branch from the specific release/X.Y branch, merge back to that release, and then merge to main. This ensures the fix reaches both the current production version and future development.

Why does GitLab Flow require manual approval for production deployment?

Manual approval provides a human checkpoint between staging validation and production deployment. Even with comprehensive automated testing, some issues only surface in production-like environments. The approval step ensures someone has verified staging results, reviewed the changelog, and confirmed the deployment timing is appropriate.

This is distinct from GitHub Flow's fully automated deployment — GitLab Flow acknowledges that many organizations need a gate between staging and production for risk management.

How does GitLab Flow compare to Git Flow?

GitLab Flow is simpler than Git Flow — it has fewer branch types and a clearer code flow direction. Git Flow has five branch types with complex merge patterns; GitLab Flow has two or three branches with downstream-only merges.

GitLab Flow also integrates more naturally with CI/CD pipelines, where each branch maps to a deployment stage. Git Flow was designed before CI/CD was ubiquitous and requires additional tooling to map branches to deployment environments.

Resources

Category

Related Posts

Git Flow: The Original Branching Strategy Explained

Master the Git Flow branching model with master, develop, feature, release, and hotfix branches. Learn when to use it, common pitfalls, and production best practices.

#git #version-control #branching-strategy

Automated Changelog Generation: From Commit History to Release Notes

Build automated changelog pipelines from git commit history using conventional commits, conventional-changelog, and semantic-release. Learn parsing, templating, and production patterns.

#git #version-control #changelog

Choosing a Git Team Workflow: Decision Framework for Branching Strategies

Decision framework for selecting the right Git branching strategy based on team size, release cadence, project type, and organizational maturity. Compare Git Flow, GitHub Flow, and more.

#git #version-control #branching-strategy