OvertimeLabs.ai
Enterprise AI8 min read30 May 2026

Migrating off tag proliferation to branch/environment CI/CD in GitLab Ultimate

TL;DR

Tag-driven releases collapse under their own weight: hundreds of release tags, no audit trail, and rebuilds per environment. Replace them with a branch/environment promotion model in GitLab Ultimate — protected branches, merge-request approval gates, LDAP/AD-mapped permissions, and a build-once-promote pipeline. The same governance backbone is what makes AI-assisted development safe to merge.

Tag proliferation is a symptom, not the disease. The disease is that nobody can answer "what is in production, who approved it, and can we reproduce it?" The fix is to stop treating tags as a release mechanism and instead model environments as branches, gate promotion with merge requests and approvals, and build the artifact exactly once. In GitLab Ultimate this is mostly configuration, not custom tooling — and it is the same backbone that lets you merge AI-generated code without losing your audit trail.

Why does tag-driven release management fall apart?

A tag-per-release scheme starts innocent: v1.4.2, v1.4.2-hotfix, release-2024-03-prod. Within a year you have thousands of tags, three naming conventions, and tags that point at commits nobody can trace to an approval.

The specific failures I see repeatedly:

  • No promotion semantics. A tag tells you a commit was named. It does not tell you whether that exact code passed staging, who signed it off, or what got deployed where. The environment history lives in someone's memory or a spreadsheet.
  • Rebuilds per environment. Most tag pipelines rebuild from source for each target. Build for staging, build again for prod. Now your prod binary is not bit-for-bit the thing QA tested — different base image digest, different transient dependency, different build clock. This is the bug that takes three days to find.
  • Weak access control. Protecting v* tags is coarse. Anyone who can push a matching tag can trigger a release pipeline. There is no per-environment gate.
  • No audit answer. When an auditor asks "show me the approval for the change currently in production," a pile of tags is not an answer.

Tags are fine for marking a point in history. They are a terrible state machine.

What does a branch/environment promotion model look like?

Model each environment as a long-lived protected branch, and treat deployment as a forward-only promotion between them. A typical topology:

feature/* ──MR──▶ main ──MR──▶ release/staging ──MR──▶ release/production
   (dev)         (CI gate)     (deploys staging)       (deploys prod)
  • main is the integration branch. Every merge runs the full build and test suite.
  • release/staging and release/production are protected branches that map one-to-one to GitLab Environments. A merge into one of them is a deployment to that environment.
  • Promotion is always a merge request: mainstagingproduction. You never deploy code that has not flowed through the previous stage.
  • Hotfixes branch from production, get fixed, deploy, then merge back down so the fix is not lost on the next promotion.

The key shift: "release" is no longer an act of naming a commit. It is an act of merging an approved MR into a protected environment branch. The merge request is the audit record — author, approvers, pipeline result, and diff, all in one immutable object.

You still cut a git tag on the production commit if you want a human-readable version label. But the tag is now a label on a governed event, not the trigger for one.

How do you make releases auditable and decide who approves?

This is the question buyers actually care about, so be concrete.

Auditability comes from three GitLab Ultimate features working together:

  1. Merge request approval rules — codify who must approve, per target branch. In Ultimate you can require approval from a specific group (e.g. a "Release Managers" group), enforce a minimum count, and prevent authors from approving their own MRs.
  2. Protected branches and protected environments — only the CI service account (and nobody's laptop) can write to release/production; only named users/groups can approve the deploy job.
  3. The pipeline record — every promotion carries the pipeline that ran on it, so the test evidence is attached to the approval, not floating separately.

A .gitlab-ci.yml deploy job gated on a protected environment:

deploy_production:
  stage: deploy
  environment:
    name: production
    url: https://app.example.com
  rules:
    - if: '$CI_COMMIT_BRANCH == "release/production"'
  script:
    - ./deploy.sh "$ARTIFACT_REF"
  # protected env + approval rule below means this
  # job cannot run until an authorised approver releases it

Pair it with a project approval rule, expressed as code via the API or set in the UI:

# Approval rule for MRs targeting release/production
name: "Production release sign-off"
approvals_required: 2
eligible_approvers: ["@release-managers"]
applies_to_branch: "release/production"
prevent_author_approval: true

Who approves? In practice I split it: a technical owner who confirms the change is sound, and a release manager (or product/change owner) who confirms it is allowed to ship now. Two distinct hats, two required approvals, author excluded. For regulated environments add a protected-environment deployment approval on top, so even after the MR merges, a named approver must release the deploy job.

The result an auditor wants: for any commit in production, one click shows the MR, the diff, both approvers, the green pipeline, and the timestamp. No spreadsheet.

How does identity and permission mapping work with LDAP/AD?

Do not manage GitLab users by hand. Wire GitLab to your corporate directory and let group membership drive permissions.

  • LDAP/AD sync (Ultimate). GitLab authenticates against AD and synchronises group membership on a schedule (commonly every hour). You map an AD group to a GitLab group with a given role, so adding someone to AD-Release-Managers automatically grants them approver rights, and removing them revokes it. Off-boarding becomes a directory action, not a GitLab chore.
  • Map roles to directory groups, not individuals. Developers group → Developer role (can push to feature/*, raise MRs). Release-Managers group → Maintainer role plus the approval rule. The CI deploy identity is a dedicated service account, never a person.
  • Protected branch rules reference groups. "Allowed to merge into release/production: Maintainers" combined with the approval rule means the permission and the directory membership are the same source of truth.

The governance property you get: access is provisioned and de-provisioned by HR/IT actions in AD, and every privileged action in GitLab traces back to a directory identity. That is the control auditors and security teams ask for first.

How do you design a build-once, promote-the-artifact pipeline?

Build the deployable artifact exactly once, on merge to main, and promote that immutable artifact through environments. Never rebuild per target.

build:
  stage: build
  rules:
    - if: '$CI_COMMIT_BRANCH == "main"'
  script:
    - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA" .
    - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
  # image is now addressed by immutable commit SHA digest
 
deploy_staging:
  stage: deploy
  environment: { name: staging }
  rules:
    - if: '$CI_COMMIT_BRANCH == "release/staging"'
  script:
    - ./deploy.sh "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"
 
deploy_production:
  stage: deploy
  environment: { name: production }
  rules:
    - if: '$CI_COMMIT_BRANCH == "release/production"'
  script:
    - ./deploy.sh "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA"

The artifact is addressed by commit SHA and stored in the GitLab Container Registry (or a package registry for non-container builds). Promotion deploys the same digest to the next environment. What QA tested is, byte-for-byte, what ships. Config differences between environments live in environment-scoped CI/CD variables, not in the artifact.

This also kills a whole class of "works in staging, breaks in prod" incidents, because the only thing that changed between environments is configuration you can diff.

Why is this the backbone for AI-assisted development?

AI coding assistants raise volume and lower the cost of producing a change. That is exactly why you need a strong merge gate, not a weaker one. The promotion model above does not care whether a human or an agent wrote the diff — it only cares that the MR passed CI and was approved by an authorised human before it touched a protected environment.

Concretely: AI-generated branches land in feature/*, run the same pipeline, and require the same human approval to promote. The directory-driven permissions mean an agent's commits are still attributed and still gated. You get the throughput benefit of AI assistance without surrendering the audit trail — the human approval on the MR is the control point, and it is non-negotiable.

When is this worth doing, and what are the trade-offs?

Worth doing when: you have multiple environments, more than a handful of engineers, any audit or change-control obligation, or you are introducing AI-assisted development at scale. The bigger the team and the higher the compliance bar, the faster this pays back.

The honest trade-offs:

  • More merge ceremony. Every promotion is an MR with approvals. For a two-person side project this is overkill — stick with tags.
  • Up-front design. Branch topology, approval rules, and LDAP mapping need deciding before you cut over. Budget a focused engagement, typically in the region of one to two weeks for a mid-sized org, plus a migration window.
  • Cultural shift. Engineers used to pushing tags will feel the gate. That friction is the point, but it needs explaining, not just enforcing.
  • GitLab Ultimate licensing. Approval rules, protected environments, and LDAP group sync are paid-tier features. If you are on the free tier, some of this is manual or unavailable.

Done right, you trade a pile of meaningless tags for a system that answers "what is in production and who approved it" in one click — and stays answerable when AI is writing half the commits.

FAQ

How do we make our releases auditable in GitLab?

Make every deployment a merge request into a protected environment branch. The MR captures author, approvers, the diff and the green pipeline as one immutable record. For any commit in production, one click shows who approved it, when, and the test evidence — no spreadsheets.

Who should approve a production release?

Split it into two hats: a technical owner who confirms the change is sound, and a release manager who confirms it is allowed to ship now. Require both approvals, exclude the author, and for regulated systems add a protected-environment deploy approval on top.

How do GitLab permissions map to our Active Directory?

Use LDAP/AD sync in GitLab Ultimate. Map AD groups to GitLab groups with roles, so directory membership drives permissions. Adding someone to an AD release group grants approver rights; removing them revokes it. Off-boarding becomes an HR action, and every privileged action traces to a directory identity.

Want this built — or reviewed — properly?

Book a 15-minute call and tell me what you're working on.