---
title: Scopes with Bazel
description: Configure merge queue scopes using Bazel's dependency graph for precise batching.
---

If you're using monorepo tools like Bazel that have built-in dependency graph analysis,
you can use their affected project detection instead of file patterns.
This approach is often more accurate because these tools understand your project's
dependency relationships.

## Configuring Manual Scopes

To use the manual scopes mechanism, configure Mergify to expect scopes from your CI system:

```yaml
scopes:
  source:
    manual:

queue_rules:
  - name: default
    batch_size: 5
```

## Detecting Scopes with Bazel

Use `bazel query` to determine affected projects and upload them to Mergify.

### GitHub Actions

```yaml
name: Detect Scopes
on:
  pull_request:

jobs:
  detect-scopes:
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v5
        with:
          # The queue-aware base commit must exist locally for `git diff`.
          fetch-depth: 0

      - name: Get git refs
        id: refs
        uses: Mergifyio/gha-mergify-ci@@@GHA_MERGIFY_CI_VERSION@@
        with:
          action: scopes-git-refs

      - name: Get scopes
        id: scopes
        env:
          HEAD: ${{ steps.refs.outputs.head }}
          BASE: ${{ steps.refs.outputs.base }}
        run: |
          changed=$(git diff --name-only --diff-filter=ACMR "$BASE" "$HEAD" | tr '\n' ' ')
          if [ -z "$changed" ]; then
            echo "scopes=" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          set +e
          packages=$(bazel query --keep_going --noshow_progress --output=package \
            "buildfiles(set($changed))" 2>query.err)
          rc=$?
          set -e
          # --keep_going exits 3 when it skipped files outside any Bazel package
          # (README.md, .github/**). Anything else is a real failure and must not
          # be swallowed into an empty scope list.
          if [ "$rc" -ne 0 ] && [ "$rc" -ne 3 ]; then
            cat query.err >&2
            exit "$rc"
          fi

          scopes=$(printf '%s\n' "$packages" | sed '/^$/d' | sort -u | paste -sd, -)
          echo "Detected scopes: ${scopes:-<none>}"
          echo "scopes=$scopes" >> "$GITHUB_OUTPUT"

      - name: Scopes upload
        uses: Mergifyio/gha-mergify-ci@@@GHA_MERGIFY_CI_VERSION@@
        with:
          action: scopes-upload
          token: ${{ secrets.MERGIFY_TOKEN }}
          scopes: ${{ steps.scopes.outputs.scopes }}
```

### Buildkite

Using the
[`mergifyio/mergify-ci`](https://github.com/Mergifyio/mergify-ci-buildkite-plugin)
Buildkite plugin, a first step resolves the merge-queue-aware base and head
SHAs and exposes them as meta-data, while a second step computes the affected
projects with `bazel query` and uploads them to Mergify:

```yaml
steps:
  - label: ":mag: Get git refs"
    key: git-refs
    plugins:
      - mergifyio/mergify-ci#@@BUILDKITE_PLUGIN_VERSION@@:
          action: scopes-git-refs

  - label: ":mag: Detect and upload scopes"
    depends_on: git-refs
    command: |
      BASE=$(buildkite-agent meta-data get "mergify-ci.base")
      HEAD=$(buildkite-agent meta-data get "mergify-ci.head")
      CHANGED=$(git diff --name-only --diff-filter=ACMR "$BASE" "$HEAD" | tr '\n' ' ')
      SCOPES=""
      if [ -n "$CHANGED" ]; then
        set +e
        PACKAGES=$(bazel query --keep_going --noshow_progress --output=package \
          "buildfiles(set($CHANGED))" 2>query.err)
        RC=$?
        set -e
        # --keep_going exits 3 when it skipped files outside any Bazel package.
        # Anything else is a real failure, not an empty scope list.
        if [ "$RC" -ne 0 ] && [ "$RC" -ne 3 ]; then
          cat query.err >&2
          exit "$RC"
        fi
        SCOPES=$(printf '%s\n' "$PACKAGES" | sed '/^$/d' | sort -u | paste -sd, -)
      fi
      buildkite-agent meta-data set "mergify-ci.scopes" "$SCOPES"
    plugins:
      - mergifyio/mergify-ci#@@BUILDKITE_PLUGIN_VERSION@@:
          action: scopes-upload
          token: "${MERGIFY_TOKEN}"
```

### Any CI (Mergify CLI)

<ScopesDetection
  command={String.raw`CHANGED=$(git diff --name-only --diff-filter=ACMR "$BASE" "$HEAD" | tr '\n' ' ')

if [ -z "$CHANGED" ]; then
  echo '{"scopes": []}' > scopes.json
else
  # Capture the exit code rather than piping straight into jq: a failed query
  # piped into jq still writes a well-formed, empty scopes.json and returns 0.
  if PACKAGES=$(bazel query --keep_going --noshow_progress --output=package \
                  "buildfiles(set($CHANGED))" 2>query.err); then
    RC=0
  else
    RC=$?
  fi

  # --keep_going exits 3 when it skipped files outside any Bazel package
  # (README.md, .github/**). Anything else is a real failure.
  if [ "$RC" -ne 0 ] && [ "$RC" -ne 3 ]; then
    cat query.err >&2
    exit "$RC"
  fi

  printf '%s\n' "$PACKAGES" | sort -u \
    | jq -R -s '{scopes: split("\n") | map(select(length > 0))}' > scopes.json
fi`}
/>
