Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.aspect.build/llms.txt

Use this file to discover all available pages before exploring further.

This is the per-task migration guide for aspect delivery. See Migrating from legacy YAML-configured tasks for shared setup (CLI version pinning, the recommended two-step migration path, top-level workspaces:).
The legacy Rosetta delivery system used two YAML-configured tasks:
  • delivery_manifest — queried Bazel for deliverable targets, hashed their outputs, and recorded a manifest in Redis.
  • delivery — read the manifest from Redis and ran bazel run on each new target, signing completed deliveries to prevent repeats.
aspect delivery collapses both into a single command, shaped by three orthogonal flags:
  • --mode={selective,always} (default selective) — the delivery model. selective uses change detection: only runs candidates that haven’t already been delivered for the --task-key[:--salt] prefix. always skips change detection and treats every resolved target as a delivery candidate (closest analogue to the legacy “always deliver” model — --force-target is redundant).
  • --dry-run (default off) — print what would be delivered, don’t actually run anything.
  • --track-state={true,false} (default true) — whether to track delivery state across runs (record what’s been delivered, query before re-delivering). Required (true) when --mode=selective — selective delivery has no meaning without persisted state. Set to false outside Aspect Workflows runners (where the state backend isn’t yet available).
This migration only affects how delivery is triggered and tracked. The delivery targets themselves (bazel run-able rules tagged deliverable) do not need to change.
The flags compose:
CombinationBehaviorPrerequisites
--mode=selective (default)selective; runs candidates; records statestate backend, remote cache
--mode=selective --dry-runselective preview; records digests for future runs; no runstate backend, remote cache
--mode=alwaysruns every target; records digests + deliveriesstate backend, remote cache
--mode=always --dry-runalways preview; records digests; no runstate backend, remote cache
--mode=always --track-state=falseruns every target; nothing recorded; no digests computednone
--mode=always --dry-run --track-state=falsebest-effort preview without state tracking; nothing recorded; no runremote cache (optional — degrades to a digest-less list if missing)
--mode=selective --track-state=false (any --dry-run)invalid — selective delivery requires state
--mode=selective --track-state=false is rejected — selective delivery has no meaning without persisted state. For previews outside Aspect Workflows runners use --mode=always --dry-run --track-state=false; to actually run targets without state tracking use --mode=always --track-state=false. See Requirements below for the constraints on each prerequisite.

What determines “needs delivery”

Legacy YAML-configured tasks: a target was new if its Bazel output hash had never been recorded in Redis (SETNX). The salt could be customized via salt_envs. Aspect CLI: change detection considers a target new if the combination of its action digest (a stable hash of the target’s action graph) and the change-detection prefix has not been recorded by the state backend. The prefix is --task-key alone, or --task-key:--salt when both are provided.
Because the action digest is computed by the remote cache protocol — not by hashing local output files — it is stable across machines and rebuild attempts. Two runners producing bit-identical outputs derive the same digest, so change detection correctly skips the second delivery.
Always set --task-key explicitly. If you omit it, the CLI generates a random one per invocation, which shifts the change-detection scope every run and every target will be re-delivered every time. Pick a stable identifier for your pipeline (e.g. delivery, production-deploy) and reuse it across runs.

Requirements

The combination matrix above shows which prerequisites apply per invocation. Two notes worth highlighting:
  • The remote cache derives action digests via Bazel’s gRPC log. It’s required whenever digests are recorded or surfaced in --dry-run output — i.e. everywhere except --mode=always --track-state=false (which builds and runs targets directly without computing digests). For --mode=always --dry-run --track-state=false the cache is optional: if configured, digests appear in the preview; if not, the preview prints a NOTE and lists candidates without digests.
  • The state backend records what’s been delivered. It’s started automatically on Aspect Workflows runners. Pass --track-state=false outside Aspect Workflows runners.
The state backend is currently available only on Aspect Workflows CI runners. Leaving --track-state=true (the default) on other CI hosts or local machines fails at startup with ASPECT_WORKFLOWS_DELIVERY_API_ENDPOINT is not set. The delivery state backend must be running. Outside Aspect Workflows runners, switch to --mode=always: pair with --dry-run --track-state=false for a digest preview, or with --track-state=false alone to actually deliver every target. State-tracking support outside Aspect Workflows runners is planned for a future release.

What changed

AreaYAML-configured tasksAspect CLI tasks
Invocationrosetta run delivery_manifest then rosetta run delivery (two-step pipeline reading/writing a manifest in Redis)aspect delivery (single command)
Targets (query)deliverable: '<query>' (query expression)--query '<query>' — e.g. aspect delivery --query 'attr("tags", "deliverable", //...)'
Targets (label list)deliverable: [//foo, //bar]pass labels positionally — e.g. aspect delivery //foo //bar
Stamp / Bazel flagsstamp_flags: [<flags>] (any Bazel flags to apply during delivery)Repeat --bazel-flag=<flag> for each entry — e.g. --bazel-flag=--config=release. --stamp is added automatically when no --bazel-flag is set; passing any --bazel-flag drops that default, so re-introduce stamping either by passing --bazel-flag=--stamp explicitly or by folding --stamp into a .bazelrc config that one of your --bazel-flag=--config=<name> flags activates.
Saltsalt_envs: [A, B] (list of env var names; values concatenated automatically)--salt "$A:$B" — compose the string yourself and pass a single value. Appended to --task-key to scope change detection.
Dry-run / manifest-onlymanifest_only: true--dry-run
Forced re-delivery (per-rule)only_on_change: false on a rule (disable change detection for every target the rule produces)--force-target=//foo:bar per target (granularity shifts from rule → specific label), or --mode=always to disable change detection for every target in the invocation
Forced re-delivery (env var)ASPECT_WORKFLOWS_DELIVERY_TARGETS=//foo,//bar (comma-separated)ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS="//foo //bar" (whitespace-separated) — merged with any --force-target=... flags. Repeating --force-target=//foo --force-target=//bar works too. Each label must already be in the resolved delivery set (from --query / positional args): force-target only flips the change-detection skip; unmatched labels hard-fail at startup.
Branch filtercondition: { branches: [...] }run aspect delivery only from the relevant CI branch step
Tag filtercondition: { tags: [...] }run aspect delivery only from the relevant CI tag step
Limit querylimit_querynot needed — scope the --query expression directly
State storeredis_url / redis_use_tlsnot applicable (the state backend replaces Redis)
Triggerauto_deliver: truenot applicable — calling aspect delivery is itself the trigger
Workspacesworkspaces: top-level listnot applicable — cd <dir> in your CI config and run aspect delivery from there

Examples

The CI examples below omit --commit-sha and --build-url: both are auto-detected from CI environment variables (GITHUB_SHA / BUILDKITE_COMMIT / CIRCLE_SHA1 / CI_COMMIT_SHA for the SHA; the corresponding run/build URL for the build URL). Pass them explicitly only when you need to override the detected values — for instance, if your CI host isn’t recognized or you want to attribute the delivery to a different commit. On GitHub Actions pull_request events, detection picks the PR-head SHA rather than the synthetic merge SHA in GITHUB_SHA. On GitHub Actions, the build URL is upgraded from the run page (/actions/runs/<id>) to the specific job page (/actions/runs/<id>/job/<job_id>) when the Aspect Workflows GitHub App is installed and the ASPECT_API_TOKEN in use has a role granting actions: read (e.g. GitHub CI or GitHub Token: actions — see Token roles and GitHub scopes). If not, the run URL is used.

Query-based delivery on main

Before.aspect/workflows/config.yaml:
tasks:
  - delivery:
      auto_deliver: true
      rules:
        - deliverable: 'attr("tags", "\\bdeliverable\\b", //...)'
          condition:
            branches: [main]
After — CI configuration:
on:
  push:
    branches: [main]

jobs:
  deliver:
    runs-on: aspect-workflows
    permissions:
      id-token: write   # ArtifactUpload uses the runner's OIDC token to call the GitHub Actions artifact API
    env:
      ASPECT_API_TOKEN: ${{ secrets.ASPECT_API_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - name: Deliver
        run: |
          aspect delivery \
            --query 'attr("tags", "deliverable", //...)' \
            --task-key delivery

Multi-condition rules (branches and tags)

Before.aspect/workflows/config.yaml:
tasks:
  - delivery:
      auto_deliver: true
      rules:
        - deliverable: 'set(//services/...)'
          condition:
            branches: [main, 'hotfix/.*']
        - deliverable: [//tools:release]
          condition:
            tags: ['v[0-9]+\.[0-9]+\.[0-9]+']
After — CI configuration:
on:
  push:
    branches: [main, 'hotfix/**']
    tags: ['v[0-9]+.[0-9]+.[0-9]+']

jobs:
  deliver:
    runs-on: aspect-workflows
    permissions:
      id-token: write
    env:
      ASPECT_API_TOKEN: ${{ secrets.ASPECT_API_TOKEN }}
    steps:
      - uses: actions/checkout@v4

      - name: Deliver services
        if: github.ref_type == 'branch'
        run: |
          aspect delivery \
            --query 'set(//services/...)' \
            --task-key delivery

      - name: Deliver release
        if: github.ref_type == 'tag'
        run: |
          aspect delivery \
            //tools:release \
            --task-key delivery

Dry-run / manifest-only mode

Use --dry-run to preview what would be delivered without actually invoking any targets or recording state. This is useful when porting an existing config — run it on a representative branch to confirm the new command selects the same targets the legacy manifest did.
Before.aspect/workflows/config.yaml:
tasks:
  - delivery:
      manifest_only: true
      rules:
        - condition:
            branches: [main]
After — CI configuration:
on:
  push:
    branches: [main]

jobs:
  deliver:
    runs-on: aspect-workflows
    permissions:
      id-token: write
    env:
      ASPECT_API_TOKEN: ${{ secrets.ASPECT_API_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - name: Deliver (dry run)
        run: |
          aspect delivery \
            --query 'attr("tags", "deliverable", //...)' \
            --task-key delivery \
            --dry-run

Always deliver (--mode=always)

--mode=always disables change detection entirely and runs every resolved target. It’s the closest analogue to the legacy only_on_change: false behavior. Pair with --track-state=true (the default) on CI to record digests and deliveries — that way a subsequent --mode=selective run sees the up-to-date state. Pair with --track-state=false outside Aspect Workflows runners (local development, non-Aspect-Workflows CI) to skip both state tracking and the remote-cache requirement entirely. Common use cases:
  • Backfills and cache-invalidation events in CI, where every matching target should re-deliver regardless of change-detection state. Equivalent to listing every target in --force-target=… but in one flag, and the deliveries are still recorded:
    aspect delivery \
      --query 'attr("tags", "deliverable", //...)' \
      --task-key delivery-backfill \
      --mode=always
    
  • Local testing of forced-delivery flows. Preview isn’t enough; you want to actually invoke each target on your machine. No remote cache, no state tracking:
    aspect delivery \
      --query 'attr("tags", "deliverable", //tools/...)' \
      --task-key delivery-dev \
      --mode=always \
      --track-state=false
    
  • Adopting aspect delivery before a remote cache is provisioned. Run with --mode=always --track-state=false initially, then switch to --mode=selective once a remote cache and a state backend are wired up.
--mode=always runs every resolved target on every invocation. There is no skip path — if you point it at hundreds of targets you will deliver hundreds of targets, every time. Scope your --query carefully and be deliberate about when to invoke this mode in CI.

Break-glass forced re-delivery

Forced re-delivery bypasses change detection and re-delivers the target(s) even if their action digests have already been recorded. Use sparingly — typically only when a previous delivery partially succeeded and needs to be retried.
There are two break-glass paths: force a specific target (or set of targets), or force everything in scope.

Force everything (--mode=always)

When you want every resolved target re-delivered — backfills, cache-invalidation events, post-incident catch-up — pass --mode=always instead of enumerating each label. It skips change detection entirely while still recording the resulting deliveries (so the next --mode=selective run sees them):
aspect delivery \
  --query 'attr("tags", "deliverable", //...)' \
  --task-key delivery-backfill \
  --mode=always
See Always deliver above for the full discussion of --mode=always.

Force specific targets (parameterized CI job)

The legacy ASPECT_WORKFLOWS_DELIVERY_TARGETS env var let users pick the target list at job-trigger time from a CI parameter field. The new aspect delivery accepts the same pattern via ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS (whitespace-separated labels), which is merged with any --force-target flags. Wire your CI’s parameter input straight to that env var:
on:
  workflow_dispatch:
    inputs:
      force_targets:
        description: "Bazel targets to force-deliver (space-separated). Leave empty for normal selective delivery."
        required: false
        default: ""

jobs:
  deliver:
    runs-on: aspect-workflows
    permissions:
      id-token: write
    env:
      ASPECT_API_TOKEN: ${{ secrets.ASPECT_API_TOKEN }}
      ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS: ${{ inputs.force_targets }}
    steps:
      - uses: actions/checkout@v4
      - name: Deliver
        run: |
          aspect delivery \
            --query 'attr("tags", "deliverable", //...)' \
            --task-key delivery
To trigger a forced re-delivery, run the CI job from your provider’s UI and fill in the parameter field. An empty value falls through to normal selective delivery.
--force-target and ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS are merged, so you can hard-code always-forced labels in --force-target=... and still let users append more from the CI parameter field at trigger time.
Force-target only flips the change-detection skip for labels already in the resolved delivery set (from --query or positional args). It does not add new labels. If a label passed via --force-target or ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS isn’t present in the resolved set, aspect delivery hard-fails at startup so typos and stale labels are caught early. Update your --query or positional targets to include the label, or fix the typo.

Custom stamp flags

Before.aspect/workflows/config.yaml:
tasks:
  - delivery:
      stamp_flags:
        - --config=release
After — set once in .aspect/config.axl so every aspect delivery invocation picks it up:
.aspect/config.axl
def config(ctx: ConfigContext):
    ctx.tasks["delivery"].args.bazel_flags = ["--config=release"]
Or pass per invocation on the CLI when different delivery jobs need different flags (e.g. a release-only aspect delivery step alongside a default one):
jobs:
  deliver:
    runs-on: aspect-workflows
    permissions:
      id-token: write
    env:
      ASPECT_API_TOKEN: ${{ secrets.ASPECT_API_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - name: Deliver
        run: |
          aspect delivery \
            --query 'attr("tags", "deliverable", //...)' \
            --task-key delivery \
            --bazel-flag=--config=release
Roll --stamp into the release config in your .bazelrc rather than passing it alongside --config=release on every invocation:
.bazelrc
common:release --stamp
# ...other release-specific flags
That way --bazel-flag=--config=release is enough on its own — anywhere you’d want stamping (delivery, release builds, etc.) gets it via the same config, and there’s only one place to maintain the list.

Salted change detection

The legacy salt_envs list mixed environment variable values into the Redis state key so that changing any of them would re-trigger delivery. The new --salt flag serves the same purpose — pass the concatenated values yourself.
Changing the salt invalidates all prior change-detection state for the given --task-key. Every target will be re-delivered on the next run. Pick salt sources that change only when you actually want a full re-delivery (e.g., a config version, not the current timestamp).
Before.aspect/workflows/config.yaml:
tasks:
  - delivery:
      auto_deliver: true
      salt_envs:
        - DEPLOY_ENV
        - FEATURE_FLAG_VERSION
      rules:
        - deliverable: 'attr("tags", "deliverable", //...)'
          condition:
            branches: [main]
After — CI configuration:
on:
  push:
    branches: [main]

jobs:
  deliver:
    runs-on: aspect-workflows
    permissions:
      id-token: write
    env:
      ASPECT_API_TOKEN: ${{ secrets.ASPECT_API_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - name: Deliver
        env:
          DEPLOY_ENV: ${{ vars.DEPLOY_ENV }}
          FEATURE_FLAG_VERSION: ${{ vars.FEATURE_FLAG_VERSION }}
        run: |
          aspect delivery \
            --query 'attr("tags", "deliverable", //...)' \
            --task-key delivery \
            --salt="$DEPLOY_ENV:$FEATURE_FLAG_VERSION"

Preview without state tracking (--mode=always --dry-run --track-state=false)

For environments where the state backend isn’t available — local development machines, non-Aspect-Workflows CI, or anywhere ASPECT_WORKFLOWS_DELIVERY_API_ENDPOINT is not set — combine --mode=always with --dry-run --track-state=false. Nothing is recorded or queried, and no targets are run.
aspect delivery \
  --query 'attr("tags", "deliverable", //...)' \
  --commit-sha="$(git rev-parse HEAD)" \
  --task-key delivery \
  --mode=always \
  --dry-run \
  --track-state=false
Every candidate appears as DRY-RUN, scoped by the same --task-key[:--salt] change-detection prefix you’d use in CI. If a remote cache is configured, action digests are computed and shown alongside each candidate — useful for verifying digest stability or sanity-checking your setup. If no remote cache is configured, the preview prints a NOTE and falls back to listing candidates without digests; everything else still works. The other combination tolerant of a missing remote cache is --mode=always --track-state=false (which actually runs targets, also without computing digests); every other mode needs digests for correctness.

Key behavioral differences

AreaYAML-configured tasksAspect CLI tasks
PipelineTwo tasks (manifest → delivery)Single command
State storeRedisBuilt-in state backend on Aspect Workflows runners
Hash sourceBazel output file hashRemote cache action digest
Branch/tag conditionsDeclared in .aspect/workflows/config.yamlHandled by CI script
ParallelismSequential by default--max-parallelization flag
Forced deliveryASPECT_WORKFLOWS_DELIVERY_TARGETS env var or only_on_change: false--force-target flag, ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS env var, or --mode=always