GitLab merge trains Jump to heading

GitLab’s answer to the merge queue is the merge train. When a merge request is added to the train, GitLab creates a pipeline for the result of merging it on top of the target branch plus every merge request ahead of it in the train. If that pipeline passes, the merge request lands; if it fails, it is removed and the merge requests behind it are re-tested without it. The effect is the same guarantee a merge queue gives: the commit that lands on the target branch is exactly the commit that was tested. The mechanics differ enough from GitHub’s merge queue that teams moving between them get surprised. This page covers enabling trains, the pipeline configuration they need, what happens on failure, and tuning, within merge queues and required checks.

When to use this approach Jump to heading

  • Your repositories are on GitLab and the default branch breaks after merges that each passed CI.
  • Several merge requests land per hour, so “rebase and re-run before merge” is too slow.
  • You want the tested commit and the landed commit to be the same.
  • On GitHub, the equivalent is covered in setting up a GitHub merge queue.

Step 1 — Enable merged results pipelines, then merge trains Jump to heading

Merge trains build on merged results pipelines — pipelines that run on the merge of the merge request into its target rather than on the source branch alone. Enable both in the project’s merge request settings, and require pipelines to succeed.

glab api -X PUT "projects/$PROJECT_ID" \
  -f merge_pipelines_enabled=true \
  -f merge_trains_enabled=true \
  -f only_allow_merge_if_pipeline_succeeds=true
glab api "projects/$PROJECT_ID" | jq '{merge_pipelines_enabled, merge_trains_enabled}'
How a merge train tests each entryThree merge requests join the train. The first is tested merged onto main. The second is tested merged onto main plus the first. The third is tested on main plus the first two. Each lands only if its own cumulative pipeline passes.each pipeline tests everything ahead of it toomainMtrain car 1M+MR1train car 2M+MR1+MR2train car 3M+MR1+MR2+MR3car 3 already includes 1 and 2, so when it passes it can land directly How a merge train tests each entryThree merge requests join the train. The first is tested merged onto main. The second is tested merged onto main plus the first. The third is tested on main plus the first two. Each lands only if its own cumulative pipeline passes.each pipeline tests everything ahead of it toomainMtrain car 1M+MR1train car 2M+MR1+MR2train car 3M+MR1+MR2+MR3car 3 already includes 1 and 2, so when it passes it can land directly

Step 2 — Make the pipeline run for train events Jump to heading

Pipelines for merge trains have CI_MERGE_REQUEST_EVENT_TYPE set to merge_train. Rules that only allow merge_request_event pipelines usually include them, but rules that filter on branch names or exclude merged results can skip jobs in the train. Check that required jobs run.

workflow:
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

test:
  script: make test
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"    # includes merge_train events
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

expensive-e2e:
  script: make e2e
  rules:
    - if: $CI_MERGE_REQUEST_EVENT_TYPE == "merge_train"    # only in the train, not on every push

Running the slowest tests only in the train is a common optimisation: merge requests get fast feedback during development, and the expensive suite runs once on the exact combination that will land. The broader rule syntax is in mapping GitLab CI rules to branch and path changes.

Step 3 — Add merge requests to the train Jump to heading

Merge requests join the train with “Merge” (or “Set to auto-merge”) once their own pipeline passes. From the CLI or API:

glab mr merge 812 --auto-merge
glab api "projects/$PROJECT_ID/merge_trains/merge_requests/812" | jq '{status, pipeline: .pipeline.status}'
# What is in the train right now?
glab api "projects/$PROJECT_ID/merge_trains?scope=active" | jq -r '.[] | "\(.merge_request.iid)\t\(.status)\t\(.pipeline.status)"'

Step 4 — Understand what happens on failure Jump to heading

When a car’s pipeline fails, GitLab drops that merge request from the train and restarts the pipelines of every car behind it, now without the failed one. Cars ahead are unaffected.

A failure in the middle of the trainThree cars are running. Car two's pipeline fails. GitLab removes merge request two from the train and notifies its author. Car one is unaffected and lands. Car three's pipeline is cancelled and restarted on main plus car one only.car 1car 2car 3GitLabpipeline failedremoved, author notifiedpassed → mergedrestart: main + MR1a failing car costs the cars behind it a restart — flaky tests are expensive here A failure in the middle of the trainThree cars are running. Car two's pipeline fails. GitLab removes merge request two from the train and notifies its author. Car one is unaffected and lands. Car three's pipeline is cancelled and restarted on main plus car one only.car 1car 2car 3GitLabpipeline failedremoved, author notifiedpassed → mergedrestart: main + MR1a failing car costs the cars behind it a restart — flaky tests are expensive here

That restart is why flaky tests hurt trains badly: one flaky failure ejects a good merge request and restarts every pipeline behind it. Fix flakiness before relying on a busy train, as described in handling flaky tests in a merge queue.

Step 5 — Tune for throughput Jump to heading

Trains run pipelines in parallel for several cars at once. Throughput depends on pipeline duration and failure rate. Two settings matter: how many cars run concurrently (a GitLab instance setting on self-managed, fixed on GitLab.com), and whether fast-forward merges are used, which avoids creating merge commits.

# Pipeline duration for train pipelines, last 50
glab api "projects/$PROJECT_ID/pipelines?source=merge_request_event&per_page=50" |
  jq -r '.[] | .id' | while read -r id; do
    glab api "projects/$PROJECT_ID/pipelines/$id" | jq -r 'select(.duration) | .duration'
  done | awk '{s+=$1; n++} END {printf "avg train pipeline: %.1f min\n", s/n/60}'
Merge train against rebase-and-merge by handWithout a train, each merge request must be rebased and re-tested after every other merge, so merges serialise and authors wait. With a train, cars test in parallel against the cumulative state, land automatically when green, and only a failure causes re-testing.Rebase + merge by handMerge traintested commit = landedonly if nobody mergedalwaysparallel testingnoyesauthor effortrebase, re-run, retryclick oncecost of a failureone MRrestart cars behindthe train's weak spot is flaky tests — everything else favours it Merge train against rebase-and-merge by handWithout a train, each merge request must be rebased and re-tested after every other merge, so merges serialise and authors wait. With a train, cars test in parallel against the cumulative state, land automatically when green, and only a failure causes re-testing.Rebase + merge by handMerge traintested commit = landedonly if nobody mergedalwaysparallel testingnoyesauthor effortrebase, re-run, retryclick oncecost of a failureone MRrestart cars behindthe train's weak spot is flaky tests — everything else favours it

Validation checklist Jump to heading

Frequently Asked Questions Jump to heading

Can urgent merge requests skip the train? Jump to heading

GitLab allows a maintainer to “merge immediately”, which skips the train and restarts every running car afterwards. Reserve it for incidents; the trade-offs are in prioritising urgent changes in a merge queue.

Do merge trains work with squash merging? Jump to heading

Yes. The squash happens as part of the train’s merge, and the tested result is the squashed commit on top of the cars ahead.

What if the target branch receives a direct push? Jump to heading

Every running car’s pipeline is based on the old target, so GitLab restarts the train. Protect the target branch so that merge requests are the only way in.