GitHub’s Merge Queue is a system designed to prevent the dreaded "merge hell" that plagues large projects with many contributors. It acts as a gatekeeper, ensuring that only well-tested and conflict-free code gets merged into your main branches.
Here’s how it works in practice:
Imagine a repository with a main branch that needs to stay stable. Developers create feature branches, make their changes, and open Pull Requests (PRs). Without a merge queue, multiple developers might try to merge their PRs simultaneously. This can lead to:
- Merge Conflicts: If two PRs modify the same lines of code, they can’t be merged automatically. Someone has to manually resolve the conflict, which takes time and can introduce errors.
- Broken Main Branch: A PR might pass its own tests but, when merged with other concurrently merged PRs, introduce a bug that breaks the main branch. This requires an emergency rollback or hotfix.
- Slow CI/CD: The CI/CD pipeline might get overloaded with tests for every single PR independently, and then again for the main branch after merges.
The Merge Queue tackles this by creating a single, ordered line of PRs waiting to be merged. When a PR is ready to merge (all checks pass, approved, etc.), it’s added to the queue. The system then:
- Tests the PR against the latest state of the main branch. This is crucial. It’s not just testing against the base branch it was opened against, but against what
mainwould look like if all the PRs ahead of it in the queue were already merged. - If it passes, it’s merged.
- If it fails, it’s rejected from the queue. The developer is notified and must rebase their branch onto the updated
mainand fix the failing tests before re-submitting.
Let’s set it up. You’ll need GitHub Enterprise Cloud or GitHub Enterprise Server.
First, navigate to your repository’s Settings.
Then, go to Code and automation -> Pull requests.
You’ll see a section for Merge queue. Click Set up merge queue.
GitHub provides a default configuration, but you can customize it. The core components are:
- Protected branches: You need to have branch protection rules set up for the branch you want to queue merges for (e.g.,
main). This ensures that only PRs that pass the queue can actually be merged into it. - Required status checks: These are the automated checks (CI tests, linting, security scans) that must pass for a PR to be considered mergeable before it even enters the queue.
- Merge group checks: These are checks that run specifically for the merge queue. This is where you can define checks that verify the PR against the entire proposed merge group.
Here’s a sample configuration you might add to your .github/merge_queue.yml file:
# .github/merge_queue.yml
# This configuration ensures that PRs are tested against the latest main
# and that a group of PRs can be merged together without conflicts.
# Define the base branch that the merge queue will protect.
# This must be a branch that has branch protection enabled.
base_branch: main
# List of required status checks that must pass for a PR to be considered
# for the merge queue. These are the checks that run on individual PRs.
required_checks:
- name: ci/circleci:build
- name: ci/circleci:test
- name: lint/eslint
# List of merge group checks. These checks run on the *combined* code
# of the PR and all preceding PRs in the queue against the base branch.
# This is the core of preventing merge conflicts and regressions.
merge_group_checks:
- name: ci/circleci:build-merge-group
- name: ci/circleci:test-merge-group
- name: security/snyk
# If true, the merge queue will require all required_checks to pass
# before a PR can be added to the queue. If false, PRs can enter the queue
# even if some required_checks are pending or failed, but they will be
# blocked from merging until all checks pass.
queue_checks_must_pass: true
# If true, the merge queue will automatically rebase PRs that are
# ahead of a failing PR. This helps to keep the queue moving.
automerge_with_rebase: true
This configuration tells GitHub:
- Protect the
mainbranch. - Before a PR can even think about entering the queue,
ci/circleci:build,ci/circleci:test, andlint/eslintmust all pass. - Once in the queue, GitHub will bundle PRs together. For each bundle, it will run
ci/circleci:build-merge-group,ci/circleci:test-merge-group, andsecurity/snyk. These checks are designed to run on the combined code of all PRs in the bundle against the latestmain. queue_checks_must_pass: truemeans a PR can’t even join the queue if its individual checks aren’t green.automerge_with_rebase: trueis a powerful option: if a PR fails its merge group checks, GitHub will attempt to rebase the PRs ahead of it in the queue onto the latestmain. This can automatically resolve many transient merge conflicts and keep the queue from getting stuck.
The most surprising true thing about the Merge Queue is that it doesn’t just test your PR in isolation; it tests it as part of a potential future state of your main branch. This is a fundamental shift from traditional CI, where you test against the current state.
Consider this scenario: PR A and PR B are both ready to merge. PR A modifies line 10 of file.txt to "Hello". PR B modifies line 10 of file.txt to "World". Individually, both might pass their tests. If they were merged sequentially without a queue, the second merge would overwrite the first, and the final state of file.txt would only have "World", losing "Hello".
With the Merge Queue, when PR B is ready, the queue might look like [PR A, PR B]. The merge group check would test the combined effect of PR A and PR B against main. It would detect that both PRs try to modify line 10. If this causes a conflict or a test failure in the merge group check, PR B would be rejected, and the developer would be prompted to resolve the conflict by rebasing. This prevents the "last writer wins" scenario and ensures that only code that integrates cleanly with all preceding queued changes gets merged.
You’ll notice that the merge group checks often have similar names to your regular required checks (e.g., build vs. build-merge-group). This is common. The distinction is that the merge group checks are executed on a simulated merge of the PR with all preceding PRs in the queue against the latest base_branch.
After successfully configuring and using the Merge Queue, the next hurdle is often managing the visibility and throughput of the queue itself.