Skip to main content
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:
  1. The CI environment: GITHUB_BASE_REF on GitHub Actions pull requests, BUILDKITE_PULL_REQUEST_BASE_BRANCH on Buildkite, CI_MERGE_REQUEST_TARGET_BRANCH_NAME on GitLab merge request pipelines.
  2. The remote’s default branch.
  3. origin/main.
Pass --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
With --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
The patch goes through the same 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
Complete pipeline examples shows the same setup on Buildkite, GitLab CI and CircleCI, and on Aspect Workflows CI runners.