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.
GitLab Flow: Environment and Release-Based Branching
GitHub Flow works great when you deploy straight to production. But what if your team needs a staging validation step first? Or you’re shipping versioned software to customers on different release trains? That’s where GitLab Flow comes in.
It takes GitHub Flow’s simplicity and adds structure for teams that can’t—or shouldn’t—deploy straight from main. GitLab created it based on how their own teams actually worked: code goes through staging before it reaches production.
Two main variants exist: environment-based branching for teams with staging environments, and release-based branching for teams managing multiple versions. Both follow the same downstream principle: code flows from main toward increasingly stable branches.
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
| Branch | Purpose | Deployment Target |
|---|---|---|
main | Development and staging | Staging environment |
production | Production-ready code | Production 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
| Branch | Purpose | Deployment Target |
|---|---|---|
main | Active development | Pre-release / Nightly |
release/X.Y | Stable release branch | Production |
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
| Scenario | What Happens | Mitigation |
|---|---|---|
| Staging passes, production fails | Environment difference causes production-only bug | Use identical infrastructure for staging and production; run smoke tests post-deploy |
| Production branch diverges | Hotfixes to production aren’t merged back to main | Enforce merge-back policy; CI checks that production is an ancestor of main |
| Release branch staleness | Old release branches accumulate without cleanup | Archive release branches after 2 versions; automate branch lifecycle management |
| Merge conflict during promotion | main to production merge has conflicts | Resolve conflicts in main first; never resolve in production |
| Pipeline bottleneck | Long CI pipelines delay staging deployments | Parallelize test suites; use incremental builds; cache dependencies |
Trade-off Analysis
| Aspect | Advantage | Disadvantage |
|---|---|---|
| Environment mapping | Clear branch-to-environment relationship | Extra merge step for production |
| Simplicity | Fewer branch types than Git Flow | More complex than GitHub Flow |
| Staging validation | Dedicated environment for QA | Requires maintaining staging infrastructure |
| Release management | Release branches for versioned software | Manual branch cutting and maintenance |
| CI/CD integration | Native GitLab CI/CD pipeline support | Less portable to other CI systems |
| Audit trail | Clear promotion path from staging to production | Additional 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 / Anti-Patterns
- The Staging Quagmire — Staging becomes a dumping ground for half-tested features. Only merge to main when features are complete and tested.
- Production Branch Divergence — Hotfixes applied directly to production without merging back to main cause the bug to reappear. Always merge back.
- Manual Deployment Drift — Manual deployments outside the pipeline create inconsistency. All deployments must go through CI/CD.
- Environment Configuration Drift — Staging and production configurations diverge over time. Use infrastructure as code to keep them identical.
- Release Branch Proliferation — Too many active release branches create confusion. Limit to 2-3 active releases maximum.
- Skipping Staging Validation — Auto-promoting from staging to production without validation defeats the purpose. Require explicit approval.
- Pipeline Coupling — Tying deployment to pipeline success without manual gates for production is risky. Use
when: manualfor production.
Quick Recap Checklist
-
mainbranch deploys to staging automatically -
productionbranch receives merges frommainonly - 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 Questions
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.
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.
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.
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.
GitHub Flow is a simpler model with a single main branch and feature branches. Every merge to main goes directly to production with automated CI/CD. It assumes continuous deployment.
GitLab Flow adds an intermediate production branch between main and production. This creates a staging gate where validated code on main is explicitly promoted to production. GitLab Flow is designed for teams that need environment separation and manual approval before production deployment.
GitLab CI/CD uses environment-scoped variables. Secrets are stored as CI/CD variables with environment scope, ensuring that staging credentials never reach the production environment. The environment: block in job definitions binds jobs to specific deployment targets, and GitLab's environment dashboard provides a live view of what is deployed where.
When conflicts occur during the main to production merge, you resolve them in main first, never in the production branch. The fix involves checking out a feature branch from the current main, merging production into it to create a conflict resolution commit, then merging that resolved branch back into main. Once main is conflict-free and re-validated, the promotion merge to production proceeds cleanly.
Production divergence is prevented through a merge-back policy enforced by CI. Every hotfix merged to production must also be merged to main before the pipeline can complete on production. A CI job can check that main is an ancestor of production using git merge-base --is-ancestor. If the hotfix was not merged back, the pipeline fails and blocks the production deployment.
When staging and production differ, bugs that only appear in production slip through staging validation. This is the "staging passes, production fails" problem. Identical infrastructure—ideally managed through infrastructure as code (Terraform, Pulumi)—ensures that the staging environment exercises the same configurations, service versions, and network paths as production. Any difference is a potential production-only bug hiding in plain sight.
Each CI job that deploys can declare an environment: block with a name and url. GitLab automatically tracks these deployments in its environment dashboard, showing which commits are currently deployed to each environment, the deployment history, and the health status. When a merge request promotes code between environments, GitLab links the merge request to both the source and target environment deployments, creating a complete traceability trail.
Branch protection rules enforce the quality gates at each environment boundary. For main, rules typically require: at least 2 MR approvals, a passing CI pipeline, and no direct pushes. For production, rules are stricter: only merges from main are allowed, no direct pushes, and manual approval is required in the pipeline. These rules are configured via the GitLab UI or API and ensure that no code reaches an environment without passing through the defined workflow.
Long staging validation creates a bottleneck. The mitigation involves parallelizing pipelines (running test suites simultaneously), using incremental builds (caching unchanged layers), and implementing fast-failing smoke tests that give a preliminary pass/fail within minutes before running full regression suites. Additionally, staging validation should not block subsequent development on main; features can be merged to main while earlier features are still in staging validation.
Choose environment-based when you deploy a web application to servers you control (staging then production) and want continuous delivery. Choose release-based when you ship versioned software to customers (mobile apps, libraries, on-premise software) with scheduled or event-driven releases. Release-based is also appropriate when you need to maintain multiple simultaneously-supported versions in production (e.g., v1.x, v2.x) and cannot auto-push every main merge to all customers.
Rollback in environment-based GitLab Flow is straightforward: you revert the production merge by creating a new merge from the previous known-good commit to production, or by using git revert on the merge commit. In GitLab, you can reopen the previous merge request or create a new rollback MR. The pipeline's smoke tests on production should catch the failure within minutes, triggering an alert. Automating rollback scripts that run on pipeline failure reduces mean time to recovery.
Key metrics include: lead time (time from commit to production), deployment frequency (deploys per day/week per environment), change failure rate (percentage of deploys causing production failures), mean time to recovery (how fast rollbacks happen), and staging-to-production promotion time (how long code sits in staging before approval). These metrics align with DORA benchmarks and reveal bottlenecks in the pipeline.
when: manual in a GitLab CI job means the job does not start automatically—it requires a user to click "play" in the GitLab UI or trigger it via API. This is used for production deployments where a human must approve. Automatic jobs run as soon as their conditions are met (branch match, pipeline source, etc.). The distinction is critical: staging deploys should be automatic (every merge to main deploys), but production deploys should be manual to maintain a human gate.
Version drift occurs when staging and production run different commit SHAs. GitLab Flow mitigates this through: 1) Mandatory promotion workflow where main is explicitly merged to production, 2) Environment dashboard tracking what version is deployed where, 3) CI checks that verify production is always a descendant of main (no divergence), 4) Regular synchronization schedules where long-staging features are rebased or closed. If drift occurs, the fix is always: merge main into production (never the reverse).
GitLab's pipeline DAG (Directed Acyclic Graph) structures jobs so that: 1) test jobs run first (gate quality), 2) build jobs run after tests pass (create artifacts), 3) deploy-staging jobs run after builds (promote to staging), 4) deploy-production jobs run after staging deploys (require manual gate). This DAG enforces that code cannot reach production without passing through each preceding stage. Jobs can run in parallel within a stage, but the stage ordering is strictly enforced by dependency relationships.
Feature flags decouple deployment from release, allowing GitLab Flow benefits even with continuous deployment: 1) Merge feature branches to main as usual (all features behind flags are disabled in production), 2) The flag system (LaunchDarkly, Unleash) controls who sees the feature, not the branch structure, 3) When the feature is validated, enable it for the target percentage, 4) Remove the flag code once fully rolled out. This lets GitLab Flow provide environment gates for non-production-ready features while enabling rapid iteration on flags.
The merge-back policy is enforced by CI: every commit to production must also be merged to main before the pipeline completes. GitLab's pipeline can check git merge-base --is-ancestor main production — if main is not an ancestor of production, the pipeline fails and blocks deployment. This ensures: (1) hotfixes on production are never lost, (2) main always contains all production changes (no regression risk), (3) the commit graph stays linear-ish with downstream-only merges, (4) audit trails show exactly when each production change arrived on main.
Further Reading
- GitLab Flow documentation — Official GitLab guide
- GitLab CI/CD best practices — Pipeline configuration guide
- Environment-based deployments — GitLab environment management
- GitHub Flow vs GitLab Flow — Atlassian comparison
- Infrastructure as Code — Keep staging and production identical with Terraform
Conclusion
GitLab Flow gives you deployment gates without Git Flow’s ceremony. It works well when you genuinely need that staging step—or when you’re shipping versioned releases to customers on different schedules. If you don’t have those needs, GitHub Flow is simpler.
The implementation boils down to two branches (or a few for releases), CI validating every merge, and a manual gate for production. That’s it.
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.
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.
Choosing a Git Team Workflow: Decision Framework
Decision framework for selecting the right Git branching strategy based on team size, release cadence, and project type.