View as Markdown

Stacked Pull Requests

How the Merge Queue lands stacked pull requests: queue propagation, stack-aware batching, cascade dequeue, and partial landings.


The Merge Queue lands stacked pull requests: chains where each pull request builds on the one below it. It recognizes two kinds of stack, and once they are in the queue it treats them the same way: a comment on the top member queues the whole chain, the members keep their order and ride in the same batch when it has room for them, and pulling one out pulls out everything above it.

They differ in how the chain is created and how the queue recognizes it.

GitHub-Native Stacked Pull Requests

Section titled GitHub-Native Stacked Pull Requests

GitHub has its own stacking model. You reach it with gh-stack, its stacking extension for the gh CLI, or with mergify stack push, which registers the stack with GitHub’s stacking API unless you pass --no-github-native. Each member is a separate branch targeting the branch below it, and GitHub itself holds the ordering, so the pull request bodies carry no marker.

GitHub has no auto-merge for stacked pull requests, so without a queue each member is merged by hand as the one below it lands.

Mergify must be a bypass actor with the exempt bypass mode on every GitHub ruleset that applies to the base branch. With any other bypass mode, the Merge Queue refuses the pull request. See GitHub Rulesets Compatibility for how to set it. This requirement is specific to GitHub-native stacks.

Mergify Stacks are created with mergify stack push --no-github-native, which maps each commit on a single branch to its own pull request. There is no stack object on GitHub’s side, so the queue recognizes the chain only when all of these hold at every step:

  • The PRs are physically chained: each PR’s base branch is the previous PR’s head branch.

  • Each PR carries a Depends-On: #N marker in its body, declaring its dependency on the previous PR.

  • Every PR’s head branch lives in the repository the stack targets, not in a fork.

PRs chained only by branch refs (for example, GitFlow promotion chains like dev → staging → prod) are not treated as a stack. Without the Depends-On: marker, the queue keeps each PR’s literal base ref and queues them independently.

The last condition is why a stack cannot be opened from a fork. The first two signals compare branch names, which only mean something inside one repository: any fork can have a branch called main, so a fork PR’s head branch tells the queue nothing about where this repository’s branches point. A PR opened from a fork keeps its own base ref and is queued on its own.

main A B C PR #1 base: main PR #2 base: PR #1 PR #3 base: PR #2

Queueing a Whole Stack at Once

Section titled Queueing a Whole Stack at Once

Run @mergifyio queue on the top PR of a stack and the queue command propagates synthetically to every predecessor. The whole stack enters the queue from a single comment. You don’t need to comment on each PR.

For a stack PR1 → PR2 → PR3, commenting @mergifyio queue on PR3 enqueues PR1, PR2, and PR3 in the right order. While PR3 waits for its predecessors to join the queue, its Summary check lists one pending depends-on= condition per predecessor, each tagged [stack]. That’s the queue holding PR3 back until PR1 and PR2 are queued ahead of it.

Propagation reaches predecessors only. Commenting on PR2 enqueues PR1 and PR2 and leaves PR3 where it is, so queue the highest member you want to land.

Every stacked PR is queued against the stack root (e.g. main), not its immediate parent branch. Without this, PR2 would be queued against PR1’s head branch and could never reach main, so the queue would have nothing to merge into.

PR1 PR1 main main PR1->main PR2 PR2 PR2->main PR3 PR3 PR3->main
Every pull request in a stack is queued against main, not against its parent branch

You don’t configure this. It’s automatic for any PR detected as part of a stack.

The queue treats a stack as an ordered chain when assembling batches. Two guarantees hold:

  • Same scope group. With scopes enabled, stacked PRs are consolidated into the scope group of the bottom PR, even if their individual scopes differ. The stack always travels through the same CI lane rather than getting split across unrelated lanes.

  • Bottom-up order. Within that group, predecessors always queue ahead of successors. PR3 is never validated before PR1 and PR2.

In sequential batching, the queue actively packs a stack into the same batch when its predecessors still fit in the remaining capacity. In parallel checks, a stack longer than batch_size (or a stack sharing its scope group with higher-priority unrelated PRs) lands across consecutive batches. Order is preserved either way.

cluster_batch1 Batch 1 — the stack kept together PR1 PR1 PR2 PR2 PR1->PR2 PR3 PR3 PR2->PR3 PR4 PR4 PR3->PR4 PR5 … PR4->PR5
Merge queue with batch_size: 5 — the whole stack lands in one batch

A PR only joins a batch if it and its still-waiting predecessors fit inside the remaining capacity. When the stack is larger than batch_size, it lands across consecutive batches bottom-first: the first batch validates the deepest PRs that fit, and once they merge they drop out of the predecessor set, so the next batch picks up where the previous one stopped.

When a PR is taken out of a queued stack with @mergifyio dequeue, from the dashboard, or through the API, every successor still in the queue is dequeued with it, under the stack-predecessor-dequeued dequeue reason. This stops the queue from validating PRs whose dependency just disappeared. There’s no point checking PR3 if PR1 has left the queue.

PR1 PR1 (dequeued) PR2 PR2 (cascaded) PR1->PR2 dequeue PR3 PR3 (cascaded) PR2->PR3 dequeue PR4 PR4 PR3->PR4
Cascade dequeue: dequeuing PR1 also dequeues PR2 and PR3; PR4 stays in the queue

Once the PR you pulled out is ready again, re-queue the stack from the top with @mergifyio queue. Propagation re-enqueues the predecessors as needed.

Members land from the bottom up. A batch that holds the whole stack lands every member of it, in order, and merged members stay in a GitHub-native stack, so that stack never shrinks.

A stack can also be partly landed while the rest is still in flight: you queued only part of it, it is longer than batch_size, or a member above the ones that merged failed. Whatever the reason, the members that already merged stay merged.

The members still open are not rebased off the commits that just landed. For a GitHub-native stack, GitHub retargets the next member onto the stack’s base branch and stops there. For Mergify Stacks, GitHub retargets the next member when the merged head branch is deleted, which is what automatic head-branch deletion is for. Either way, nothing rewrites the remaining branches.

So each remaining member still carries its predecessor’s pre-landing commits. When the queue squashes or rebases, what landed on the base branch has new SHAs, so the member still shows that change in its diff and conflicts wherever the two touch the same lines. A member already in the queue when its predecessor landed can fail its merge for that reason; one queued afterwards is taken back out of the queue for conflicting with its base branch. With merge_method: merge the original commits land unchanged and the remaining members merge cleanly.

Rebase the remaining members before queueing them again:

  • Mergify Stacks: run mergify stack sync to drop the merged commits and rebase the rest, then mergify stack push.

  • GitHub-native stacks: run gh stack sync, or rebase each remaining branch on the base branch by hand.

Then comment @mergifyio queue on the top member again. Propagation puts the rest of the chain back in the queue.

  • Maximum stack depth: 20. Stacks deeper than 20 PRs aren’t recognized as a stack by the queue and fall back to per-PR queueing.

  • A draft predecessor holds the stack. Propagation still reaches it: the draft PR gets its own queue command, then waits on the same -draft condition as every queued PR. Nothing above it is validated until you mark it ready for review, at which point it joins the queue. You don’t have to comment @mergifyio queue again.

Was this page helpful?