aspect delivery builds and dispatches Bazel targets (services, binaries, containers) to their destination. Its defining feature is content-hash-based change detection: a target is only re-delivered if its build outputs have actually changed, not just because a commit landed on main.
On a monorepo with hundreds of services, this prevents cascade deploys. If a commit touches //backend/api:server but not //frontend/web:app, delivery re-deploys only the API service. The frontend deploy is a no-op — recorded as “already delivered” for this content, not skipped and not re-run.
Selective delivery in depth
Standard CI deploys trigger on commit SHA: every push to main re-deploys everything. At monorepo scale this means 200 services re-deploying when 1 changed.aspect delivery uses content hashes instead:
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 extracting digest hashes from a gRPC execution log. This gives a stable, hermetic hash of each target’s actual build outputs — not the source inputs, not the git SHA.
Phase 2: State query
deliveryd (a Unix-socket HTTP server started automatically on Aspect Workflows CI runners) stores delivery history as (label, digest) → delivery event tuples, keyed by commit SHA + task key. Before dispatching, the task queries deliveryd to check whether each (label, hash) pair has already been delivered. If yes, the target is skipped. If no, it’s a delivery candidate.
Phase 3: Dispatch
Surviving candidates download their runfiles, then bazel run each target in parallel (up to --max-parallelization goroutines, defaulting to hardware thread count). After each dispatch, deliveryd records the result.
Why content hashes beat git SHAs
Git SHAs capture “what committed?”, not “what changed in the outputs?”. Two commits that produce identical binaries (e.g., a docs-only change alongside a code change) get different SHAs. Content hashes capture the actual artifact identity, so identical outputs are never re-delivered.Configuration
Delivery mode
selective is the correct mode for production pipelines. always is useful for initial setup, debugging, or forced rollouts.
Resolving delivery targets
--query flag is a Bazel query expression. Any Bazel query syntax works: kind(...), attr(...), unions, intersections.
Forcing re-delivery
Dry run
Parallelism
Stamping and Bazel flags
Delivered binaries are stamped by default —--release-bazel-flag defaults to --stamp, so version-control info is embedded without any configuration. There are two separate flags for passing Bazel flags, and choosing the right one matters for change detection:
| Flag | Applies to | Use for |
|---|---|---|
--release-bazel-flag | the final delivery build only | --stamp, --workspace_status_command=<path>, and any flag — including a --config — that adds non-determinism to release artifacts |
--bazel-flag | every build phase, including the change-detection digest | only flags you’re certain don’t change outputs run-to-run — e.g. --jobs=100, --remote_cache=<uri> |
.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”] — 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.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.
Four DeliveryTrait hooks let you enrich, render, upload, and act on the manifest. The on-disk file and the CI artifact have separate renderers so you can ship JSON for tooling AND YAML/CSV for humans (or any other split) from the same run:
delivery_target(entry)— per-target enrichment (e.g. attach the OCI image digest your push rule wrote alongside the binary).render_manifest_file(manifest)— content of--manifest-file. Default: pretty-printed JSON.upload_manifest(manifest)— list of CI artifacts to upload. Default: one JSON artifact. Return multiple entries to split the manifest across files / formats; return[]to disable uploads.delivery_manifest(manifest)— end-of-task action (e.g. assemble an OCI layer in-task, post to an escrow registry, write an audit log).

