Implementing a Two⁠-⁠Step CI with Mergify

Run essential tests on every PR and comprehensive tests before merging, optimizing CI time and resources.


As your codebase grows, so does your test suite. What once took minutes can balloon into hour-long CI runs that slow down development and drain resources. But here’s the insight: not all tests need to run at every stage of the PR lifecycle.

Two-step CI is a strategy that separates your test suite into two phases—fast preliminary checks that run on every commit, and comprehensive validation that runs only before merging. Combined with Mergify’s merge queue, this approach can dramatically reduce CI time and costs while maintaining code quality.

A two-step CI approach separates tests into two phases based on when they need to run and what they validate:

Step 1: Preliminary Tests (Fast Feedback)

Section titled Step 1: Preliminary Tests (Fast Feedback)

Run immediately when a PR is opened or updated. These are:

  • Fast: Complete in minutes, not hours
  • Essential: Code quality checks that catch obvious issues

Examples: Linters, formatters, unit tests, basic compile checks

These give developers rapid feedback and prevent broken code from entering the merge queue.

Step 2: Pre-Merge Tests (Comprehensive Validation)

Section titled Step 2: Pre-Merge Tests (Comprehensive Validation)

Run only when a PR enters the merge queue, just before merging. These are:

  • Thorough: Full test coverage including edge cases
  • Expensive: Integration tests, end-to-end tests, performance benchmarks
  • Slower: May take 30+ minutes to complete

These jobs ensure code quality before merging to your main branch, without slowing down every PR update.

%0 open 🔄 PR opened or updated preliminary 🧪 Preliminary tests (Unit tests, linting) open->preliminary preliminary_ok ✅ Tests passed (Ready for queue) preliminary->preliminary_ok preliminary_fail ❌ Tests failed (Needs fixes) preliminary->preliminary_fail queue_req 📝 Queue command (@mergifyio queue) preliminary_ok->queue_req queued ⏳ PR queued queue_req->queued premerge 🔬 Pre-merge tests (Integration, performance) queued->premerge premerge_ok ✅ All tests passed (Ready to merge) premerge->premerge_ok premerge_fail ❌ Pre-merge failed (Removed from queue) premerge->premerge_fail merged 🎉 Merged to main premerge_ok->merged

Batch Processing: Even More Efficient

Section titled Batch Processing: Even More Efficient

When you enable batching in Mergify’s merge queue, you can achieve even greater efficiency. Instead of running pre-merge tests separately for each PR, Mergify:

  1. Groups multiple PRs together in a batch
  2. Runs the expensive pre-merge tests once for the entire batch
  3. Merges all PRs if the batch passes

Example: If you have 5 PRs ready to merge and your pre-merge tests take 30 minutes:

  • Without batching: 5 × 30 minutes = 2.5 hours of CI time
  • With batching: 1 × 30 minutes = 30 minutes of CI time

That’s 80% reduction in CI resource usage!

%0 cluster_pr1 PR #1 Workflow cluster_pr2 PR #2 Workflow cluster_batch open1 🔄 PR #1 opened preliminary1 🧪 Tests open1->preliminary1 preliminary_ok1 ✅ Passed preliminary1->preliminary_ok1 queue_req1 📝 Queue preliminary_ok1->queue_req1 queued ⏳ Both PRs queued together queue_req1->queued open2 🔄 PR #2 opened preliminary2 🧪 Tests open2->preliminary2 preliminary_ok2 ✅ Passed preliminary2->preliminary_ok2 queue_req2 📝 Queue preliminary_ok2->queue_req2 queue_req2->queued batch_title 📦 Batch Processing (Resource Efficient) premerge 🔬 Single test run (Both PRs tested) queued->premerge premerge_ok ✅ Batch passed premerge->premerge_ok merged1 🎉 PR #1 merged premerge_ok->merged1 merged2 🎉 PR #2 merged premerge_ok->merged2

Implementing two-step CI requires coordinating your CI system with Mergify’s merge queue. Here’s how:

  1. Configure CI to distinguish test phases: Set up your CI to run preliminary tests on all branches, but pre-merge tests only on merge queue branches

  2. Configure Mergify conditions: Define which tests must pass to enter the queue vs. which must pass to merge

Step 1: Configure Your CI System

Section titled Step 1: Configure Your CI System

The key is to run pre-merge tests only on merge queue branches. By default, Mergify creates branches prefixed with mergify/merge-queue/ for queued PRs (customizable via queue_branch_prefix in queue_rules).

Pattern:

  • ✅ Preliminary tests: Run on all branches
  • ✅ Pre-merge tests: Run only on branches matching mergify/merge-queue/*
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

How it works:

  • preliminary-tests runs on every PR (fast feedback)

  • pre-merge-tests runs only when the branch name starts with mergify/merge-queue/ (comprehensive validation before merge)

The same principle applies to any CI system. Just configure the pre-merge job to run only on merge queue branches:

  • CircleCI: Use branch filters with regex /^mergify\/merge-queue\/.*/
  • Jenkins: Add conditional execution based on BRANCH_NAME
  • GitLab CI: Use only: /^mergify\/merge-queue\/.*/

Set up your queue_rules to define when PRs can enter the queue and when they can merge:

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

What this means:

  • queue_conditions: Preliminary tests must pass before a PR can enter the merge queue

  • merge_conditions: Pre-merge tests must pass before the PR actually merges to main