Skip to main content
aspect lint runs one or more aspect_rules_lint aspects as a standard Bazel build, reads SARIF diagnostic output, and decides whether to fail the CI step based on which findings are in scope. It posts inline review comments on the diff via the GithubLintComments (GitHub) and GitlabLintComments (GitLab) features. The key design: aspect lint doesn’t re-implement linting. It drives Bazel, which caches lint results the same as any other action. If a file hasn’t changed, its lint results come from the remote cache rather than running the linter again. At scale, aspect lint //... on a large repo is fast because Bazel only re-lints what’s actually changed.

Hold-the-line strategy

The default failure strategy — hold-the-line — is what makes lint practical on large, pre-existing codebases with known violations. Instead of failing on any finding anywhere, hold-the-line:
  1. Detects the PR’s changed lines (via GitHub PR Files API or git diff)
  2. Fails only on error-severity findings on lines you actually changed
This means: existing violations in untouched files are visible but don’t block the PR. New violations introduced by the PR do. Teams adopting lint don’t have to clean up the entire codebase first. Four strategies are available:
StrategyWhen it fails
hold-the-line (default)Error-severity findings on changed lines
hold-the-fileError-severity findings anywhere in a changed file
hardAny error-severity finding in any linted target
softNever (report only)
aspect lint --strategy=hold-the-line //...   # default
aspect lint --strategy=hard //...            # zero-tolerance
aspect lint --strategy=soft //...            # informational only

Configuration

Lint aspects

aspect lint requires at least one --aspect pointing to an aspect_rules_lint aspect:
aspect lint \
  --aspect=//tools/lint:linters.bzl%eslint \
  --aspect=//tools/lint:linters.bzl%shellcheck \
  //...
Declare them in config.axl so developers running aspect lint //... locally use the same set as CI:
.aspect/config.axl
def config(ctx: ConfigContext):
    ctx.tasks["lint"].args.aspects = [
        "//tools/lint:linters.bzl%eslint",
        "//tools/lint:linters.bzl%shellcheck",
        "//tools/lint:linters.bzl%buf",
    ]
CLI flags override config.axl. A CI job that needs only fast linters can pass --aspect= directly without affecting the default config.

Apply auto-fix patches

Some rules_lint linters produce fix patches. Pass --fix to apply them to the working tree:
aspect lint --fix //...
Useful in local dev and in automated “fix and commit” CI flows.

Changed-file scope

By default, aspect lint runs on all targets and strategy filtering determines which findings fail. To restrict the Bazel build to targets in changed packages:
aspect lint --base-ref=origin/main //...
Override --base-ref when your main branch isn’t origin/main.

GitHub PR annotations

GithubLintComments posts inline PR review comments for every error-severity finding that holds the line (i.e., would have caused a failure under hold-the-line). Reviewers see the issue directly on the diff line without switching to CI logs. Enable by authenticating the Aspect Workflows GitHub App and setting ASPECT_API_TOKEN. The feature is always enabled — it just no-ops when not authenticated. Suggestions from auto-fix linters become one-click “commit suggestion” buttons in the PR review.

GitLab MR comments

GitlabLintComments is the GitLab counterpart: it posts each finding as a position-bound discussion on the merge-request diff (the GitLab equivalent of a GitHub PR review comment), so reviewers see the issue inline in the MR’s Changes view. Enable by authenticating the Aspect GitLab App and setting ASPECT_API_TOKEN. The feature is always enabled — it no-ops when not in a merge-request pipeline or when not authenticated. It mirrors the GitHub feature, with three GitLab-specific behaviors:
  • Suggestion blocks — auto-fix patches render as GitLab ```suggestion blocks reviewers apply with one click.
  • Dedup across runs — re-running the lint task replaces a stale discussion at the same position instead of stacking duplicates.
  • Thread auto-resolve — on a clean pass (the task finds nothing), discussions Aspect opened on prior runs are marked Resolved rather than deleted, preserving the review history. (GitHub PR review comments have no native resolve, so the GitHub path deletes instead.)
Cap the number of discussions per run with --gitlab-lint-comments:max-pr-comments=<n> (default 25; 0 disables posting while keeping the lint exit code intact). Errors are posted first, then warnings, then notes; any excess is dropped from the MR but still drives the lint verdict.

Routing findings: findings_destination

Where findings land is controlled by the shared LintTrait.findings_destination knob (applies to both the GitHub and GitLab features). The default is auto: auto-fix suggestions post as inline comments, while other findings post as check-run annotations on GitHub — and on GitLab, which has no per-finding annotation surface, auto posts only the auto-fix suggestions. To post every finding as an inline comment (recommended on GitLab so non-fix findings show up too), set it to comments in .aspect/config.axl:
.aspect/config.axl
load("@aspect//traits.axl", "LintTrait")

def config(ctx: ConfigContext):
    ctx.traits[LintTrait].findings_destination = "comments"
Accepted values:
ValueBehavior
auto (default)Fix-bearing findings → inline comments; the rest → check-run annotations (GitHub) or no inline surface (GitLab).
commentsEvery finding → inline comment (GitHub PR review comment / GitLab MR discussion).
annotationsEvery finding → check-run annotation; no inline comments. Suggestion blocks are dropped.
bothEvery finding posted to both surfaces.
The config hook is named config (def config(ctx: ConfigContext):) — a function named configure is not invoked by the CLI and silently does nothing.

How it works under the hood

aspect lint uses two Bazel output groups from aspect_rules_lint:
  • rules_lint_machine — SARIF JSON and per-target exit codes. Used for CI verdicts and annotation posting.
  • rules_lint_human — Colored terminal output. Streamed to the developer’s terminal.
After the Bazel build completes, the task downloads SARIF files from the output groups (polling for up to 30s to allow remote cache downloads to finish) and processes them:
  1. Reads every diagnostic from every SARIF file
  2. Computes the changed-line set for the PR (GitHub PR Files API or git diff)
  3. Filters diagnostics according to the chosen strategy
  4. For hold-the-line, uses ±3 line context around each changed hunk to catch findings that are technically on adjacent-but-related lines
  5. Posts inline comments for each finding that passed the filter — via GithubLintComments on GitHub, GitlabLintComments on GitLab
  6. Exits 0 or 1 based on whether any filtered findings have error severity
Because lint results go through Bazel’s action cache, re-running aspect lint after an unrelated change doesn’t re-execute any linter whose inputs haven’t changed. On Aspect Workflows with remote cache, lint is O(changed targets), not O(repo size).

CI examples

on:
  pull_request:
    branches: [main]

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