Aspect CLI

repository·main·Indexed 41 days ago

https://github.com/aspect-build/aspect-cli

A programmable task runner built on top of Bazel that provides a consistent interface for developer workflows including build, test, lint, format, gazelle, and delivery. It ensures identical behavior across local machines and CI environments, featuring built-in support for SARIF diagnostics, code coverage, and integration with GitHub and GitLab.

Tokens
61K
Snippets
153
Records
250
Agent score
51%

What's inside Aspect CLI

  1. Overview of aspect built-in tasks

    main

    Aspect-CLI provides six built-in tasks designed to drive Bazel for common CI workflows. Each task integrates Bazel commands with cross-cutting features like GitHub status checks, Buildkite annotations, artifact uploads, and runner health checks, ensuring consistent output across different CI hosts.

    Built-in Tasks:

    • build: Runs bazel build over target patterns.
    • test: Runs bazel test over target patterns.
    • lint: Runs bazel build with specific aspects and processes SARIF diagnostics.
    • format: Runs a formatter target and checks for changes.
    • gazelle: Runs Gazelle targets and captures patches.
    • delivery: Executes a multi-phase delivery flow against a deliveryd backend.
  2. What is basil and how to use it for testing

    main

    basil is a mock bazel binary designed to drive integration tests for ctx.bazel.build within the AXL ecosystem. It mimics the Bazel CLI just enough to satisfy axl-runtime without the overhead of a real Bazel server.

    To use basil as a replacement for Bazel in your tests, point the runtime to it using the BAZEL_REAL environment variable (following the Bazelisk convention).

    Key Features:

    • Deterministic & Fast: It provides canned Build Event Protocol (BEP) sequences, making it ideal for unit-level coverage of BES (Build Event Stream) handling.
    • Edge Case Reproduction: It can simulate complex scenarios like cache eviction, broken pipes, or retry storms that are difficult to trigger with a live Bazel instance.
    • Scenario-based: You can select specific behaviors using the --scenario flag.
    export BAZEL_REAL=/path/to/basil
  3. How tasks use and register Traits

    main

    In AXL, tasks declare which traits they consume, and features register themselves onto those traits by subscribing to hooks.

    1. Declaring Traits in a Task When defining a task, list the required traits in the traits argument of the task(...) function.

    build = task(
        name = "build",
        traits = [BazelTrait, HealthCheckTrait, Phases, ...],
        implementation = _impl,
        ...
    )

    2. Registering on Traits in a Feature Features (implementations) access traits via the ctx.traits mapping and append handlers to the trait's slots (hook lists).

    def _artifact_upload_impl(ctx: FeatureContext):
        bazel_trait = ctx.traits[BazelTrait]
        artifacts_trait = ctx.traits[ArtifactsTrait]
        
        # Subscribe to build events
        bazel_trait.build_event.append(_on_build_event)
        bazel_trait.build_end.append(_on_build_end)
    build = task(
        name = "build",
        traits = [BazelTrait, HealthCheckTrait, Phases,
                  GitHubStatusChecksTrait, ArtifactsTrait],
        implementation = _impl,
        ...
    )
    
    def _artifact_upload_impl(ctx: FeatureContext):
        bazel_trait    = ctx.traits[BazelTrait]
        artifacts_trait = ctx.traits[ArtifactsTrait]
        ...
        bazel_trait.build_event.append(_on_build_event)   # subscribe
        bazel_trait.build_end.append(_on_build_end)       # subscribe
  4. How flags are handled by the `tools/bazel` wrapper

    main

    The wrapper does not parse or modify flags. It forwards every argument to aspect exactly as typed, in its original position.

    Because Aspect CLI supports direct Bazel flag passthrough, any flag that aspect does not recognize is automatically forwarded to Bazel. This ensures that all Bazel startup options (before the verb) and command options (after the verb) continue to work correctly (e.g., --config=ci, -c opt, or --output_base).

    Compatibility Note:

    • Wrapper v2 + Aspect CLI v2+: Uses direct passthrough. This is the recommended setup.
    • Wrapper v1 + Older CLI: Requires flags to be rewritten as --bazel-flag=.... If you use a newer wrapper with an older CLI, you will encounter error: unexpected argument errors.
  5. How AXL (Aspect Extension Language) works

    main

    AXL is a typed Starlark dialect used to configure built-in tasks and define custom ones.

    • Configuration: Built-in tasks are configured in .aspect/config.axl.
    • Custom Tasks: New tasks are added by creating .axl files in the .aspect/ directory.
    • Evaluation: AXL files are evaluated by the Rust Starlark interpreter, ensuring type errors are caught at parse time.

    This allows you to extend the CLI with custom commands that can shell out, read/write files, query the build graph, and subscribe to Bazel's Build Event Stream.

  6. Use the Bazel invocation API for tasks

    main

    The Aspect CLI provides a high-level, composable API for driving Bazel from tasks, replacing manual spawn/drain logic. Instead of using command strings or enums, you should use dedicated constructors for each command. This API handles the full lifecycle: setup, spawning, event handling, waiting, retrying, and concluding.

    Key characteristics of the API:

    • Separate constructors: Use specific functions like bazel.build(ctx) or bazel.test(ctx) rather than passing a command string.
    • Pull-based event consumption: The callsite loops over an event stream directly. There are no callbacks or manual pumps required at the callsite; the runtime manages the cadence so heartbeats fire even during Bazel quiet periods.
    • Explicit retry logic: Retry decisions are made explicitly at the callsite using build.should_retry(...) rather than being hidden inside a wait function.
    • Integration with BazelTrait: The API is a plain AXL library that consumes the existing BazelTrait, allowing features to continue extending builds through that trait.
    # Conceptual usage pattern
    # build.axl / test.axl flow:
    # setup -> spawn -> events -> wait -> retry -> conclude
    
    # Example of explicit retry at callsite:
    if build.should_retry(...):
        # handle retry
  7. Understand how aspect <task> reports results in CI

    main

    When running aspect <task> in a CI environment, the CLI automatically posts results to several surfaces to improve visibility:

    • PR task summary comment: A single consolidated comment summarizing every task in the pipeline.
    • GitHub Status Checks: One check is created per aspect <task> invocation, with the check name determined by the --task:name flag.
    • Buildkite annotations: When running on Buildkite, one annotation is created per aspect <task> invocation, rendered at the top of the build page.
  8. Use task hooks to react to task outcomes

    main

    Use ctx.hooks to register functions that run at specific points in the task lifecycle. Unlike ctx.defer, hooks can access the TaskConclusion to make decisions based on whether the task succeeded or failed.

    • ctx.hooks.pre_task(fn): Runs fn(ctx) before the task body. If a pre-task hook exits or fails, the body never runs.
    • ctx.hooks.post_task(fn): Runs fn(ctx, conclusion) after the body, regardless of how it ended (return, exit, or error). The conclusion object contains exit_code, text, flagged, and message.

    Execution Order:

    1. Pre-task hooks (in registration order)
    2. Task body
    3. Post-task hooks (in registration order)
    4. ctx.defer callbacks (in reverse registration order)
    5. Bookend
    def _keep_scratch_on_failure(ctx: TaskContext, outcome: TaskConclusion):
        if outcome.exit_code == 0:
            ctx.std.fs.remove_dir_all(work)
        else:
            print("kept " + work + " for inspection")
    
    ctx.hooks.post_task(_keep_scratch_on_failure)
  9. Handle asynchronous operations with Futures

    main

    Several AXL APIs return Future objects to support non-blocking operations. To use the result of an asynchronous operation, call .block() on the Future.

    Supported Async APIs:

    • ctx.http().get(...) $\rightarrow$ Future[HttpResponse]
    • ctx.http().download(...) $\rightarrow$ Future
    • ctx.bazel.build(...) $\rightarrow$ Build object (provides .wait() and .try_wait()).

    This model allows tasks to issue multiple requests concurrently before blocking to wait for the results.

  10. Handle BES iterators and retries in tasks

    main

    When working with Build Event Stream (BES) iterators in Aspect CLI tasks, keep the following rules in mind:

    • Pre-spawn subscription: Always create the iterator handle via bazel.build_events.iterator() before invoking ctx.bazel.build(...) and pass it in the build_events list.
    • Single-use handles: Iterator handles are single-use. If your task logic requires retrying a Bazel invocation, you must create a fresh iterator() handle for every new attempt.
  11. Use traits as interfaces, not data carriers

    main

    A trait should act as a bridge between a task and its extending features. It should expose hooks or callbacks that features inject behavior into.

    Do not use a trait to ferry plain data between phases. Instead, carry data through the feature's own closure state or a record, keeping the trait's surface strictly for the callable interface.