---
title: Run CI on a Batch
description: 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](/merge-queue/batches), 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](/merge-queue/batches/draft-pull-requests#when-the-queue-checks-in-place) builds
no branch: its CI runs on the original pull request, and nothing on this page
applies to it.

## How the queue checks a batch

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`](/configuration/file-format/#queue-rules) 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`.

```dot class="flow" title="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)"
digraph {
  push [label="Mergify pushes\nthe batch to\nmergify/merge-queue/…", class="mergify"];
  draft [label="Draft pull\nrequest\n(default)", class="mergify"];
  ci [label="Your CI runs", class="external"];
  checks [label="Checks report on\nthe branch's\nhead commit", class="pending"];
  read [label="Mergify matches them\nagainst merge_conditions", class="mergify"];
  merged [label="Batch merged,\nbranch deleted", class="merged"];

  push -> draft;
  draft -> ci [label="  pull_request"];
  push -> ci [label="  push, without\n  a draft", style=dashed];
  ci -> checks;
  checks -> read;
  read -> merged [class="merged"];
}
```

A batch gets a fresh CI run each time it is checked again. A
[retry](/merge-queue/lifecycle#automatic-ci-retries) recreates the draft, or
pushes the batch under a new branch name when there is no draft, and each test
of a [split batch](/merge-queue/batches#handling-batch-failure-or-timeout) 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`](/merge-queue/batches#reading-the-metadata) | The draft's number | `null` |
| Queue rules | All of them | [Not all](#queue-rules-that-still-need-a-draft) |
| Availability | Default | Preview, [turned on in Mergify](#turn-drafts-off) |

## With a draft pull request

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](/merge-queue/batches/draft-pull-requests) 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](/merge-queue/two-step#github-actions).

## Without a draft pull request

<PreviewNotice />

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.

### What changes without a draft

- **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](#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](#cancel-runs-when-a-batch-ends).

### 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:

- [`merge_method: fast-forward`](/merge-queue/merge-strategies#fast-forward) or
  `queue_branch_merge_method: fast-forward`;

- [`allow_queue_branch_edit`](/merge-queue/merge-strategies#editing-the-batch-branch);

- [`merge_method: merge-batch`](/merge-queue/merge-strategies#merge-batch) with a
  [`merge_queue.mode`](/merge-queue/queue-modes) other than `serial`.

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](/merge-queue/batches/draft-pull-requests#when-the-queue-checks-in-place) is not
affected, since it opens no draft in the first place.

### Turn drafts off

[Set up your CI](#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](/merge-queue/lifecycle#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](/api/merge-queue) sets the same levels, each with its own
`/merge-queue/settings` endpoint. To turn drafts off for `main`:

```bash
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.

### Set up your CI

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.

#### GitHub Actions

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

```yaml
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.

#### CircleCI

**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:

```bash
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:

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

#### Buildkite

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:

```text
main mergify/merge-queue/*
```

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

#### Jenkins

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`:

```yaml
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.

#### GitLab CI

With [CI/CD for external repositories](https://docs.gitlab.com/ci/ci_cd_for_external_repos/),
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:

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

#### Azure Pipelines

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:

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

#### TeamCity

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:

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

#### Travis CI

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

```yaml
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

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:

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

The same goes for the [Two-step CI](/merge-queue/two-step#github-actions) 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`](/merge-queue/batches#reading-the-metadata) 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.

### Cancel runs when a batch ends

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`:

```yaml
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.
