View as Markdown

Run CI on a Batch

How the merge queue runs your CI on each batch, on a draft pull request by default or on the pushed queue branch alone, and how to set up your CI for each.


To check a batch, the merge queue builds a branch holding it and waits for your CI to report on that branch. It can bring the branch to your CI in two ways:

  • On a draft pull request, the default. The queue opens a draft pull request for the branch, and any CI that builds pull requests runs on it.

  • On the pushed branch alone, in Preview. The queue opens no pull request, and your CI runs on the push to the branch.

Both check the same code against the same merge_conditions. What differs is the event that starts your CI and what shows up in your repository.

A queue rule whose checks run in place builds no branch: its CI runs on the original pull request, and nothing on this page applies to it.

The queue builds the batch on top of the base branch: the pull requests ahead of the batch, then the batch’s own pull requests. It pushes that to a branch under the queue_branch_prefix of the queue rule, mergify/merge-queue/ by default. It then waits for the checks reported on the branch’s head commit and matches them, by name, against merge_conditions.

push Mergify pushes the batch to mergify/merge-queue/… draft Draft pull request (default) push->draft ci Your CI runs push->ci  push, without  a draft draft->ci  pull_request checks Checks report on the branch's head commit ci->checks read Mergify matches them against merge_conditions checks->read merged Batch merged, branch deleted read->merged
The queue pushes the batch to a queue branch. By default it opens a draft pull request for it, and your CI runs on that pull request; without a draft, your CI runs on the push itself. Either way, the queue reads the checks on the branch's head commit (teal is Mergify, grey your CI, orange the checks it waits for)

A batch gets a fresh CI run each time it is checked again. A retry recreates the draft, or pushes the batch under a new branch name when there is no draft, and each test of a split batch gets a branch of its own. When a batch merges, fails or is reset, the queue deletes its branch.

Where the two ways differ:

With a draft Without a draft
What starts your CI The draft’s pull_request event The push to the queue branch
What lists the batch’s pull requests The draft’s description A Mergify Merge Queue check run
Your pull request list and pull_request webhooks Show the draft Show nothing
When the batch ends Deleting the branch closes the draft The branch is deleted
batch_pr_number The draft’s number null
Queue rules All of them Not all
Availability Default Preview, turned on in Mergify

By default, the queue opens a draft pull request for the queue branch, the batch pull request other pages refer to, and your CI runs on it as on any other pull request. A CI that builds pull requests needs no setup for the queue, and the checks on the draft are the ones the queue reads.

The draft is a pull request like the others: it shows up in your pull request list and fires pull_request webhooks, so anything that reacts to new pull requests, such as deploy previews, runs for it. Keep Merge Queue Pull Requests Out of Your Way explains how to recognize the draft and filter it out of searches, notifications, webhooks and CI.

A job that runs only in the queue tells a queue build from any other by the draft’s head branch, github.head_ref in GitHub Actions, or by its title, which starts with merge queue: . See Two-step CI.

If your CI can run on a push, you can turn the drafts off. Nothing then shows up in your pull request list and no pull_request webhook fires for the queue, so nothing that reacts to new pull requests runs for it.

  • No pull request. Your CI learns about the batch from the push alone, so it has to run on a push to the queue branch prefix. See Set up your CI for each CI system.

  • A Mergify Merge Queue check run on the head commit of the queue branch takes the place of the draft’s description. It names the pull requests in the batch, links to the queue on the dashboard, and once the batch is done, says how its check ended. It concludes neutral, so it never fails a branch.

  • The same conditions. merge_conditions are matched against the checks reported on the head commit, by name, as they are on a draft. On most CI systems a check keeps its name whichever event started it; the recipes below name the exceptions.

  • The end of a batch deletes its branch, with no draft to close. See Cancel runs when a batch ends.

Queue rules that still need a draft

Section titled Queue rules that still need a draft

Some queue rules land a batch through its draft pull request, so they cannot do without one. A queue rule needs a draft when it uses any of:

On a base branch where drafts are off, Mergify does not queue a pull request for such a rule, and says which rule and why. A rule whose checks run in place is not affected, since it opens no draft in the first place.

Set up your CI first, merge that change to the base branch, and only then turn the drafts off. The queue branch is built on the base branch, so it carries the CI configuration the base branch has: a trigger that is still on a pull request of yours does nothing for the queue. A batch whose CI never starts gets no result: it waits until its checks timeout fails it, if one applies.

The setting is stored by Mergify, not in your configuration file. Change it in the dashboard, under Merge Queue → Configuration, on the Draft pull requests for batches card of the Checks section. It can be set at four levels, and a base branch takes it from the narrowest one that sets it:

  1. the base branch itself;
  2. a pattern of base branch names, such as release/*;
  3. the repository;
  4. the organization.

The Merge Queue API sets the same levels, each with its own /merge-queue/settings endpoint. To turn drafts off for main:

Terminal window
curl -X PUT \
-H "Authorization: Bearer <my-application-key>" \
-H "Content-Type: application/json" \
-d '{"batch_pull_request": false}' \
https://api.mergify.com/v1/repos/<owner>/<repository>/merge-queue/settings/branches/main

GET /repos/<owner>/<repository>/branches/main/merge-queue/settings returns the value main ends up with and the level it comes from.

Changing it at the repository or a branch level requires the admin role on the repository; at the organization level, it requires the organization admin role.

The queue applies the change on its next pass. A batch already being checked with a draft finishes with it; turning drafts back on checks the batches in flight again, each with a draft.

Each recipe makes your CI build a branch pushed under mergify/merge-queue/. With a custom queue_branch_prefix, use that prefix instead. Keep running the same jobs as on a queue pull request: the batch merges only once the checks in your merge_conditions pass on its head commit.

Add a push trigger on the queue branch prefix next to pull_request:

on:
pull_request:
push:
branches:
- "mergify/merge-queue/**"

Mergify pushes with its GitHub App token, and a push made with an app token starts workflows. The job names do not change, so neither do the check names your merge_conditions list.

While drafts are still on, a queue branch then runs the workflow twice, once for the push and once for its draft. Add the trigger right before you turn the drafts off, or skip the push run on queue branches until then.

OAuth app integration. With Only build pull requests on, CircleCI builds a branch only once a pull request is open for it, so it never builds a queue branch. Turn the setting off, or keep it and list the queue branches as an override through CircleCI’s API. The list holds regular expressions and replaces the current one, so include your default branch:

Terminal window
curl -X PUT \
-H "Circle-Token: <your-circleci-token>" \
-H "Content-Type: application/json" \
-d '{"feature_flags": {"pr-only-branch-overrides": "main, mergify/merge-queue/.*"}}' \
https://circleci.com/api/v1.1/project/github/<owner>/<repository>/settings

GitHub App integration. A pipeline whose only trigger is PR opened or pushed to, default branch and tag pushes never runs for a queue branch, and neither does Pushes to merge queues, which matches GitHub’s own merge queue branches. Add an All pushes trigger in the project settings, then keep the other pushes out with a when condition on the workflow:

workflows:
merge-queue:
when:
matches:
pattern: "^mergify/merge-queue/.*"
value: << pipeline.git.branch >>
jobs:
- test

A pipeline with Build branches on in its GitHub settings already builds the queue branch when it is pushed. If you build pull requests only, turn Build branches on, and use the branch filter to limit it to your default branch and the queue branches:

main mergify/merge-queue/*

The commit status keeps its name, so merge_conditions do not change.

With a multibranch pipeline on the GitHub Branch Source plugin, the queue branch is discovered as a branch once it has no pull request. The default Discover branches strategy, Exclude branches that are also filed as PRs, already builds it, and so does All branches. Only branches that are also filed as PRs never does: change it to one of the other two. A branch name filter, if you set one, has to include mergify/merge-queue/*.

A branch build reports continuous-integration/jenkins/branch, while a pull request build reports continuous-integration/jenkins/pr-merge or continuous-integration/jenkins/pr-head. List the branch one in your merge_conditions:

queue_rules:
- name: default
merge_conditions:
- check-success = continuous-integration/jenkins/branch

A when { branch 'mergify/merge-queue/*' } block matches the queue branch on a branch build, where BRANCH_NAME is the branch name itself.

With CI/CD for external repositories, GitLab mirrors the queue branch and runs a branch pipeline for it, where it used to run an external pull request pipeline as well. If your workflow:rules admit only external_pull_request_event pipelines, admit the queue branches too:

workflow:
rules:
- if: $CI_PIPELINE_SOURCE == "external_pull_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
- if: $CI_COMMIT_BRANCH =~ /^mergify\/merge-queue\//

A YAML pipeline with no trigger key builds every pushed branch, the queue branches included. With trigger: none, or a branch list, add the queue branches to it:

trigger:
branches:
include:
- main
- mergify/merge-queue/*

The Pull Requests build feature builds pull requests, not pushed branches. Add the queue branches to the branch specification of the VCS root, so the VCS trigger builds them, and keep the Commit status publisher build feature, which reports the result to GitHub:

+:refs/heads/main
+:refs/heads/mergify/merge-queue/*

Turn on Build pushed branches in the repository settings, and if your .travis.yml has a branches safelist, add the queue branches to it:

branches:
only:
- main
- /^mergify\/merge-queue\/.*$/

Travis CI reports a push build as Travis CI - Branch and a pull request build as Travis CI - Pull Request. List the branch one in your merge_conditions.

Adapt jobs that read the pull request

Section titled Adapt jobs that read the pull request

A push build has no pull request, so anything a job reads from it is empty. That matters for the conditions that tell a queue build from any other.

In GitHub Actions, github.head_ref and github.event.pull_request are empty on a push, and github.ref_name holds the branch name. A condition that covers both events reads whichever is set:

jobs:
pre-merge-tests:
if: startsWith(github.head_ref || github.ref_name, 'mergify/merge-queue/')

The same goes for the Two-step CI jobs that run only in the queue, and for a step gated on the draft’s title, such as startsWith(github.event.pull_request.title, 'merge queue:'): test the branch name instead. On other CI systems, test the variable that holds the pushed branch:

CI system Branch variable
CircleCI pipeline.git.branch, or CIRCLE_BRANCH in a job
Buildkite BUILDKITE_BRANCH
Jenkins BRANCH_NAME
GitLab CI CI_COMMIT_BRANCH
Azure Pipelines Build.SourceBranch, which starts with refs/heads/
TeamCity teamcity.build.branch
Travis CI TRAVIS_BRANCH

What the queue wrote in the draft’s description is in a Git note on the batch’s head commit, which mergify ci queue-info reads on a push build too. Without a draft, batch_pr_number is null in previous_failed_batches and previous_check_retries, and batch_pr_head_sha still names the commit each earlier run checked, which is what the recipes on that page use. mergify ci scopes and mergify ci git-refs read the note on a push build too, as long as your Mergify CLI is up to date.

When a batch merges, fails or is reset, Mergify deletes its queue branch. With a draft, a workflow on pull_request closed could cancel the runs still going; without one, the branch deletion is the event to react to. In GitHub Actions, cancel the runs of a deleted queue branch on delete:

name: Cancel queue branch runs
on:
delete:
permissions:
actions: write
jobs:
cancel:
if: github.event.ref_type == 'branch' && startsWith(github.event.ref, 'mergify/merge-queue/')
runs-on: ubuntu-latest
steps:
- name: Cancel the runs of the deleted branch
env:
GH_TOKEN: ${{ github.token }}
BRANCH: ${{ github.event.ref }}
run: |
for status in pending waiting queued in_progress; do
gh run list --repo "$GITHUB_REPOSITORY" --branch "$BRANCH" \
--status "$status" --json databaseId --jq '.[].databaseId' |
xargs -r -n1 gh run cancel --repo "$GITHUB_REPOSITORY"
done

GitHub runs a delete workflow only from the default branch, so merge it there. A retry pushes the batch under a new branch name and deletes the old one, so this also stops the runs of the attempt it replaces.

Was this page helpful?