# Supply-chain pinning (/docs/supply-chain)



Actio freezes mutable action and image refs to immutable digests at build time, and
ships a `pins` management surface for auditing and updating those locks — all without
introducing a foreign runtime. For action dependencies, the `pins` surface now serves
as a **legacy bridge** while GitHub's native workflow `dependencies:` lock model rolls
out.

## Automatic pinning at build time [#automatic-pinning-at-build-time]

`actio build` resolves every mutable `uses:` ref to an immutable commit SHA and every
`docker://image:tag` to a digest, keeping the human-readable tag as a trailing
comment. The emitted workflow becomes a **lockfile** — the SHA is what runs, the
comment is what you read, and [`actio check`](/docs/cli) stops the two drifting.

```yaml title="in.actio.yml"
- uses: pnpm/action-setup@v2
- uses: docker://alpine:3.18
```

```yaml title=".github/workflows/out.yml"
- uses: pnpm/action-setup@d648c2dd069001a242c621c8306af467f150e99d # v2
- uses: docker://alpine@sha256:51b67269f354137895d43f3b3d810bfacd3945438e94dc5ac55fdac340352f48 # 3.18
```

Resolved digests are cached in `actio.lock`, so repeat builds are deterministic and
`--offline` works without touching the network; re-pinning an already-pinned ref is a
no-op. Pinning is **on by default** for third-party actions and `docker://` images;
first-party `actions/*` and `github/*` refs are left on their tag. Tune it with the
[`pin` config block](/docs/configuration#pinning) or the
`--pin`/`--no-pin`/`--pin-github`/`--offline` flags.

Only Docker Hub registry images auto-resolve. A `docker://` image on an **unsupported
registry host** — `ghcr.io`, a private/internal registry, anything with a host
segment we can't auto-resolve — is **left on its tag** with a
`pin-unresolvable-registry` warning rather than hard-failing, so a private registry
never becomes an exit-`1` cliff while you're still told what wasn't locked. That skip
is narrow: a failure to resolve on a registry we *do* support (a Docker Hub auth
error, missing tag, rate-limit `429`, or `5xx`) is a **real failure and hard-fails**
(exit `1`), never silently shipped unpinned. `--offline` likewise stays
**fail-closed** (exit `2`) when the lock can't satisfy a ref.

### Pinning config [#pinning-config]

The [`pin` config key](/docs/configuration#pinning) accepts `"all"`, `"off"`, or a
`PinConfig` object for per-class control:

<AutoTypeTable path="../packages/core/src/config.ts" name="PinConfig" />

## The `pins` command [#the-pins-command]

```bash
# verify pin state
npx actio-cli pins check .github/actio/ci.actio.yml

# mechanical pin rewrite only (no build/config/custom-pass execution)
npx actio-cli pins update .github/actio/ci.actio.yml --no-exec --delta-out .actio/pins-delta.json

# privileged apply of a precomputed delta with strict allowlist checks
npx actio-cli pins apply --constrained .actio/pins-delta.json
```

## Exit codes [#exit-codes]

Exit codes are security-significant:

| Code | Meaning                                                           |
| ---- | ----------------------------------------------------------------- |
| `0`  | Clean.                                                            |
| `1`  | Resolvable drift (for example, a tag moved).                      |
| `2`  | Hard failure (including integrity mismatch); **never auto-heal**. |

`pins apply --constrained` only accepts a single-action, three-artifact delta shape:

1. source `.actio.yml` `uses: owner/repo@ref` bump,
2. matching `actio.lock` digest update,
3. matching generated `uses: owner/repo@<digest> # <ref>` substitution.

Anything outside that shape is rejected with exit `2`.

## Dependabot reconcile (safe two-phase pattern) [#dependabot-reconcile-safe-two-phase-pattern]

Use a two-phase workflow split:

<Steps>
  <Step>
    ### Phase 1 — `pull_request`, read-only token [#phase-1--pull_request-read-only-token]

    Run `pins update --no-exec` on the PR head and upload the delta artifact.
  </Step>

  <Step>
    ### Phase 2 — `workflow_run`, write token [#phase-2--workflow_run-write-token]

    Download the trusted artifact and run `pins apply --constrained`; never run `build`
    or `pins update` in this privileged phase.
  </Step>
</Steps>

This avoids executing PR-controlled config/passes in a write-privileged context
while still keeping source, lockfile, and generated pins aligned.

## Relationship to GitHub native `dependencies:` [#relationship-to-github-native-dependencies]

Actio has two action-locking modes selected by target capability:

1. **Native preview target (`github-actions-native-dependencies-preview`):** Actio
   resolves `uses:` actions to immutable SHAs + integrity and emits a top-level
   `dependencies:` block in generated workflows.
2. **Legacy target (`legacy`, default):** Actio keeps using `actio.lock` + `pins`
   (`check` / `update --no-exec` / `apply --constrained`) as a transitional bridge.

Why both exist:

* Native `dependencies:` is the long-term destination because runner-side
  pre-execution verification is stronger than compile-time-only pinning.
* `actio.lock` remains for environments that don't yet support native workflow
  dependencies.

Set the target in config (or via `--target`) to choose behavior per repository.


## Sitemap

Browse the full documentation: [Markdown sitemap](https://austenstone.github.io/actio/sitemap.md) · [XML sitemap](https://austenstone.github.io/actio/sitemap.xml)