aspect format runs your formatter (Prettier, gofmt, Black, clang-format or any other) on the files a change touched, and reports whether the tree was already formatted. It needs no CI-specific setup and runs the same on a developer’s machine.
How change detection works
aspect format finds the changed files with git diff against the merge base. The base ref comes from --base-ref, or when that’s empty:
- The CI environment:
GITHUB_BASE_REFon GitHub Actions pull requests,BUILDKITE_PULL_REQUEST_BASE_BRANCHon Buildkite,CI_MERGE_REQUEST_TARGET_BRANCH_NAMEon GitLab merge request pipelines. - The remote’s default branch.
origin/main.
--merge-base=<sha> to diff against an explicit commit instead.
When git diff can’t resolve a base (a shallow clone, for example) and the Aspect Workflows GitHub App is authenticated on a pull request build, it asks the GitHub PR Files API instead, which includes renames and moves.
If neither resolves a base, --scope=changed degrades to a whole-tree format and says so — Could not determine the changed-file set (<source>). Formatting all files in scope (equivalent to --scope=all). Worth knowing because excluded paths may still be rewritten under scope-all. The warning names the same two fixes: --merge-base=<sha>, or a --base-ref that exists locally.
Either way the formatter receives paths relative to the repository root.
Detecting what the formatter changed
Most formatters rewrite files in place and exit 0 whether or not they changed anything, so the exit code can’t say whether formatting was needed.aspect format snapshots the tree first with git stash create, which records the working tree without touching it, the index or the stash list. After the formatter runs, it diffs against that snapshot. Uncommitted changes you had before the run aren’t counted as formatter changes.
Configuration
Formatter target
aspect format builds and runs a Bazel target that contains your formatters. The default is //tools/format, typically a format_multirun from aspect_rules_lint.
.aspect/config.axl
Scope: changed files or the whole tree
--scope=all suits nightly jobs and introducing a new formatter. The formatter walks the tree itself and change detection is skipped. Files passed as arguments override both.
Reacting to formatter output
.aspect/config.axl
auto, the default, is fail on a recognized CI host and info off it.
File filters
.aspect/config.axl
--scope=changed, excluded paths are dropped from the file list before the formatter runs. With --scope=all the formatter finds files itself, so it may still rewrite excluded ones, but they’re left out of the diff that decides the result. include_patterns does the reverse, passing only matching files. Pattern syntax: * matches within one path segment, ** any number of segments, ? one character; matching is case-sensitive.
Patch upload
When the formatter modifies files on CI,aspect format uploads the diff as a format.patch artifact so developers can apply it instead of re-running the formatter. It’s on by default. To turn it off:
.aspect/config.axl
ArtifactUpload feature build and test use, and status checks link to it where the CI provider returns a download URL.
Options
It also takes the Bazel flag options common to every task that runs Bazel (
--bazel-flag, --bazel-startup-flag, --announce-bazel-*, --bes-backend, --bes-header); see aspect run.
CI example
.github/workflows/format.yaml

