aspect delivery builds and dispatches Bazel targets (services, binaries, containers) to their destination. It uses content-hash change detection: a target is re-delivered only when its build outputs changed, not because a commit landed on main.
If a commit changes the outputs of //backend/api:server but not //frontend/web:app, delivery re-deploys only the API service; the frontend is already delivered for that content.
The default
—track-state=true records delivery state in deliveryd, which runs on Aspect Workflows runners. Off a Workflows runner, run with —mode=always —track-state=false, adding —dry-run for a preview. —mode=selective can’t run there.Selective delivery in depth
A deploy keyed on the commit SHA re-deploys everything on every push to main.aspect delivery keys on content hashes instead, in phases:
Phase 1: Hash extraction
A custom hashsum Bazel aspect runs alongside the normal build. It produces a per-target action digest by re-running the build with --experimental_remote_require_cached and reading the digests from a gRPC log. The result is a stable hash of each target’s build outputs, not of its source inputs or the commit.
Phase 2: State query
deliveryd, which runs on Aspect Workflows CI runners, stores delivery history keyed by --task:name (plus --salt), label and digest. Before dispatching, the task asks it whether each (label, digest) pair was already delivered. If it was, the target is skipped; if not, it’s a delivery candidate.
Phase 3: Fused download and dispatch
The remaining candidates download their runfiles and dispatch as one pipelined stream: each target runs as soon as its runfiles are on disk, so a slow target doesn’t hold up ones that are ready. Up to --max-parallelization deliveries run at once (default 1), and deliveryd records each result.
Content hashes, not commit SHAs
A commit SHA says what was committed, not what changed in the outputs. Two commits that produce identical binaries, such as a README edit, have different SHAs but the same content hash, so identical outputs aren’t re-delivered.Uncacheable actions fail the task
Change detection needs a cache entry for every action in a deliverable’s dependency graph in order to compute its digest. If any action in that graph is uncacheable, typically a non-hermetic genrule (one that shells out togit or embeds a timestamp) or a stamped action, the digest can’t be computed.
By default aspect delivery treats this as a hard failure: the task exits non-zero and prints the offending target(s), the mnemonic(s) of the uncacheable action(s), and how to resolve it:
- Make the action deterministic and cacheable (remove the
git/datecall, dropno-cachetags, etc.). - Tag it
no-remoteorlocalso Bazel runs it locally on every invocation and never blocks the digest lookup on a missing remote cache entry.
--warn-unresolved-digest:
WARN, leaves them undelivered, and exits zero. Fixing the uncacheable action is better: with the warning, those targets never deliver.
Configuration
Delivery mode
selective for production pipelines. always suits initial setup, debugging and forced rollouts. --mode=selective needs delivery state, so it can’t be combined with --track-state=false.
Resolving delivery targets
--query takes any Bazel query expression, such as kind(...), attr(...), unions and intersections, and takes precedence over positional targets. With neither, nothing is delivered. Targets tagged manual are included when the expression matches them. --limit caps how many resolved targets deliver (default 0, no limit).
Forcing re-delivery
ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS environment variable, separated by whitespace, are added to the list.
Dry run
--dry-run=build also runs the final build, for the manifest’s on-disk paths and the runnable checks.
Parallelism
1, one target at a time. Raise it when your deploy targets tolerate concurrency; above 1, each target’s output goes to its own log.
Stamping and Bazel flags
Delivered binaries are stamped by default:--release-bazel-flag defaults to --stamp, so version-control info is embedded with no configuration. Which flag carries a Bazel flag matters for change detection:
Set them once in
.aspect/config.axl. A release --config typically enables stamping, so it goes in release_bazel_flags:
.aspect/config.axl
Assigning
release_bazel_flags replaces the default [“—stamp”], so include —stamp in your list if you still want stamping. To turn stamping off, set release_bazel_flags = [“—nostamp”] or pass —release-bazel-flag=—nostamp.Isolate the delivery Bazel server (Aspect Workflows)
On an Aspect Workflows runner, every task shares one warm Bazel server (the runner-derived--output_base), keyed by its analysis cache. The delivery task’s final build stamps and applies --config=release, which changes the build options, so on the shared server it discards the warm analysis cache the next build/test job on that runner would have reused (Build options … have changed, discarding analysis cache).
--workflows-runner-delivery-output-base-suffix=<suffix> moves that release build to its own persistent Bazel server, so its analysis stays warm across delivery runs without invalidating the cache the other jobs share. The change-detection build stays on the shared server; the checksum phase always uses its own.
.aspect/config.axl:
.aspect/config.axl
--workflows:runner-output-base-suffix for other tasks.
Delivery manifest
Every invocation produces a structured delivery manifest: a JSON record of every target’s outcome (ok / skip / warn / fail / pending), the resolved CI metadata, and any per-target enrichment your config.axl attached. It’s uploaded as a CI artifact (one labeled download link per file on the GitHub Status Check / Buildkite annotation); pass --manifest-file=<path> to also write it to disk. Customize the delivery manifest covers the schema and the DeliveryTrait hooks that enrich, render, upload, and act on it.
Options
It also takes
--bazel-startup-flag, --announce-bazel-*, --bes-backend and --bes-header, as described for aspect run.

