GitLab Merge Trains actually prevent you from merging multiple PRs in order; they’re designed to merge one PR at a time, but after it’s been validated against the latest main (or whatever your target branch is). The "order" part is handled by the fact that they queue up.

Let’s see this in action. Imagine we have a few merge requests (MRs) open against our main branch.

  • MR 1: feat: Add user profile page (depends on nothing)
  • MR 2: fix: Correct login bug (depends on MR 1)
  • MR 3: chore: Update dependencies (depends on nothing)

If we enable Merge Trains on our project and try to merge MR 1, MR 2, and MR 3 simultaneously, GitLab won’t just merge them all. Instead, it will:

  1. Pick MR 1: It adds MR 1 to the merge train. GitLab builds and tests MR 1 on top of the latest main branch. If it passes, MR 1 is merged into main.
  2. Pick MR 3: Now that MR 1 is in main, GitLab picks MR 3. It builds and tests MR 3 on top of the latest main (which now includes MR 1). If it passes, MR 3 is merged into main.
  3. Pick MR 2: Finally, GitLab picks MR 2. It builds and tests MR 2 on top of the latest main (which now includes MR 1 and MR 3). If it passes, MR 2 is merged into main.

Notice how MR 2, which depended on MR 1, was only considered after MR 1 was successfully merged. This is the core of the merge train: ensuring that each individual merge request is tested against the most up-to-date version of the target branch before it’s merged. This prevents the dreaded "it worked on my machine" scenario where a PR passes tests locally but breaks the main branch because it was based on an older version of the code.

The problem Merge Trains solve is integration hell. When multiple developers are working on separate features or fixes, and they all try to merge into a shared main branch, conflicts and regressions become rampant. Even if CI passes for each individual MR against its base branch at that moment, the merge itself can introduce new problems. By creating a queue and re-testing each MR against the newly updated target branch, merge trains guarantee that the target branch is always in a working state after each successful merge.

Here’s how it works internally:

  1. Enabling Merge Trains: You enable this feature in your project’s Settings > General > Merge request approvals. You’ll see a checkbox for "Enable merge trains."
  2. Adding to the Train: When an MR is ready to merge and has all approvals, a user (or a bot) clicks the "Add to merge train" button instead of "Merge."
  3. CI/CD for the Train: GitLab’s CI/CD system then takes over. It creates a temporary branch that’s a merge of the latest main and the MR’s branch. It runs all configured CI jobs on this temporary branch.
  4. Merging: If all CI jobs pass, the MR is automatically merged into main. If any job fails, the MR is removed from the train, and the author is notified to fix the issues.
  5. Queuing: If the train is busy (meaning another MR is currently being built and tested), the new MR is simply added to the end of the queue. The "order" is determined by when each MR successfully passes its CI checks against the evolving main branch.

The actual levers you control are primarily within your .gitlab-ci.yml file. You need to ensure your CI jobs are configured to run correctly on these temporary merge branches. For example, a typical job might look like this:

rspec_job:
  stage: test
  script:
    - bundle install
    - bundle exec rspec
  rules:
    - if: '$CI_MERGE_REQUEST_IID' # Run only for merge requests

The key is that the rules often implicitly handle the merge request context. When GitLab sets up the merge train job, it provides the necessary environment variables so your scripts run against the correct merged code.

The one thing most people don’t realize is that merge trains don’t strictly enforce a developer-defined order of merging if multiple MRs are ready simultaneously. Instead, they enforce an order based on successful CI validation against the evolving target branch. An MR that declares itself as dependent on another might be added to the train earlier, but it won’t merge until its dependency has successfully merged and its own CI checks pass against the updated main. This can sometimes lead to a surprising sequence of merges if an earlier MR in the queue has a flaky test that takes time to resolve, allowing a later, simpler MR to pass and merge first.

The next conceptual hurdle is understanding how to manage complex dependency graphs and ensure that a merge train doesn’t get stalled by a single, long-running, or unstable MR.

Want structured learning?

Take the full Gitlab course →