---
title: Implementing a Two-Step CI with Mergify
description: Run essential tests on every PR and comprehensive tests before merging, optimizing CI time and resources.
---

Split your CI into fast preliminary checks on every PR and full tests that run only before merging.

:::tip
  Understand the concepts behind two-step CI at the
  [Merge Queue Academy](https://merge-queue.academy/features/two-step-ci/).
:::

<Youtube video="2mVymDFMaMk" title="Using two-step CI"/>

## The Two Phases

**Step 1: Preliminary tests** run on every PR push: linters, formatters,
unit tests, basic compile checks. Fast feedback, cheap to run.

**Step 2: Pre-merge tests** run only when a PR enters the merge queue:
integration tests, end-to-end tests, performance benchmarks. Thorough but
expensive.

## How It Works

```dot class="flow"
digraph {
  label="Two-step continuous integration";
  node [width=2.2];

  open [label="Pull request opened\nor updated", class="queued"];
  preliminary [label="Preliminary tests\n(unit tests, linting)", class="pending"];
  preliminary_ok [label="Tests passed\nready for the queue", class="merged"];
  preliminary_fail [label="Tests failed\nneeds fixes", class="failed"];
  queue_req [label="Queue command\n(@mergifyio queue)", class="queued"];
  queued [label="Pull request queued", shape=oval, class="queued"];
  premerge [label="Pre-merge tests\n(integration, performance)", class="pending"];
  premerge_ok [label="All tests passed\nready to merge", class="merged"];
  premerge_fail [label="Pre-merge failed\nremoved from the queue", class="failed"];
  merged [label="Merged to main", class="merged"];

  open -> preliminary;
  preliminary -> preliminary_ok [class="merged"];
  preliminary -> preliminary_fail [class="failed"];
  preliminary_ok -> queue_req;
  queue_req -> queued;
  queued -> premerge;
  premerge -> premerge_ok [class="merged"];
  premerge -> premerge_fail [class="failed"];
  premerge_ok -> merged [class="merged"];

  { rank=same; preliminary_ok; preliminary_fail; }
  { rank=same; premerge_ok; premerge_fail; }
}
```

## Batch Processing

Enable [batching](/merge-queue/batches) to run pre-merge tests **once** for a
group of PRs instead of per PR. For 5 PRs with a 30-minute pre-merge suite,
that's 30 minutes instead of 2.5 hours.

## Configuration

### CI System

Run pre-merge tests only on merge queue branches. Mergify creates branches
prefixed with `mergify/merge-queue/` (customizable via `queue_branch_prefix`
in [`queue_rules`](/configuration/file-format/#queue-rules)).

#### GitHub Actions

```yaml
name: CI

on:
  pull_request:
    branches:
      - main

jobs:
  # STEP 1: Preliminary tests (runs on every PR)
  preliminary-tests:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Run linters
      run: make lint
    - name: Run unit tests
      run: make test-unit

  # STEP 2: Pre-merge tests (runs only on merge queue branches)
  pre-merge-tests:
    if: startsWith(github.head_ref, 'mergify/merge-queue/')
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - name: Run integration tests
      run: make test-integration
    - name: Run E2E tests
      run: make test-e2e
    - name: Run performance benchmarks
      run: make benchmark
```

#### Other CI Systems

- **CircleCI**: Branch filter regex `/^mergify\/merge-queue\/.*/`
- **Jenkins**: Conditional execution on `BRANCH_NAME`
- **GitLab CI**: `only: /^mergify\/merge-queue\/.*/`

### Mergify

```yaml
queue_rules:
  - name: default
    # PRs can enter the queue after preliminary tests pass
    queue_conditions:
      - check-success=preliminary-tests

    # PRs can merge only after pre-merge tests pass
    merge_conditions:
      - check-success=pre-merge-tests
```

`queue_conditions` gates entry into the queue; `merge_conditions` gates the
final merge to main.
