View as Markdown

Scopes with File Patterns

Configure merge queue scopes with file patterns so the queue batches related pull requests together.


File-pattern scopes tag each pull request with the parts of your monorepo it changes, so the merge queue can batch related changes together. They need no CI setup and work best when your monorepo follows clear directory boundaries.

Define scopes in your .mergify.yml configuration file using file patterns:

scopes:
source:
files:
python-api:
include:
- api/**/*.py
- libs/shared/**/*.py
frontend:
include:
- web/**/*.js
- web/**/*.jsx
- web/**/*.ts
- web/**/*.tsx
docs:
include:
- docs/**/*.md
- docs/**/*.mdx
queue_rules:
- name: default
batch_size: 5

In this example:

  • Changes to Python files in api/ or libs/shared/ get the python-api scope
  • Changes to frontend files in web/ get the frontend scope
  • Documentation changes get the docs scope

include and exclude accept the same glob patterns as the *= operator in conditions. That includes brace alternation, which collapses a list of extensions into one pattern. The frontend scope above can also be written as:

scopes:
source:
files:
frontend:
include:
- web/**/*.{js,jsx,ts,tsx}

Mergify validates every pattern when it loads your configuration. An unbalanced { is a configuration error, not a scope that silently matches nothing.

How Mergify evaluates file-pattern scopes

Section titled How Mergify evaluates file-pattern scopes

Mergify evaluates file-pattern scopes itself. For each pull request, it matches the changed files against your include (and optional exclude) patterns and tags the pull request with the scopes it touches. This happens in the merge queue automatically, with no CI job or GitHub Action to set up: declare the patterns above and the queue batches by scope.

Conditioning CI jobs on scopes (optional)

Section titled Conditioning CI jobs on scopes (optional)

The same scopes can also drive your CI, letting you skip jobs whose scope a pull request doesn’t touch. This is Job Selection, and it is optional and independent of merge queue batching. To wire it up, follow the Job Selection section of the GitHub Actions integration. It walks through:

  • Adding the gha-mergify-ci workflow step that reads your file-pattern scopes and exposes them as outputs.

  • Conditioning downstream jobs (frontend, backend, docs, etc.) on those scope outputs.

  • Waiting on the right set of jobs so required checks always report, even when work is skipped.

That section already includes complete workflow samples, so you do not need to duplicate the config here. Just reuse the same scopes you declared above. The Buildkite integration walks through the first two steps for Buildkite pipelines, using a dynamic pipeline instead of job conditions.

When you detect scopes in CI, detection also reports a special merge-queue scope. It is true only when the build runs on a draft pull request the merge queue creates, and false on every other build. On GitHub Actions, the gha-mergify-ci action exposes it as an output like any other scope. On Buildkite, the mergify-ci plugin writes it to the mergify-ci.scopes meta-data key next to your other scopes. The name comes from scopes.merge_queue_scope, which defaults to merge-queue. If you set it to something else, detection reports that name instead, so gate your jobs on it.

This is useful for:

  • Integration tests that only need to run before merging
  • End-to-end tests that are expensive and should only run in the merge queue
  • Deployment validation that needs to happen before code reaches the main branch

Gate the job on the scope, the same way you gate any other. detect-scopes below is the job from the GitHub Actions integration, with merge-queue added to its outputs:

integration-tests:
if: ${{ needs.detect-scopes.outputs.merge-queue == 'true' }}
needs: detect-scopes
runs-on: ubuntu-26.04
steps:
- uses: actions/checkout@v5
- name: Run expensive integration tests
run: npm run test:integration

On Buildkite, emit the step from the dynamic pipeline script only when the scope is "true":

Terminal window
if echo "$SCOPES" | jq -e '.["merge-queue"] == "true"' > /dev/null 2>&1; then
cat <<'YAML'
- label: ":rocket: Integration tests"
command: npm run test:integration
YAML
fi

Example: multi-language monorepo

Section titled Example: multi-language monorepo

This configuration covers a monorepo with Python, JavaScript, and Go services:

scopes:
source:
files:
python-api:
include:
- services/api/**/*.py
- libs/python/**/*.py
user-service:
include:
- services/users/**/*.go
frontend:
include:
- apps/web/**/*.js
- apps/web/**/*.jsx
- apps/web/**/*.ts
- apps/web/**/*.tsx
shared-config:
include:
- config/**/*
- docker/**/*
queue_rules:
- name: default
batch_size: 8
batch_max_wait_time: 5 min

With this configuration, the queue prefers to batch pull requests that share a scope: a TypeScript change under apps/web/ goes into a batch with other frontend changes before it is mixed with a Python change under services/api/.

A scope name carries no special meaning, so shared-config does not batch with every other scope just because it happens to hold shared files. To make a change to shared files affect every scope, list those files under barrier_files instead.

Scope detection is PR-specific

Section titled Scope detection is PR-specific

Mergify evaluates the files changed by each pull request on its own, not the files of other pull requests in the same batch, so its scopes stay the same whatever else is queued or batched with it.

Renamed files count at both paths

Section titled Renamed files count at both paths

A rename touches two paths: the file disappears from where it was and appears where it went. Mergify matches your patterns against both, so moving a file out of a scope reports that scope just as deleting the file would.

With the multi-language scopes above, git mv services/api/settings.py config/settings.py reports both python-api (the path the file left) and shared-config (the path it arrived at). That matters when a scope guards something: a pull request that moves a file out of services/api/ is a change to services/api/, so the python-api scope’s CI jobs and batching rules apply to it.

The same rule applies to barrier_files: renaming a file out of a barrier pattern makes the pull request a barrier, because the barrier file is gone from its declared location.

GitHub Actions offers path filtering (on.pull_request.paths), but it does not work well with a merge queue:

# ❌ Don't use path filtering for merge queues
on:
pull_request:
paths:
- 'api/**'

Problems with path filtering:

  • When a job doesn’t run, you can’t distinguish between “filtered out” and “CI failed to start”

  • Required status checks stay pending and block the merge when filtering keeps a workflow from running

  • In merge queues, you don’t want to skip tests on PR2 just because PR1 in the batch modified different files

With scopes:

  • Jobs always run but can conditionally skip work based on scope detection
  • Status checks always report (success or skipped)
  • Merge queue batching respects scope boundaries

Was this page helpful?