Skip to content

Release management at scale: merge queues and release trains

Test each change on top of everything queued ahead of it before it lands; then ship what landed, versioned from the changes' own notes.

Predict first

Before you read: predict

ExamHall's monorepo merges through a GitLab merge train, and every train pipeline runs the same node --test as each MR's own pipeline. Three MRs were cut from the same commit of main, and each one's own merged-results pipeline passed. rename-deadline renames deadlineAt to dueAt in packages/contracts/exam.mjs and updates its one caller. countdown-banner adds a proctor banner that imports deadlineAt from the contracts package. grading-csv adds a CSV header that imports examTitle. At 10:00 they join the train in this order: rename-deadline, countdown-banner, grading-csv.

When the train is empty, which of the three are on main?

Pick one

The 30-second version

An MR's own pipeline never sees the MRs landing beside it. A merge queue builds each entry on main plus every entry ahead, all at once, and lands them in order; a failing entry leaves and the builds behind it restart. What lands then ships continuously or on a train, versioned from changesets.

Test each change on top of everything queued ahead of it before it lands; then ship what landed, versioned from the changes' own notes.

How it works

Step 1 of 14

Green alone is not green together. An MR's pipeline tests it merged with main as it was; it never sees the MRs landing beside it.
In the kitchen
  • each member's module, checked at home against the layout from the last club night → an MR green against main as it was (step 1)

Analogy: A toy-brick club's modular city layout, taken to shows

At the club In the repository
each member's module, checked at home against the layout from the last club night an MR green against main as it was (step 1)
on club night, each new module clicked in beside every module accepted ahead of it before it joins a queue build: main, the entries ahead, the entry (step 2)
several members test-fitting at once, each on a copy of the layout that holds the modules ahead builds overlap (step 3)
a module that will not line up is lifted out, and the modules fitted after it are fitted again without it a failed build removes its entry (step 5)
the van leaves for each show at a fixed hour with the layout as it stood at Friday's cut-off a release train (step 11)
each module's card ("a tram stop: small change"; "a wider street: big change"), put together into the show guide changesets into versions and changelogs (steps 12–13)

In real terms: before an MR lands, test it on top of everything queued ahead of it; when it lands, it ships on the next deploy or the next train, and its changeset says how the version moves.

Breaks down when: a member sees a misfit by eye, while a clash in code shows only when the build runs on the combination.

Contrasting case: one member's solo display, which nobody else's module ever joins: one stream of changes needs no queue.

Examples, run for real

4.1 ExamHall EXH-4340: main broke twice in a week, replayed through a merge train

EXH-4340: every MR in ExamHall's monorepo passed its own pipeline, yet main went red twice in one week. The replay takes that week's eight MRs, each cut from the same commit and green on its own, and merges them in turn; the team reverts a red merge at once. MR !4640 turns on GitLab merge trains with the same node --test, and the same eight MRs join the train in the same order.

4.1 EXH-4340 without a train: 8 MRs merged in turn, each green on its own · main red after 2 merges (grade-export, alert-feed), each reverted
4.1 MR !4640, the same 8 through the train: 6 merged · grade-export and alert-feed left the train, each red on top of the MRs ahead · node --test on main green after every merge · 14 pipelines started

Neither red merge had a conflict. grade-export still calls scoreOf, which an MR merged before it renamed, and alert-feed expects the old candidate label (step 1). On the train each was tested on top of the MRs ahead, failed there and left; the pipelines behind it ran again without it (steps 2, 5).

4.2 The release MR: changesets into versions

That week two of the MRs also carried changesets: "@examhall/contracts": major with the score rename, and "@examhall/proctor-app": minor with the room map. ExamHall's release MR runs changeset version. For contrast, the same two MRs are replayed with each writing its own line under ## Unreleased in one shared CHANGELOG.md.

4.2 changeset version: 2 changesets consumed · @examhall/contracts 2.4.0 -> 3.0.0 · @examhall/proctor-app 1.4.2 -> 1.5.0, its range on contracts ^2.4.0 -> ^3.0.0 · changesets left 0
4.2 @examhall/contracts CHANGELOG.md: ## 3.0.0 · ### Major Changes · - scoreOf is now pointsOf
4.2 the same two MRs, each writing its line under "## Unreleased" in one CHANGELOG.md: points-not-score's train pipeline merges cleanly · room-map's stops on a conflict in CHANGELOG.md

Changeset files never touch one shared line, so they never meet on the train; the release MR turns them into one version per package and its changelog lines (steps 12–13).

Try it

Answer first, then check. Nothing is saved.

Question 1 of 7

A payments-ledger repository on GitHub requires a merge queue with build concurrency 2. Each queue build runs node --test and takes 10 minutes. Four pull requests were cut from the same commit of main, and each passes node --test merged with main alone. At 09:00 they join the queue in this order: currency-required (makes postEntry throw unless an entry has a currency, and adds one to its existing caller), fee-report (a new fees module), refund-posting (a new refund() that calls postEntry with no currency, and its test), ledger-csv-export (a new CSV module).

At what time does ledger-csv-export reach main?

Pick one

Question 2 of 7

A payments company ships its ledger web dashboard continuously: every change that lands on main is deployed and reaches users 45 minutes after it lands. Its mobile app ships on a weekly train: every Monday at 12:00 a release branch is cut from main, and that branch reaches users through the app stores on the Wednesday at 15:00. Four changes land on main:

lands on main      change           part
Mon 19 Oct 10:00   refund-status    mobile app
Mon 19 Oct 11:30   ledger-export    web dashboard
Mon 19 Oct 14:00   payout-alert     mobile app
Thu 22 Oct 11:00   fee-report-fix   web dashboard

List two that reach users together in the order they landed.

In what order do users first get them?

Pick one

Question 3 of 7

A ledger repository has five approved pull requests, each green on today's main and none behind it, at 14:00. Required checks take 12 minutes. Setup X: branch protection "Require branches to be up to date before merging", no queue; whenever a pull request falls behind main, its author merges main in at once and the checks run again, and a pull request merges the moment it is green and up to date. Setup Y: a merge queue with build concurrency 3 instead; all five join it at 14:00.

At what time is the last of the five on main, in each setup?

Pick one

Question 4 of 7

A payments-ledger monorepo releases with Changesets (@changesets/cli 3.0.3, "updateInternalDependencies": "patch"). @ledger/contracts is at 2.3.1; @ledger/posting depends on it, and @ledger/reports on @ledger/posting:

{ "name": "@ledger/posting", "version": "1.8.0", "dependencies": { "@ledger/contracts": "^2.3.1" } }
{ "name": "@ledger/reports", "version": "2.4.0", "dependencies": { "@ledger/posting": "^1.8.0" } }

Since the last release, one pull request landed, with this changeset:

---
'@ledger/contracts': major
---

Rename amount to amountMinor

After changeset version, what are the three versions?

Pick one

Question 5 of 7

In a mock interview you said the payments-ledger repository should use a GitHub merge queue. The interviewer follows up: "Your end-to-end suite takes 40 minutes and fails now and then for no reason in the code. The platform team requires that each change has passed the end-to-end suite on a queue build holding it before it reaches main, and that no correct pull request is thrown out of the queue. Five correct pull requests join at 09:00 with build concurrency 5, and the end-to-end suite fails by chance once, on the queue build that ends with the second of them. Your fast suite (lint, types, unit tests) takes 8 minutes and never fails by chance. What do you set up? Three candidates answered:

Whose answer survives the follow-up?

Pick one

Question 6 of 7

A chat app's monorepo releases with Changesets (@changesets/cli 3.0.3, "updateInternalDependencies": "patch"). @chat/protocol is at 4.2.0, and three apps depend on it: @chat/web 1.12.3 with "@chat/protocol": "^4.2.0", @chat/mobile 2.0.5 with "@chat/protocol": "~4.2.0", and @chat/desktop 3.1.4 with "@chat/protocol": "4.2.0". One pull request landed since the last release, with this changeset:

---
'@chat/protocol': minor
---

Add message reactions

After changeset version, what are the four versions?

Pick one

Question 7 of 7

A video platform's repository merges through a GitLab merge train. Every pipeline takes 25 minutes and passes, including the one main runs after each merge. Four merge requests join the train at 09:00, in order, so four train pipelines start at once. At 09:15 a maintainer uses "Merge immediately" on a hotfix for broken subtitles.

At what time does the fourth merge request reach main?

Pick one

Go deeper

Pro tips
  1. Make every required workflow run for the queue, not only for pull requests. In GitHub Actions, add the merge_group trigger beside pull_request:
    on:
      pull_request:
      merge_group:
    When: turning on a merge queue for a branch whose required checks run in GitHub Actions (step 2). Without it the required check is never reported for the queue's temporary branch and the entry cannot merge. Verify: "Otherwise, status checks will not be triggered when you add a pull request to a merge queue. The merge will fail as the required status check will not be reported." Status: CITED, not run (GitHub Actions cannot run here).
  2. Read the release plan before you version. changeset status --verbose lists each package that will be bumped, the bump and the changesets behind it, and changes no file. When: reviewing a release MR before running changeset version (step 13).
tip 2 changeset status --verbose -> exit 0 · major · @examhall/contracts -> 3.0.0 · .changeset/points-not-score.md · minor · @examhall/proctor-app -> 1.5.0 · .changeset/room-map.md · files changed: none
Source
  • GitHub Docs, "Managing a merge queue" (the row's first source): https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/configuring-pull-request-merges/managing-a-merge-queue · fetched 2026-10-15 with curl. "the changes in the pull request are grouped into a merge_group with the latest version of the base_branch as well as changes from pull requests ahead of it in the queue" (step 2); "Build concurrency: The maximum number of merge_group webhooks to dispatch (between 1 and 100), throttling the total amount of concurrent CI builds." (step 6); "The merge queue provides the same benefits as the Require branches to be up to date before merging branch protection, but does not require a pull request author to update their pull request branch and wait for status checks to finish before trying to merge." (step 7); "jumping to the top of a merge queue will cause a full rebuild of all in-progress pull requests" (step 8); "Only merge non-failing pull requests: This setting determines how a merge queue forms groups of pull requests to be merged." (step 10); the merge_group trigger (tip 1). Valid as of 2026-10-15.
  • GitHub Docs, "Merging a pull request with a merge queue": https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/merging-a-pull-request-with-a-merge-queue · fetched 2026-10-15. "Configured CI service is reporting test failures for a merge group" as a reason for removal (step 5). Valid as of 2026-10-15.
  • GitLab Docs, "Merge trains" (the row's second source): https://docs.gitlab.com/ci/pipelines/merge_trains/ · fetched 2026-10-15. "Two merge requests can each pass their own pipeline, but their combined changes can still conflict." (step 1); "If a merge train pipeline fails, the merge request is not merged. GitLab removes that merge request from the merge train, and starts new pipelines for all the merge requests that were queued after it." (step 5); "Each merge request merges into the target branch only after: The merge request's pipeline completes successfully. All other merge requests queued before it are merged." (step 4); "The merge train pipelines for all other merge requests on the train are canceled." on Merge immediately (step 8). Valid as of 2026-10-15.
  • Changesets (the row's third source), github.com/changesets/changesets, from the main branch (the v3 line; @changesets/cli 3.0.3 is the npm release run here), fetched 2026-10-15 from raw GitHub: README.md, docs/detailed-explanation.md: "A changeset is a Markdown file with YAML front matter." and "Versioning - combines all changesets, creates one version bump for each package based on the maximum version bump of each package, updates dependencies where needed, and writes changelogs." (steps 12–13); docs/decisions.md: "we flatten the version bumps into one single bump at the highest semver range specified" and "All updating of dependencies is done as a patch bump." (steps 13–14); docs/config-file-options.md: "Changesets will always update the dependency if it would leave the old semver range." (step 14); docs/intro-to-using-changesets.md. The docs files carry the banner "This documentation is outdated. View the up-to-date version at https://changesets.dev"; every Changesets fact here is also executed on 3.0.3. Valid as of 2026-10-15.

Checked against the tools and docs as of 2026-10-15.