GitLab Merge Requests for Infra Changes

Creating MRs, reviewing infra diffs, approval rules, responding to comments, and merging safely.

Why MRs matter for infrastructure

For infrastructure code, the MR is not just a review nicety — it is a safety control. A bad Ansible change can take down a service or misconfigure dozens of machines. The MR gives:

Creating an MR

After pushing your branch:

  1. Go to the project in GitLab
  2. Click Merge Requests → New merge request
  3. Select your source branch and main as the target
  4. Fill in the title and description
  5. Assign a reviewer
  6. Click Create merge request

GitLab often shows a banner on the project page when you push: "Create merge request for branch/name" — click that for a shortcut.

# After pushing, GitLab shows a direct link in the terminal output:
git push -u origin feature/add-ntp-server
# To create a merge request for feature/add-ntp-server, visit:
#   https://gitlab.example.com/infra/repo/-/merge_requests/new?merge_request[source_branch]=feature/add-ntp-server

Writing a useful MR description

A good MR description for an infrastructure change answers:

## What
Add internal NTP server to chrony configuration for all production hosts.

## Why
Ticket INF-2431 — external NTP access is being blocked by firewall policy.

## Affected hosts
All hosts in inventories/production/ (via group_vars/all.yml)

## Testing
Ran `ansible-playbook site.yml --check --diff -i inventories/production/hosts.ini`
Output shows only chrony.conf changing — diff attached to ticket.

## Rollback
Revert this MR commit. chrony will sync from external servers again on next run.

Reviewing an infra diff

When reviewing someone else's Ansible MR, look at:

Check which files changed

Use the Changes tab in the MR. The file list tells you the scope of the change before you read any code.

YAML / variable file changes

Task file changes

Template changes

Checking pipeline status in an MR

The pipeline status is shown in the MR view — a green tick (passed), orange circle (running), or red X (failed).

Click the pipeline status to see individual job results. Click a failed job to read its log.

If the pipeline is failing on a lint rule you disagree with:

# Skip a specific rule on a task
- name: Run legacy script that we cannot replace yet
  ansible.builtin.shell: /opt/legacy/setup.sh
  # noqa: command-instead-of-shell

If the pipeline passes but you are not confident in the dry-run result, ask the author to paste the --diff output in the MR description.

Approval rules

Protected branches can require approvals before merge. Common setups:

If your MR is blocked on approvals, the MR page shows "Approvals: 0/1". Ask the designated reviewer to approve. You cannot approve your own MR.

Responding to review comments

When a reviewer leaves a comment requesting a change:

  1. Make the change in your local branch
  2. Add and commit: git add . && git commit -m "Address review: fix variable name"
  3. Push: git push — the MR updates automatically
  4. Reply to the comment in GitLab explaining what you changed, or resolve it if the reviewer left instructions you followed exactly
Do not force-push during review. It makes it hard for the reviewer to see what changed since their last look. Add commits instead — you can squash them before or after merge.

Squash commits

If you made many small commits during development ("fix typo", "another fix", "address review"), you can squash them into one clean commit at merge time.

Enable Squash commits in the merge dialog. GitLab combines all commits in your branch into one commit on main. The commit message is the MR title by default — edit it to something descriptive.

A squashed commit makes git log on main readable: one line per feature/fix, not a trail of work-in-progress commits.

Merging — what happens next

When all of these are satisfied:

Click Merge. GitLab merges your branch into main and (if configured) triggers a new pipeline on main — which may automatically deploy.

If a deploy pipeline runs after merge, watch it. If it fails, you need to act quickly — main now has code that does not deploy cleanly.

When an MR is stuck

# Bring your branch up to date with main
git fetch origin
git rebase origin/main

# If conflicts during rebase
git status             # shows conflicting files
# edit files, resolve markers
git add resolved-file.yml
git rebase --continue

# Push the rebased branch (requires force)
git push --force-with-lease

--force-with-lease is safer than --force — it fails if someone else has pushed to your branch since you last pulled, preventing accidental overwrite.

Create MR from git push

You can open a GitLab MR directly from the git push command using push options (-o). GitLab prints the MR URL in the push output.

# Push branch and create an MR targeting main
git push -o merge_request.create origin feature/add-firewalld-rules

# Set a specific target branch
git push \
  -o merge_request.create \
  -o merge_request.target=main \
  origin feature/add-firewalld-rules

# Set MR title at push time
git push \
  -o merge_request.create \
  -o merge_request.target=main \
  -o merge_request.title="Add firewalld rules for web tier" \
  origin feature/add-firewalld-rules

# Assign to a reviewer immediately
git push \
  -o merge_request.create \
  -o merge_request.assign=alice \
  origin feature/add-firewalld-rules

GitLab outputs something like: remote: View merge request for feature/add-firewalld-rules: https://gitlab.internal/infra/config/-/merge_requests/42 — you can share this URL immediately without opening a browser.

Draft MRs

A Draft MR signals that the work is in progress and must not be merged yet. GitLab blocks the merge button until the Draft status is removed.

Create a Draft MR

# Method 1: Add "Draft:" prefix to the MR title in the UI

# Method 2: Create as Draft via push option
git push \
  -o merge_request.create \
  -o merge_request.title="Draft: Refactor nginx upstream config" \
  origin feature/nginx-upstream-refactor

When to use Draft MRs

Removing Draft status

In the GitLab UI, click Mark as ready at the top of the MR page. This removes the Draft prefix and enables the merge button (subject to any approval rules).

Best practice: Always open MRs as Draft when your branch is freshly pushed. This prevents a teammate from merging before you've had a chance to review the pipeline results and add a proper description.

CODEOWNERS file

A CODEOWNERS file in your repo root (or .gitlab/) automatically assigns reviewers based on which files are changed in an MR. This ensures the right team is always looped in for critical paths.

File location

# One of these locations (GitLab checks in order):
CODEOWNERS
docs/CODEOWNERS
.gitlab/CODEOWNERS

Syntax

# Pattern                                   Owner(s)
# ──────────────────────────────────────────────────────────────
# All files (catch-all)
*                                            @team-infra

# Specific directory — all files inside
/ansible/roles/                              @team-infra

# Specific file type in any directory
*.tf                                         @team-terraform

# Files in a specific directory by extension
/ansible/group_vars/*.yml                    @team-infra @team-security

# A specific file
/ansible/inventories/production/hosts.ini   @team-infra-leads

# Group syntax (configure groups in GitLab Admin → Groups)
/deploy/                                     @infra/senior-ops

How CODEOWNERS interacts with approval rules

In GitLab, enable Settings → Merge requests → Code Owners → Require approval from code owners to make CODEOWNERS approval mandatory before merge. Without this setting, CODEOWNERS only adds reviewers automatically without blocking the merge.

Merge trains

A merge train batches queued MRs and runs their pipelines in sequence against the would-be state of the target branch. Each train job's pipeline runs against main + earlier-trains-merged + this-MR, so you can't merge an MR whose tests pass in isolation but would break main once another simultaneously-merging MR lands first.

When to enable

Enable under Settings → Merge requests → Merge method → Merge trains. Once on, clicking Merge adds the MR to the tail of the train instead of merging immediately.

The ABA problem

Merge trains solve the combined-state problem, but they introduce a subtler one: a passing MR at position 2 can be invalidated after it enters the train. If position 1 fails and is removed from the train, every MR behind it re-pipelines against a new base — tests that just passed are thrown away and re-run, and an MR that was green can now fail.

The "ABA" case is worse: MR A lands, MR B lands on top of A, then A is reverted — B's code now runs against a base where A was never present. If B's tests only passed because of a helper A added, B is now silently incorrect on main. Merge trains don't re-check already-merged MRs against post-revert state; you need a post-merge pipeline to catch it.

Turn merge trains on when race-condition breakages actually happen to you. If main is green nearly every time, the CI cost isn't worth it.

Release-branch cherry-pick

A hotfix needs to ship to production off a release branch (release/2024.11) while the fix also needs to go into main. GitLab's one-click cherry-pick keeps both branches honest.

  1. Land the fix on main first. Open MR → review → merge. This is the source of truth; every release branch gets the fix cherry-picked from main.
  2. Open the merged MR and scroll to the merge commit. Click Cherry-pick.
  3. Pick the release branch as the target (release/2024.11). Check Start a new merge request with these changes so the cherry-pick goes through the same review + CI as any other change.
  4. Review + merge the cherry-pick MR against the release branch. Tag a new patch release from that branch.
# Equivalent CLI workflow
git fetch origin
git switch release/2024.11
git cherry-pick -x <merge-commit-sha-from-main>
# -x appends "(cherry picked from commit ...)" to the message — keeps provenance
git push -u origin hotfix/INF-2431-release-picks
# then open an MR targeting release/2024.11

Always cherry-pick from main, never the reverse. If you patch the release branch directly and back-port to main, main's history diverges and the next release branch cut off main may silently re-ship the "fix" on top of itself. Fixing forward from main keeps release branches strict subsets of history.

Cherry-pick conflicts are a signal. If a cherry-pick doesn't apply cleanly, either the release branch has diverged more than expected (review for inadvertent changes) or the fix depends on something only on main. Don't force-edit conflicts in the cherry-pick commit — re-implement the minimal fix on the release branch and reference the main commit in the MR description.