# GitLab Flow: The Middle Ground Between GitFlow and GitHub Flow

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.

![](https://cdn.hashnode.com/uploads/covers/6a11b66c1a3cf7bffc297b8a/3b46eba3-fa36-4ec7-9007-788b02fa627a.svg align="center")

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

```mermaid
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:**

```bash
# 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.

![](https://cdn.hashnode.com/uploads/covers/6a11b66c1a3cf7bffc297b8a/1dc595cb-2b83-465e-9595-5965cdbc1334.svg align="center")

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:

```bash
# 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.

![](https://cdn.hashnode.com/uploads/covers/6a11b66c1a3cf7bffc297b8a/b32e59c1-8c80-4e29-a315-74619308a900.svg align="center")

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

![](https://cdn.hashnode.com/uploads/covers/6a11b66c1a3cf7bffc297b8a/e4947330-f7ec-407f-a5ac-b0a0d301690c.svg align="center")

## 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:

```yaml
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**](https://buymeacoffee.com/vatul16) ☕

Thanks for reading!
