> ## Documentation Index
> Fetch the complete documentation index at: https://site.aspect.build/llms.txt
> Use this file to discover all available pages before exploring further.

# aspect delivery

> Migrate Rosetta delivery and delivery_manifest tasks to a single aspect delivery command with selective change detection in the Aspect CLI.

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:name[:--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 CI runners (where the state backend isn't yet available).

<Note>
  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.
</Note>

The flags compose:

| Combination                                              | Behavior                                                             | Prerequisites                                                       |
| -------------------------------------------------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------- |
| `--mode=selective` (default)                             | selective; runs candidates; records state                            | state backend, remote cache                                         |
| `--mode=selective --dry-run`                             | selective preview; records digests for future runs; no run           | state backend, remote cache                                         |
| `--mode=always`                                          | runs every target; records digests + deliveries                      | state backend, remote cache                                         |
| `--mode=always --dry-run`                                | always preview; records digests; no run                              | state backend, remote cache                                         |
| `--mode=always --track-state=false`                      | runs every target; nothing recorded; no digests computed             | none                                                                |
| `--mode=always --dry-run --track-state=false`            | best-effort preview without state tracking; nothing recorded; no run | remote cache (optional — degrades to a digest-less list if missing) |
| `--mode=selective --track-state=false` (any `--dry-run`) | **invalid** — selective delivery requires state                      | —                                                                   |

<Note>
  **`--mode=selective --track-state=false` is rejected** — selective delivery has no meaning without persisted state. For previews outside Aspect Workflows CI runners use `--mode=always --dry-run --track-state=false`; to actually run targets without state tracking use `--mode=always --track-state=false`. See [Requirements](#requirements) below for the constraints on each prerequisite.
</Note>

## 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:name` alone, or `--task:name:--salt` when both are provided.

<Info>
  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.
</Info>

<Warning>
  Always set `--task:name` 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.
</Warning>

## 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 CI runners. Pass `--track-state=false` outside Aspect Workflows CI runners.

<Warning>
  **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 CI 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 CI runners is planned for a future release.
</Warning>

## What changed

| Area                                                   | YAML-configured tasks                                                                                               | Aspect CLI tasks                                                                                                                                                                                                                                                                                                                                                                                        |
| ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Invocation                                             | `rosetta 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 flags (`--stamp`, `--workspace_status_command`)  | `stamp_flags: [<flags>]`                                                                                            | Use `--release-bazel-flag=<flag>` — **not** `--bazel-flag`. Release flags apply only to the final delivery build, so they don't shift the change-detection digest. `--release-bazel-flag` defaults to `--stamp`, so stamping is already on; add `--release-bazel-flag=--workspace_status_command=<path>` for a custom status script. See [Stamp and release-only flags](#stamp-and-release-only-flags). |
| Other Bazel flags                                      | —                                                                                                                   | Repeat `--bazel-flag=<flag>` for each entry — e.g. `--bazel-flag=--jobs=100`. These apply to **every** build phase, including change detection, so use them only for flags that don't change outputs. A release `--config` that enables stamping is non-deterministic — pass it as `--release-bazel-flag=--config=release` instead.                                                                     |
| Salt                                                   | `salt_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:name` to scope change detection.                                                                                                                                                                                                                                                                            |
| Dry-run / manifest-only                                | `manifest_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 filter                                          | `condition: { branches: [...] }`                                                                                    | run `aspect delivery` only from the relevant CI branch step                                                                                                                                                                                                                                                                                                                                             |
| Tag filter                                             | `condition: { tags: [...] }`                                                                                        | run `aspect delivery` only from the relevant CI tag step                                                                                                                                                                                                                                                                                                                                                |
| Limit query                                            | `limit_query`                                                                                                       | not needed — scope the `--query` expression directly                                                                                                                                                                                                                                                                                                                                                    |
| State store                                            | `redis_url` / `redis_use_tls`                                                                                       | not applicable (the state backend replaces Redis)                                                                                                                                                                                                                                                                                                                                                       |
| Trigger                                                | `auto_deliver: true`                                                                                                | not applicable — calling `aspect delivery` is itself the trigger                                                                                                                                                                                                                                                                                                                                        |
| [Workspaces](/docs/cli/migration#top-level-workspaces) | `workspaces:` top-level list                                                                                        | not applicable — `cd <dir>` in your CI config and run `aspect delivery` from there                                                                                                                                                                                                                                                                                                                      |

## Examples

<Note>
  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](/docs/cli/authentication-github#github-app-installation) is installed and the `ASPECT_API_TOKEN` in use has a role granting `actions: read` (e.g. `GitHub CI` or `GitHub Token: actions` — see [GitHub token roles and scopes](/docs/cli/authentication#github-token-roles-and-scopes)). If not, the run URL is used.
</Note>

### Query-based delivery on main

**Before** — `.aspect/workflows/config.yaml`:

```yaml theme={null}
tasks:
  - delivery:
      auto_deliver: true
      rules:
        - deliverable: 'attr("tags", "\\bdeliverable\\b", //...)'
          condition:
            branches: [main]
```

**After** — CI configuration:

<CodeGroup>
  ```yaml GitHub Actions theme={null}
  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
      steps:
        - uses: actions/checkout@v4
        - uses: aspect-build/setup-aspect@2306377a61c45954ab2df7c7311698b109364352 # v2026.26.9
          with:
            aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}
        - name: Deliver
          run: |
            aspect delivery \
              --query 'attr("tags", "deliverable", //...)' \
              --task:name delivery
  ```

  ```yaml Buildkite theme={null}
  steps:
    - label: ":rocket: Deliver"
      if: build.branch == "main"
      plugins:
        - aspect-build/setup-aspect#1d5768c5d28b72bf523b4722fc9177d2cc2d85c7: ~ # v2026.26.8
      command: |
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery
      agents:
        queue: aspect-default
  ```

  ```yaml GitLab theme={null}
  include:
    - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.26.8

  deliver:
    extends: .setup-aspect
    tags:
      - aspect-workflows
      - aspect-default
    rules:
      - if: '$CI_COMMIT_BRANCH == "main"'
    script:
      - |
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery
  # Set ASPECT_API_TOKEN as a masked CI/CD variable in Settings → CI/CD → Variables.
  ```

  ```yaml CircleCI theme={null}
  orbs:
    setup-aspect: aspect-build/setup-aspect@2026.26.10

  jobs:
    deliver:
      machine: true
      resource_class: circleci-org/aspect-default
      steps:
        - checkout
        - setup-aspect/setup
        - run:
            name: Deliver
            command: |
              aspect delivery \
                --query 'attr("tags", "deliverable", //...)' \
                --task:name delivery

  workflows:
    delivery:
      jobs:
        - deliver:
            filters:
              branches:
                only: main
  # Set ASPECT_API_TOKEN as a project environment variable or context secret.
  ```
</CodeGroup>

### Multi-condition rules (branches and tags)

**Before** — `.aspect/workflows/config.yaml`:

```yaml theme={null}
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:

<CodeGroup>
  ```yaml GitHub Actions theme={null}
  on:
    push:
      branches: [main, 'hotfix/**']
      tags: ['v[0-9]+.[0-9]+.[0-9]+']

  jobs:
    deliver:
      runs-on: aspect-workflows
      permissions:
        id-token: write
      steps:
        - uses: actions/checkout@v4
        - uses: aspect-build/setup-aspect@2306377a61c45954ab2df7c7311698b109364352 # v2026.26.9
          with:
            aspect-api-token: ${{ secrets.ASPECT_API_TOKEN }}

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

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

  ```yaml Buildkite theme={null}
  steps:
    - label: ":rocket: Deliver services (branch)"
      if: build.branch == "main" || build.branch =~ /^hotfix\//
      plugins:
        - aspect-build/setup-aspect#1d5768c5d28b72bf523b4722fc9177d2cc2d85c7: ~ # v2026.26.8
      command: |
        aspect delivery \
          --query 'set(//services/...)' \
          --task:name delivery
      agents:
        queue: aspect-default

    - label: ":rocket: Deliver release (tag)"
      if: build.tag =~ /^v[0-9]+\.[0-9]+\.[0-9]+$$/
      plugins:
        - aspect-build/setup-aspect#1d5768c5d28b72bf523b4722fc9177d2cc2d85c7: ~ # v2026.26.8
      command: |
        aspect delivery \
          //tools:release \
          --task:name delivery
      agents:
        queue: aspect-default
  ```

  ```yaml GitLab theme={null}
  include:
    - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.26.8

  deliver-branch:
    extends: .setup-aspect
    tags:
      - aspect-workflows
      - aspect-default
    rules:
      - if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH =~ /^hotfix\//'
    script:
      - |
        aspect delivery \
          --query 'set(//services/...)' \
          --task:name delivery

  deliver-tag:
    extends: .setup-aspect
    tags:
      - aspect-workflows
      - aspect-default
    rules:
      - if: '$CI_COMMIT_TAG =~ /^v[0-9]+\.[0-9]+\.[0-9]+$/'
    script:
      - |
        aspect delivery \
          //tools:release \
          --task:name delivery
  # Set ASPECT_API_TOKEN as a masked CI/CD variable in Settings → CI/CD → Variables.
  ```

  ```yaml CircleCI theme={null}
  version: 2.1

  orbs:
    setup-aspect: aspect-build/setup-aspect@2026.26.10

  jobs:
    deliver-branch:
      machine: true
      resource_class: circleci-org/aspect-default
      steps:
        - checkout
        - setup-aspect/setup
        - run:
            name: Deliver services
            command: |
              aspect delivery \
                --query 'set(//services/...)' \
                --task:name delivery

    deliver-tag:
      machine: true
      resource_class: circleci-org/aspect-default
      steps:
        - checkout
        - setup-aspect/setup
        - run:
            name: Deliver release
            command: |
              aspect delivery \
                //tools:release \
                --task:name delivery

  workflows:
    delivery:
      jobs:
        - deliver-branch:
            filters:
              branches:
                only: [main, /hotfix\/.*/]
              tags:
                ignore: /.*/
        - deliver-tag:
            filters:
              branches:
                ignore: /.*/
              tags:
                only: /^v[0-9]+\.[0-9]+\.[0-9]+$/
  # Set ASPECT_API_TOKEN as a project environment variable or context secret.
  ```
</CodeGroup>

### Dry-run / manifest-only mode

<Tip>
  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.
</Tip>

**Before** — `.aspect/workflows/config.yaml`:

```yaml theme={null}
tasks:
  - delivery:
      manifest_only: true
      rules:
        - condition:
            branches: [main]
```

**After** — CI configuration:

<CodeGroup>
  ```yaml GitHub Actions theme={null}
  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:name delivery \
              --dry-run
  ```

  ```yaml Buildkite theme={null}
  steps:
    - label: ":rocket: Deliver (dry run)"
      if: build.branch == "main"
      plugins:
        - aspect-build/setup-aspect#1d5768c5d28b72bf523b4722fc9177d2cc2d85c7: ~ # v2026.26.8
      command: |
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery \
          --dry-run
      agents:
        queue: aspect-default
  ```

  ```yaml GitLab theme={null}
  include:
    - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.26.8

  deliver:
    extends: .setup-aspect
    tags:
      - aspect-workflows
      - aspect-default
    rules:
      - if: '$CI_COMMIT_BRANCH == "main"'
    script:
      - |
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery \
          --dry-run
  # Set ASPECT_API_TOKEN as a masked CI/CD variable in Settings → CI/CD → Variables.
  ```

  ```yaml CircleCI theme={null}
  orbs:
    setup-aspect: aspect-build/setup-aspect@2026.26.10

  jobs:
    deliver:
      machine: true
      resource_class: circleci-org/aspect-default
      steps:
        - checkout
        - setup-aspect/setup
        - run:
            name: Deliver (dry run)
            command: |
              aspect delivery \
                --query 'attr("tags", "deliverable", //...)' \
                --task:name delivery \
                --dry-run

  workflows:
    delivery:
      jobs:
        - deliver:
            filters:
              branches:
                only: main
  # Set ASPECT_API_TOKEN as a project environment variable or context secret.
  ```
</CodeGroup>

### 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 CI 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:

  ```shell theme={null}
  aspect delivery \
    --query 'attr("tags", "deliverable", //...)' \
    --task:name 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:

  ```shell theme={null}
  aspect delivery \
    --query 'attr("tags", "deliverable", //tools/...)' \
    --task:name 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.

<Warning>
  `--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.
</Warning>

### Break-glass forced re-delivery

<Warning>
  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.
</Warning>

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):

```shell theme={null}
aspect delivery \
  --query 'attr("tags", "deliverable", //...)' \
  --task:name delivery-backfill \
  --mode=always
```

See [Always deliver](#always-deliver---modealways) 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:

<CodeGroup>
  ```yaml GitHub Actions theme={null}
  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:name delivery
  ```

  ```yaml Buildkite theme={null}
  steps:
    # Gate the input on UI-triggered builds so webhook/scheduled builds
    # aren't blocked. On non-UI builds the input step is skipped, the
    # meta-data fetch below returns the default empty string, and delivery
    # runs in normal selective mode.
    - key: force_targets_block
      if: build.source == "ui"
      input: ":aspect-build: Force-delivery parameters"
      fields:
        - text: FORCE_TARGETS
          hint: "Space-separated Bazel labels to force-deliver. Example: `//app/a:push_release //app/b:push_release`. Leave empty for normal selective delivery."
          required: false
          default: ""
          key: force_targets
          # Validate the input against Bazel's label grammar so a typo
          # is caught at job-trigger time rather than during the build.
          # Allows whitespace (including empty), one or more labels, and
          # the full label syntax — repository, package path, and target name.
          format: ^\s*$|^\s*(?:([@]{1,2}[\w.~-]*|[@]{0})\/\/(?:[\w.@-]+\/)*[\w.@-]*(?::(?:[\w\[\]!%@^"#\$&'()*+,;<=>?{|}~.-]+\/)*[\w\[\]!%@^"#\$&'()*+,;<=>?{|}~.-]*)?(?!\/)(?:\s+)?)+\s*$

    - label: ":rocket: Deliver"
      depends_on:
        # Per-dep allow_failure so a skipped input on non-UI builds doesn't
        # bypass real failures from other dependencies (add them above).
        - step: force_targets_block
          allow_failure: true
      plugins:
        - aspect-build/setup-aspect#1d5768c5d28b72bf523b4722fc9177d2cc2d85c7: ~ # v2026.26.8
      command: |
        export ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS=$(buildkite-agent meta-data get force_targets --default '')
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery
      agents:
        queue: aspect-default
  ```

  ```yaml GitLab theme={null}
  include:
    - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.26.8

  deliver:
    extends: .setup-aspect
    tags:
      - aspect-workflows
      - aspect-default
    rules:
      - if: $CI_PIPELINE_SOURCE == "web"     # GitLab "Run pipeline" UI exposes FORCE_TARGETS as an input
      - if: $CI_PIPELINE_SOURCE == "push"
    variables:
      ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS: $FORCE_TARGETS
    script:
      - |
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery
  # Declare FORCE_TARGETS as a pipeline variable in Settings → CI/CD → Variables
  # (Type: Variable, default empty) so it appears as an editable field on the
  # "Run pipeline" page.
  # Set ASPECT_API_TOKEN as a masked CI/CD variable in Settings → CI/CD → Variables.
  ```

  ```yaml CircleCI theme={null}
  version: 2.1

  orbs:
    setup-aspect: aspect-build/setup-aspect@2026.26.10

  parameters:
    force_targets:
      type: string
      default: ""
      description: "Bazel targets to force-deliver (space-separated). Leave empty for normal selective delivery."

  jobs:
    deliver:
      machine: true
      resource_class: circleci-org/aspect-default
      environment:
        ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS: << pipeline.parameters.force_targets >>
      steps:
        - checkout
        - setup-aspect/setup
        - run:
            name: Deliver
            command: |
              aspect delivery \
                --query 'attr("tags", "deliverable", //...)' \
                --task:name delivery

  workflows:
    delivery:
      jobs: [deliver]
  # Set ASPECT_API_TOKEN as a project environment variable or context secret.
  ```
</CodeGroup>

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.

<Tip>
  `--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.
</Tip>

<Warning>
  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.
</Warning>

### Stamp and release-only flags

`aspect delivery` has **two** flags for passing Bazel flags, and the difference matters for change detection:

| Flag                   | `config.axl` arg      | Applies to                                                          | Use for                                                                                                                                          |
| ---------------------- | --------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `--release-bazel-flag` | `release_bazel_flags` | the **final delivery build only** (phase 3)                         | anything that adds non-determinism to release artifacts — `--stamp`, `--workspace_status_command=<path>`, and any `--config` that turns those on |
| `--bazel-flag`         | `bazel_flags`         | **every** build phase, including the change-detection digest phases | only flags you're certain don't change outputs run-to-run — e.g. `--jobs=100`, `--remote_cache=<uri>`                                            |

<Warning>
  Put `--stamp`, `--workspace_status_command`, and any `--config` that enables them in `--release-bazel-flag` (`release_bazel_flags`), **not** `--bazel-flag` (`bazel_flags`).

  The change-detection digest is computed from the build phases that `--bazel-flag` feeds. Stamping injects volatile values (commit SHA, build time, workspace status) into those phases, so the digest shifts on **every commit** — change detection then sees every target as new and **re-delivers everything, every time**, defeating selective delivery. `--release-bazel-flag` applies only to the final delivery build, after change detection has run, so stamped artifacts stay stamped without polluting the digest.

  This catches people via `--config`: a release config such as `build:release --stamp --workspace_status_command=...` in `.bazelrc` pulls stamping in the moment it's activated. So `--config=release` typically belongs in `--release-bazel-flag` too — putting it in `--bazel-flag` re-introduces the exact problem. Only use `--bazel-flag` for a config you've confirmed adds no non-determinism.
</Warning>

The legacy `stamp_flags` list mapped to the delivery build, so it migrates to `release_bazel_flags`:

**Before** — `.aspect/workflows/config.yaml`:

```yaml theme={null}
tasks:
  - delivery:
      stamp_flags:
        - --stamp
        - --workspace_status_command=tools/bazel/workspace_status.sh
```

**After** — set once in `.aspect/config.axl` so every `aspect delivery` invocation picks it up:

```python .aspect/config.axl theme={null}
def config(ctx: ConfigContext):
    ctx.tasks["delivery"].args.release_bazel_flags = [
        "--stamp",
        "--workspace_status_command=tools/bazel/workspace_status.sh",
    ]
```

<Note>
  `release_bazel_flags` defaults to `["--stamp"]`, so delivered binaries are stamped out of the box — you only need to set it to add a `--workspace_status_command` or other release-only flags, or to opt out with `--release-bazel-flag=--nostamp`. Assigning a new list **replaces** the default, so include `--stamp` in your list if you still want stamping.
</Note>

A release `--config` usually pulls in stamping (e.g. `build:release --stamp --workspace_status_command=...`), so it belongs in `release_bazel_flags` — that applies it only to the final delivery build and keeps the volatile values out of the change-detection digest:

```python .aspect/config.axl theme={null}
def config(ctx: ConfigContext):
    ctx.tasks["delivery"].args.release_bazel_flags = ["--config=release"]
```

Reserve `bazel_flags` for flags you've confirmed don't change outputs run-to-run (e.g. `--jobs`, `--remote_cache`); those apply to every phase so change detection and the final build agree.

Or pass either flag per invocation on the CLI when different delivery jobs need different flags (e.g. a release-only `aspect delivery` step alongside a default one):

<CodeGroup>
  ```yaml GitHub Actions theme={null}
  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:name delivery \
              --release-bazel-flag=--config=release
  ```

  ```yaml Buildkite theme={null}
  steps:
    - label: ":rocket: Deliver"
      plugins:
        - aspect-build/setup-aspect#1d5768c5d28b72bf523b4722fc9177d2cc2d85c7: ~ # v2026.26.8
      command: |
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery \
          --release-bazel-flag=--config=release
      agents:
        queue: aspect-default
  ```

  ```yaml GitLab theme={null}
  include:
    - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.26.8

  deliver:
    extends: .setup-aspect
    tags:
      - aspect-workflows
      - aspect-default
    script:
      - |
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery \
          --release-bazel-flag=--config=release
  # Set ASPECT_API_TOKEN as a masked CI/CD variable in Settings → CI/CD → Variables.
  ```

  ```yaml CircleCI theme={null}
  orbs:
    setup-aspect: aspect-build/setup-aspect@2026.26.10

  jobs:
    deliver:
      machine: true
      resource_class: circleci-org/aspect-default
      steps:
        - checkout
        - setup-aspect/setup
        - run:
            name: Deliver
            command: |
              aspect delivery \
                --query 'attr("tags", "deliverable", //...)' \
                --task:name delivery \
                --release-bazel-flag=--config=release
  # Set ASPECT_API_TOKEN as a project environment variable or context secret.
  ```
</CodeGroup>

<Tip>
  If your `--workspace_status_command` only needs to run for stamped builds, you can also fold both into a `.bazelrc` config that a single `--release-bazel-flag=--config=<name>` activates:

  ```ini .bazelrc theme={null}
  common:release-stamp --stamp --workspace_status_command=tools/bazel/workspace_status.sh
  ```

  Then `--release-bazel-flag=--config=release-stamp` is enough — one place to maintain the list, and it stays on the release-only side so it never reaches the change-detection digest.
</Tip>

### 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.

<Note>
  Changing the salt invalidates **all** prior change-detection state for the given `--task:name`. 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).
</Note>

**Before** — `.aspect/workflows/config.yaml`:

```yaml theme={null}
tasks:
  - delivery:
      auto_deliver: true
      salt_envs:
        - DEPLOY_ENV
        - FEATURE_FLAG_VERSION
      rules:
        - deliverable: 'attr("tags", "deliverable", //...)'
          condition:
            branches: [main]
```

**After** — CI configuration:

<CodeGroup>
  ```yaml GitHub Actions theme={null}
  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:name delivery \
              --salt="$DEPLOY_ENV:$FEATURE_FLAG_VERSION"
  ```

  ```yaml Buildkite theme={null}
  steps:
    - label: ":rocket: Deliver"
      if: build.branch == "main"
      plugins:
        - aspect-build/setup-aspect#1d5768c5d28b72bf523b4722fc9177d2cc2d85c7: ~ # v2026.26.8
      command: |
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery \
          --salt="$$DEPLOY_ENV:$$FEATURE_FLAG_VERSION"
      agents:
        queue: aspect-default
  ```

  ```yaml GitLab theme={null}
  include:
    - component: $CI_SERVER_FQDN/aspect-build/setup-aspect-gitlab-component/setup@2026.26.8

  deliver:
    extends: .setup-aspect
    tags:
      - aspect-workflows
      - aspect-default
    rules:
      - if: '$CI_COMMIT_BRANCH == "main"'
    script:
      - |
        aspect delivery \
          --query 'attr("tags", "deliverable", //...)' \
          --task:name delivery \
          --salt="$DEPLOY_ENV:$FEATURE_FLAG_VERSION"
  # Set ASPECT_API_TOKEN as a masked CI/CD variable in Settings → CI/CD → Variables.
  ```

  ```yaml CircleCI theme={null}
  orbs:
    setup-aspect: aspect-build/setup-aspect@2026.26.10

  jobs:
    deliver:
      machine: true
      resource_class: circleci-org/aspect-default
      steps:
        - checkout
        - setup-aspect/setup
        - run:
            name: Deliver
            command: |
              aspect delivery \
                --query 'attr("tags", "deliverable", //...)' \
                --task:name delivery \
                --salt="$DEPLOY_ENV:$FEATURE_FLAG_VERSION"
  # Set ASPECT_API_TOKEN as a project environment variable or context secret.
  ```
</CodeGroup>

### 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.

```shell theme={null}
aspect delivery \
  --query 'attr("tags", "deliverable", //...)' \
  --commit-sha="$(git rev-parse HEAD)" \
  --task:name delivery \
  --mode=always \
  --dry-run \
  --track-state=false
```

Every candidate appears as `DRY-RUN`, scoped by the same `--task:name[:--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

| Area                  | YAML-configured tasks                                                  | Aspect CLI tasks                                                                             |
| --------------------- | ---------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| Pipeline              | Two tasks (manifest → delivery)                                        | Single command                                                                               |
| State store           | Redis                                                                  | Built-in state backend on Aspect Workflows CI runners                                        |
| Hash source           | Bazel output file hash                                                 | Remote cache action digest                                                                   |
| Branch/tag conditions | Declared in `.aspect/workflows/config.yaml`                            | Handled by CI script                                                                         |
| Parallelism           | Sequential by default                                                  | `--max-parallelization` flag                                                                 |
| Forced delivery       | `ASPECT_WORKFLOWS_DELIVERY_TARGETS` env var or `only_on_change: false` | `--force-target` flag, `ASPECT_WORKFLOWS_DELIVERY_FORCE_TARGETS` env var, or `--mode=always` |
