Skip to main content
aspect gazelle runs Gazelle (or aspect_gazelle_prebuilt, for Starlark-defined extensions) to generate and synchronize your repository’s BUILD files. In CI it detects whether BUILD files are out of sync with source code and fails the step if so — preventing drift before it accumulates into a bigger cleanup. Running aspect gazelle locally applies fixes in place. Same command, different behavior based on context — --check-only defaults to true on CI and false locally.

Setup: choosing a gazelle binary

aspect gazelle drives a Gazelle binary that you point it at with --gazelle-target (default //tools/gazelle:gazelle). There are two ways to provide that binary, and aspect gazelle supports both. aspect_gazelle_prebuilt fetches a precompiled Gazelle binary from the aspect-gazelle releases. No Go toolchain and no compile step are required, and it is the only binary that can run Starlark/AXL extensions.
MODULE.bazel
# See https://github.com/aspect-build/aspect-gazelle/releases for the latest version.
bazel_dep(name = "aspect_gazelle_prebuilt", version = "0.0.21")
tools/gazelle/BUILD.bazel
load("@aspect_gazelle_prebuilt//:def.bzl", "aspect_gazelle")

aspect_gazelle(
    name = "gazelle",
    languages = ["go", "proto", "python"],  # whichever fit your repo
    extensions = ["//tools/gazelle:shell.axl"],
)
Advantages:
  • No Go toolchain download, and no first-run compile of the gazelle binary — developers fetch a cached prebuilt artifact instead.
  • Runs Starlark/AXL extensions, so you can write generators without the Go extension API.
  • Exposes prebuilt-only flags like -progress and -cache (see Additional Gazelle flags).
  • Avoids the bootstrapping trap where a broken BUILD file referenced by your gazelle binary’s load statements prevents gazelle from compiling — and therefore from fixing that file.

Building from source

Build Gazelle with the upstream gazelle_binary macro from bazel-contrib/bazel-gazelle, composing whatever language extensions you need (including custom first-party Go extensions). This requires a Go toolchain.
tools/gazelle/BUILD.bazel
load("@bazel_gazelle//:def.bzl", "gazelle", "gazelle_binary")

gazelle_binary(
    name = "gazelle_binary",
    languages = [
        "@bazel_gazelle//language/proto",
        "@bazel_gazelle//language/go",
        "@rules_python//gazelle",
        "//tools/gazelle/my_extension",  # a custom first-party go_library
    ],
)

gazelle(
    name = "gazelle",
    gazelle = ":gazelle_binary",
)
Advantages:
  • Full control: compose any language extension and write custom generators against Gazelle’s Go Language interface.
  • No dependency on which extensions ship in the prebuilt binary.
The tradeoff is build cost and complexity: a Go (and sometimes cgo) toolchain, a recompile whenever an extension changes, and the bootstrapping trap noted above. For most repos the prebuilt binary is the better default.

Incremental mode: only touch changed directories

For large repos, running Gazelle over the entire tree on every PR is slow. --scope=changed (available when --scope-all-on-change patterns aren’t matched) restricts Gazelle to directories containing changed files:
aspect gazelle --scope=changed   # only dirs with PR-changed files
aspect gazelle --scope=all       # full workspace (default)
Smart escalation: certain file changes require a full-repo re-run. By default, changes to BUILD.bazel or MODULE.bazel escalate to --scope=all because those files can affect dependency resolution across the entire repo. Configure which patterns trigger escalation:
.aspect/config.axl
def config(ctx: ConfigContext):
    ctx.tasks["gazelle"].args.scope_all_on_change = [
        "BUILD.bazel",
        "MODULE.bazel",
        "go.sum",           # Go module changes require a full re-index
    ]

How drift detection works

aspect gazelle runs Gazelle with -mode=diff internally regardless of other flags. This is the key:
  1. Gazelle runs once with -mode=diff — it produces a unified diff without writing any files
  2. The task parses the diff to determine if BUILD files are out of sync (exit code from Gazelle isn’t reliable — the task uses stdout content)
  3. On CI (--check-only=true): if the diff is non-empty, the task exits 1
  4. Locally (--check-only=false): the task applies the diff via git apply -p0
Why single-run? Running Gazelle twice (once to detect, once to apply) would be expensive on a large monorepo. The diff-mode approach gets both the verdict and the patch from one Gazelle invocation. Applying the captured diff is much faster than a second Gazelle run.

Configuration

Gazelle target

aspect gazelle --gazelle-target=//tools/gazelle:gazelle
.aspect/config.axl
def config(ctx: ConfigContext):
    ctx.tasks["gazelle"].args.gazelle_target = "//tools/gazelle:gazelle"
Default: //tools/gazelle:gazelle.

Reacting to drift

aspect gazelle --on-change=fail    # default on CI
aspect gazelle --on-change=warn    # warn but don't block
aspect gazelle --on-change=silent  # detect-only, no output

Additional Gazelle flags

Pass extra flags to the Gazelle binary (any -mode flag is stripped internally since the task controls that):
aspect gazelle --gazelle-flag=-index=lazy //...
-index=lazy pairs well with --scope=changed for the best incremental performance: Gazelle only indexes the packages it visits rather than the whole repo.

Patch upload

.aspect/config.axl
def config(ctx: ConfigContext):
    ctx.tasks["gazelle"].args.upload_gazelle_diff = True
Uploads the BUILD diff as gazelle.patch when drift is detected, so developers can apply it locally.

CI examples

on:
  pull_request:
    branches: [main]

jobs:
  gazelle:
    runs-on: [self-hosted, aspect-workflows, aspect-default]
    permissions:
      id-token: write
    steps:
      - uses: actions/checkout@v6
      - uses: aspect-build/setup-aspect@2306377a61c45954ab2df7c7311698b109364352 # v2026.26.9
        with:
          aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
      - run: aspect gazelle --task:name gazelle