> Documentation version: 3. [HTML page](actions.html)

<a id="shared-actions"></a>

# Shared actions

roc-automation provides the pieces every repository needs identically.
Reference each one by full commit SHA, and upgrade that reference through a pull
request. A shared change never reaches a repository silently.

``` yaml
- uses: lukewilliamboswell/roc-automation/actions/<name>@REVIEWED_FULL_SHA
```

<a id="risk-action-boundaries"></a>

## Risks and controls for Action boundaries

A wrapper can evaluate candidate configuration with write credentials, or
silently select different utility bytes from the ones tested upstream. Keep
GitHub integration thin and authority explicit. Pin Action/workflow source to
reviewed full SHAs, provision declared utilities and companions, and restrict
repository tasks to read-only jobs. Privileged controllers consume validated
inert inputs instead. Test invalid inputs and native handoff paths. A pinned
wrapper fixes its source revision; its external downloads and any allowed
consumer configuration retain their own verification and trust requirements.

<a id="setup-blueprint"></a>

## ` actions/setup-blueprint `

Installs the verified ` blueprint ` binary that ` Blueprint.roc ` names, on x86-64
and arm64 Linux, Apple Silicon and Intel macOS, and x86-64 Windows. Nix is
installed only with ` install-nix: 'true' `, which Windows refuses. On Windows
and Intel macOS Blueprint runs without a provider, from fetched programs and
the tools the runner already has; a project that tests on Windows lists
` "x86_64-windows" ` in its ` Systems `. For a complete recipe, lock, cache
and utility handoff, follow [the Roc package adoption guide](pure-package.md#adopt-a-pure-package-without-nix).
The setup action builds nothing.

The executable cache is enabled by default. Its exact identity includes the
Blueprint release, runner system and reviewed SHA-256. Restored bytes are copied
into a private file and rehashed before installation; a verified hit makes no
GitHub Releases request. A version or digest change selects a new cache entry.
` cache-binary: false ` disables this cache. ` binary-cache-hit ` reports actual
verified reuse, rather than trusting the cache service’s hit flag.

This is separate from the opt-in ` cache: true ` cache of Blueprint’s
` v1/artifacts ` tool/package downloads. The existing ` cache-hit ` output describes
that artifact store. Neither cache selects a newer Blueprint automatically:
maintainers change its pin through a PR. Corrupt regular executable bytes are
refetched and verified; symlink or nonregular entries fail closed. GitHub caches
are immutable; delete a corrupt remote executable entry if it causes repeated
safe fallback downloads.

``` yaml
- uses: actions/checkout@REVIEWED_FULL_SHA
  with:
    persist-credentials: false
- uses: lukewilliamboswell/roc-automation/actions/setup-blueprint@REVIEWED_FULL_SHA
- run: blueprint run check
```

The Blueprint version needs no duplicate input. The action reads the roc-blueprint release from the
platform URL in the checked-out ` Blueprint.roc `, and it records the reviewed
SHA-256 of every binary of each release it installs. ` Blueprint.roc ` is
therefore the one place a repository names its Blueprint release, and a
workflow cannot disagree with it.

| Input | Meaning |
| --- | --- |
| ` install-nix ` | Defaults to ` false `. Enable for Nix-sourced tools and sandboxed builds. |
| ` cache-binary ` | Defaults to ` true `. Reuse only executable bytes that match the selected release, system and reviewed digest. |
| ` cache ` | Defaults to ` false `. Restore/save only Blueprint ` v1/artifacts ` bytes. |
| ` cache-workload ` | Required with caching: a logical tool-set name, distinct for jobs or matrix variants that provision different subsets. |
| ` version ` | The roc-blueprint release to install. Defaults to the release named by the platform URL in ` Blueprint.roc ` at the root of the workspace. Set it only in a job that has no checkout. |
| ` sha256-x86_64-linux ` | Reviewed SHA-256 of the binary for x86\_64 Linux runners. Overrides the recorded value. |
| ` sha256-aarch64-linux ` | Reviewed SHA-256 of the binary for arm64 Linux runners. Overrides the recorded value. |
| ` sha256-aarch64-darwin ` | Reviewed SHA-256 of the binary for Apple Silicon macOS runners. Overrides the recorded value. |
| ` sha256-x86_64-darwin ` | Reviewed SHA-256 of the binary for Intel macOS runners. Overrides the recorded value. |
| ` sha256-x86_64-windows ` | Reviewed SHA-256 of ` blueprint-x86_64-windows.exe ` for x86-64 Windows runners. Overrides the recorded value. |

Outputs are ` blueprint-path `, the absolute path of the installed binary,
` system `, the release system name chosen for the runner, and ` version `, the
release that was installed. ` sha256 `, ` bytes ` and ` install-seconds ` report the
verified bootstrap executable and its download/verification duration. ` cache-hit ` reports an exact cache hit, while
` cache-key ` and ` artifacts-directory ` identify the saved download bytes. The
cache action restores before tasks and saves after a successful job. Blueprint
rehashes restored bytes and rebuilds derived trees; hit status is not verification.

The recorded checksums live in ` actions/setup-blueprint/releases.json `, one
entry for each roc-blueprint release, copied from that release’s
` blueprint.sha256 ` file. A new roc-blueprint release is reviewed once, in the
pull request that adds its entry there, and reaches a repository when that
repository moves its reference to the action. The reference is a full commit
SHA, so the recorded checksums are pinned by the same review as the action’s
code.

A release the referenced revision does not record fails the step, unless the
caller supplies a ` sha256-<system> ` input for the runner. Use those inputs to
install a release ahead of the action, such as a release candidate. A system for
which no binary is released also fails.

The action downloads the binary for the runner’s system into a private
temporary file, compares its SHA-256 with the reviewed value, and only then
makes it executable and puts it on ` PATH `. A mismatch fails the step and
installs nothing. It takes no token and needs no cache account.
[The ` blueprint ` binary](index.md#the-blueprint-binary) states
what the checksum does and does not establish.

<a id="blueprint-tool"></a>

## ` actions/blueprint-tool `

Runs a repository Blueprint task that prints one persistent executable path.
Use it in read-only build jobs after ` setup-blueprint `. The task resolves the
step’s temporary ` PATH ` link with ` realpath ` before it ends; see
[the complete handoff example](pure-package.md#pass-an-executable-to-a-shared-action).

` task ` is required. ` blueprint-binary ` may select the installed executable;
otherwise the action uses ` blueprint ` on ` PATH `. ` label ` supplies descriptive
diagnostic text. With ` measure: 'true' `, ` binary ` names a host-side wrapper
that measures calls, while ` executable-path ` names the actual file. The wrapper
preserves arguments and the command’s exit status. ` output-directory ` optionally
adds output file counts and sizes. Python is used for this optional host-side
measurement, never for source preparation or rendering.

Outputs include ` binary `, ` executable-path `, actual ` sha256 ` and ` bytes `,
` provision-seconds `, ` metrics-path ` and ` measure-script `. The last permits
separate preparation/API measurements. Provisioning includes the repository
task’s verification/download work. Labels and reported digests do not replace
Blueprint’s reviewed lock or authenticate an arbitrary task’s chosen program.
Paths are valid in that job; a new runner restores artifacts and reruns the task.

<a id="install-the-nightly-compiler"></a>

## Install the nightly compiler

The package compiler is installed by the official
[` roc-lang/setup-roc `](https://github.com/roc-lang/setup-roc) action, not by an
action in this repository. Reference it by full commit SHA like any other.

``` yaml
- uses: roc-lang/setup-roc@REVIEWED_FULL_SHA
  with:
    version: nightly-new-compiler
    nightly-tag: ${{ needs.resolve.outputs.nightly-tag }}
```

It installs the compiler on ` PATH ` as ` roc `. With ` nightly-tag ` empty it
installs the latest release of ` roc-lang/nightlies `; with a tag it installs that
release.

A workflow with more than one job resolves the latest tag once, in its first
job, and passes it to every job through ` nightly-tag `, so that one run uses one
compiler:

``` yaml
- id: nightly
  env:
    GH_TOKEN: ${{ github.token }}
  run: echo "tag=$(gh api repos/roc-lang/nightlies/releases/latest --jq .tag_name)" >> "$GITHUB_OUTPUT"
```

<a id="actions-that-run-a-roc-script"></a>

## Actions that run a Roc script

` actions/nightly-report ` and ` actions/publish-release ` are Roc scripts in
roc-automation, ` scripts/nightly_report.roc ` and ` scripts/publish_release.roc `,
written as
[Write product-specific automation as Roc scripts](repository.md#write-automation-as-roc-scripts)
describes. Each runs from source in the calling job, from the action’s own copy
of roc-automation at the referenced commit. The action:

1. installs Nix, with the install action ` actions/setup-blueprint ` uses, at the
   same full commit SHA;

2. downloads the ` blueprint ` binary that roc-automation’s ` Blueprint.roc ` names
   and checks it against its recorded SHA-256, as
   [The ` blueprint ` binary](index.md#the-blueprint-binary)
   describes; and

3. runs its task, ` blueprint run nightly-report ` or
   ` blueprint run publish-release `, in that copy of roc-automation.

roc-automation’s ` Blueprint.lock ` at the referenced commit pins what the task
starts: ` roc-stable `, which runs the script, the platform the script is built
on, the ` gh ` CLI, which makes every request to GitHub, and coreutils. The full
SHA in the ` uses: ` line therefore fixes the script and everything under it.
Nothing comes from the calling repository: its ` Blueprint.roc ` is not read, and
the binary is installed in a directory of the action’s own and is not put on
` PATH `, so a ` blueprint ` the job set up earlier is left as it was.

The script is given the action’s inputs and the token in its environment, never
as arguments. ` gh ` reads the token from ` GH_TOKEN ` and sends requests to the
GitHub host it is configured for, which is GitHub.com unless the job sets
` GH_HOST `.

These actions need a runner Blueprint supports, which is x86-64 or arm64 Linux
or Apple Silicon macOS, and ` python3 ` for the installer, which GitHub’s hosted
runners provide. On a runner with an empty Nix store, installing Nix and
downloading the environment is most of the job’s time: the environment is about
a gigabyte once unpacked, so allow a minute or two. The script itself finishes
in seconds.

<a id="release-package"></a>

## The ` release-package ` utility

The utility behind the
[shared release workflow](workflows.md#the-shared-release-workflow-for-a-pure-package)
is not an action. It is ` scripts/release_package.roc `, built by the tooling
compiler and published as an executable in each tools release, like roc-docs,
for x86-64 and arm64 Linux, Apple Silicon and Intel macOS, and x86-64 Windows. Every
asset is named ` release-package-<system> `, so one recipe serves every system;
Blueprint adds ` .exe ` where Windows needs it. A repository’s ` Blueprint.roc `
fetches it by digest and names it as tasks, so the workflow and a maintainer
run the same bytes:

``` roc
Environment("release", [Programs([Fetch("release-package", Recipes.file("https://github.com/lukewilliamboswell/roc-automation/releases/download/REVIEWED_TOOLS_RELEASE/release-package-{system}"))]), Expect(["roc", "git", "gh", "tar", "unzip", "zip", "chmod", "touch", "env"])]),
Environment("release-test", [Programs([Fetch("release-package", Recipes.file("https://github.com/lukewilliamboswell/roc-automation/releases/download/REVIEWED_TOOLS_RELEASE/release-package-{system}"))]), Expect(["roc"])]),
Task("release-package", [Use("release"), Run(["release-package"])]),
Task("release-test", [Use("release-test"), Run(["release-package"])]),
```

` REVIEWED_TOOLS_RELEASE ` is the ` tools-<version> ` tag the repository reviewed,
as ` REVIEWED_FULL_SHA ` is the commit of an action; ` blueprint update ` locks its
digests.

``` sh
blueprint run release-test -- test-examples               # the working tree's examples against tests/spec.json
blueprint run release-test -- test-examples --update      # record what they print now; writes the specification when it is missing
blueprint run release-package -- check-version 0.14.0     # version sufficiency against the latest release; needs gh
blueprint run release-package -- build 0.14.0             # dist/: bundles, examples archive, API docs archive, notes
blueprint run release-test -- test-archive                # the archive in dist/ against the bundles in dist/
```

` build ` and ` check-version ` need Git, ` gh ` and the archive tools, and run on
Linux or macOS. ` test-examples ` and ` test-archive ` start nothing but the
compiler, which is why ` release-test ` expects only ` roc `: they run on every
system a repository releases for, Windows included. On Windows the archive is
read by the system’s own ` %SystemRoot%\System32\tar.exe `, by its full path,
because Windows has no ` unzip ` and the ` tar ` of Git Bash cannot read a ZIP.

<a id="release-package-platform"></a>

### A platform release

A platform repository releases its platform the same way. Its platform root
is ` platform/main.roc `, and its examples name it by relative path,
` pf: platform "../../platform/main.roc" `. The platform is then the primary
bundle: notes write its URL as ` {platform-url} `, ` roc docs ` documents it, and a
package at ` package/main.roc ` beside it is named ` <name>-package-<hash>.tar.zst `
and written ` {package-url} `. A local platform anywhere else, or a package field
that names ` platform/main.roc `, is refused.

A platform bundle holds what only its repository knows how to make: each
target’s host library and linker inputs, their licences and the bundle’s
inventory. So the repository builds the host and writes the bundle with its own
command, and ` build ` takes the directory it wrote:

``` sh
blueprint run release-test -- test-examples               # the examples against platform/, host outputs included
blueprint run bundle -- --output-dir build/platform       # the repository's own bundle command
blueprint run release-package -- build "$VERSION" --platform-bundle build/platform
blueprint run release-test -- test-archive                # on every system the platform supports
```

The directory must hold exactly one ` .tar.zst `, and that bundle must hold every
tracked ` .roc ` file of ` platform/ `; other members are expected. ` build ` copies
it into ` dist/ ` under its release name and continues as for a package: the
archived examples name ` platform "<release URL>" `, and ` test-archive ` serves the
bundle from a loopback address in place of that URL. ` release-package ` checks
the platform bundle and never runs the command that made it, so the host build
and the bundle’s contents remain the repository’s to review.

The package compiler is the ` roc ` on ` PATH `, or ` RP_COMPILER `. ` RP_REPOSITORY `
is the ` owner/name ` the bundle URLs name; it defaults to ` GITHUB_REPOSITORY `
and then to the ` origin ` remote. ` RP_DIRECTORY ` keeps the numbered command
records and output streams of every compiler and example command, the same
files the nightly runner preserves. The tools release names the compiler the
utility was built with; the lock fixes its bytes.

<a id="actionsrelease-compatibility"></a>

## ` actions/release-compatibility `

The shared read-only runner owns selection, archive checks, compiler diagnostics
and execution of a versioned published examples suite. The
` nightly-compatibility.yml ` reusable workflow adds the complete target matrix,
evidence aggregation and a separate issue reporter. See
[Shared published-release compatibility](nightly-compatibility.md#shared-published-release-compatibility) for the
canonical archive, caller, inputs, diagnostic artifacts and failure handling.

<a id="nightly-report"></a>

## ` actions/nightly-report `

Keeps the one tracking issue of a
[nightly compatibility](workflows.md#nightly-compatibility) workflow. It
checks out nothing from the calling repository and runs none of its code.

``` yaml
report:
  needs: test
  if: ${{ always() }}
  runs-on: ubuntu-latest
  permissions:
    issues: write
  steps:
    - uses: lukewilliamboswell/roc-automation/actions/nightly-report@REVIEWED_FULL_SHA
      with:
        outcome: ${{ needs.test.outputs.outcome }}
        nightly-tag: ${{ needs.test.outputs.nightly }}
        release-tag: ${{ needs.test.outputs.release }}
```

| Input | Default | Meaning |
| --- | --- | --- |
| ` outcome ` | Required | Outcome of the step that ran the examples |
| ` nightly-tag ` | Required | The Roc nightly the examples ran on, named in the issue text |
| ` release-tag ` | Required | The release whose examples ran, named in the issue text |
| ` title ` | ` The latest release fails on the latest Roc nightly ` | Exact title of the tracking issue |
| ` token ` | ` ${{ github.token }} ` | Token used to read and write issues |

Outputs are ` action `, which is ` opened `, ` commented `, ` closed ` or ` none `, and
` issue-number `, the issue that action concerned. ` issue-number ` is empty when
` action ` is ` none `.

The tracking issue is the open issue whose title equals ` title ` exactly and
whose author is a bot account. The action never comments on, closes or reopens
any other issue. An issue with the same title that a person opened is ignored,
as are a pull request and a closed issue.

| ` outcome ` | What the action does |
| --- | --- |
| ` success ` | Comments on the tracking issue and closes it. With no tracking issue open it does nothing, and ` action ` is ` none `. |
| ` failure ` | Comments on the tracking issue. With no tracking issue open it opens one. |
| Anything else | Fails the step with an error and reads or writes no issue. An empty value, ` skipped ` and ` cancelled ` all mean that the examples did not run to a verdict, which is a setup failure and not evidence about the release. |

The issue body and every comment name the release and the nightly and link to
the workflow run. Tags are limited to 100 letters, digits and ` . `, ` _ `, \`\`,
` / `, ` - ` characters, and a title to 256 characters with no control, format or
private-use characters. A malformed input, or an empty token, fails the step
before any request is made.

The action reads the open issues a page at a time. A listing it cannot read in
full, or a record in it that is not an issue record, fails the step: it never
opens an issue while it cannot tell whether one exists.

The calling job needs ` issues: write ` and no other permission. It has no
checkout step, and it passes the action only the step outcome and the two tags,
never text produced by the examples.
[Reporting](workflows.md#reporting) states what the calling workflow must
provide, and
[Actions that run a Roc script](#actions-that-run-a-roc-script)
what the action needs of the runner.

<a id="build-docs"></a>

## ` actions/build-docs `

Renders a repository’s AsciiDoc or Markdown sources as an HTML site and a PDF
manual using a precompiled ` roc-docs ` binary. It provides a built-in theme,
embedded fonts and Roc syntax highlighting. It can also place
compiler-generated API documentation under ` site/api `, and publish static
chapter Markdown with ` llms.txt ` discovery by default. Set ` markdown: "false" `
to publish HTML and PDF without those exports.

Install a reviewed renderer binary for the runner’s architecture before invoking
the action. Pass its path explicitly, set ` ROC_DOCS_BINARY `, or put ` roc-docs `
on ` PATH `. The action does not download or compile a renderer. The tools release
pipeline builds and tests Linux x86-64, Linux ARM64 and macOS ARM64 binaries;
a release must be published before downstream consumers can install those assets
using [the complete Blueprint adoption path](pure-package.md#adopt-a-pure-package-without-nix).

``` yaml
- id: docs
  uses: lukewilliamboswell/roc-automation/actions/build-docs@REVIEWED_FULL_SHA
  with:
    renderer-binary: /path/to/verified/roc-docs
    pdf-filename: my-project.pdf
    docs-version: main
    api-entrypoint: platform/main.roc
- uses: actions/upload-pages-artifact@REVIEWED_FULL_SHA
  with:
    path: ${{ steps.docs.outputs.site-directory }}
```

| Input | Default | Meaning |
| --- | --- | --- |
| ` docs-directory ` | ` docs ` | Source directory, relative to the repository root |
| ` entrypoint ` | ` index.adoc ` | The manual’s entrypoint, relative to ` docs-directory ` |
| ` output-directory ` | ` .docs-out ` | Generated output, relative to the repository root |
| ` pdf-filename ` | ` manual.pdf ` | Filename of the generated PDF |
| ` docs-version ` | ` unreleased ` | Label passed to the sources as the ` docs-version ` attribute |
| ` api-entrypoint ` | empty | Roc package or platform root for the generated API reference |
| ` roc-command ` | ` roc ` | Roc executable used when ` api-entrypoint ` is set |
| ` markdown ` | ` true ` | Publish chapter ` .md ` files, HTML discovery links and ` llms.txt `; set ` false ` to omit them |
| ` site-url ` | empty | HTTPS publication directory for PDF links, Markdown source links and absolute ` llms.txt ` chapter URLs |
| ` renderer-binary ` | environment or ` PATH ` | Path or executable name of the precompiled renderer |
| ` title `, ` author `, ` language ` | empty | Optional overrides for document metadata |
| ` pdf-image-dpi ` | ` 144 ` | PDF raster resolution limit, from 72 to 300; original website images are retained |

Outputs are ` site-directory ` and ` pdf-path `, both absolute paths.

The action needs only the renderer binary and the action runner’s
shell. The binary parses supported AsciiDoc with roc-parser 3.0.0, decodes
PNG assets and renders the publications in Roc. It needs no Python, Docker or Roc
compiler. Generating the optional API reference requires an explicitly supplied
Roc compiler. The same binary can build a repository locally with
` roc-docs build --workspace . `.

The renderer supports the manual’s documented source subset. Unsupported
constructs fail rather than silently disappearing. Mermaid diagrams render in process from native source blocks into standalone SVG
and PDF vector figures; see
[Write for the shared renderer](documentation.md#write-for-the-shared-renderer).
The PDF theme has outlines, hierarchical linked contents with printed page
numbers, chapter opening pages and chapter-aware continuation headers. PDF raster images default to a
144 DPI limit at their printed size; ` pdf-image-dpi ` adjusts that tradeoff.
Large documents or high resolutions can exceed the PDF package’s bounded
resource budget and fail before publication.

The action uses one shared renderer and built-in theme. It stages the full
publication and preserves the previous output if conversion, rendering or API
generation fails.

<a id="publish-release"></a>

## ` actions/publish-release `

Publishes one release from files an earlier job built and tested. It stages
them in a draft, reads every asset back and compares it with the tested file,
and only then publishes. It checks out nothing from the calling repository and
runs none of its code.

``` yaml
publish:
  needs: [build, test]
  if: github.event_name == 'workflow_dispatch'
  runs-on: ubuntu-24.04
  environment: release
  permissions:
    contents: write
  steps:
    - uses: actions/download-artifact@REVIEWED_FULL_SHA
      with:
        name: release-candidate
        path: candidate
    - uses: lukewilliamboswell/roc-automation/actions/publish-release@REVIEWED_FULL_SHA
      with:
        version: ${{ needs.build.outputs.release-version }}
        prerelease: ${{ needs.build.outputs.is-prerelease }}
        notes-file: candidate/release-notes.md
        assets: |
          candidate/dist/*
          candidate/docs/*.pdf
```

The job names the ` release ` environment, holds ` contents: write ` and nothing
else, and has no checkout step. Its only inputs are the artifact the build job
uploaded and the test jobs verified. What the assets are, how they are built and
tested, and any attestation of them stay in the repository’s earlier jobs.

| Input | Default | Meaning |
| --- | --- | --- |
| ` version ` | required | The release version, such as ` 0.12.0 ` or ` 0.12.0-rc1 ` |
| ` prerelease ` | required | Exactly ` true ` or ` false `. It must agree with whether ` version ` has a prerelease suffix. |
| ` notes-file ` | required | Path of the rendered release notes, which become the body of the release |
| ` assets ` | required | Globs of the tested files, one per line, relative to the workspace |
| ` tag ` | ` version ` | The tag to create: the version, or ` <name>-<version> ` in a repository that releases more than one thing |
| ` target ` | ` ${{ github.sha }} ` | Full SHA of the tested commit the tag points at |
| ` token ` | ` ${{ github.token }} ` | A token with ` contents: write ` on the repository |

Outputs are ` release-url `, the URL of the published release, and ` digests `, the
path of a listing of the published assets in the format ` sha256sum ` writes and
` sha256sum --check ` reads.

The tag is the bare version, without a ` v `. A repository that publishes a
second, independently versioned package gives that package’s releases a name
with the ` tag ` input, such as ` kit-0.1.0 `. The tag must end with the version,
so the two cannot disagree.

The action does the following, in this order, and stops at the first failure.

1. **Check the inputs.** The version, tag, target and ` prerelease ` value must be
   well formed and agree with each other. The notes file must be a non-empty
   regular file of UTF-8 text.

2. **Collect the assets.** Every glob must match at least one file. Every match
   must be a regular, non-empty file and not a symbolic link, and no two may
   share a filename. Filenames are limited to letters, digits, ` . `, ` _ ` and ` - `,
   because GitHub rewrites other characters and the published name would then
   differ from the tested one. The SHA-256 of each file is recorded now. The
   token and the repository name are checked last, so every check that needs no
   request has passed before the first request is made.

3. **Require a new version.** The target must be a commit of the repository. The
   tag must not exist, and no release for it may exist, published or draft.

4. **Create a draft** at the target commit, with the notes file as its body.


5. **Upload** every asset to the draft.


6. **Verify the draft.** It must hold exactly the collected filenames. Each asset
   is downloaded into a new, empty directory and must equal the tested file in
   size and SHA-256. A download is cut off one byte past the tested size.

7. **Publish.** A prerelease version is published as a prerelease and is not made
   the latest release. Any other version is published as the latest release.

8. **Confirm.** The published release must carry the requested tag and be
   immutable. The digest listing is written to the job summary.

Nothing is public before the publish step. With immutable releases enabled,
publishing a draft is the moment its tag and assets become permanent, which is
why every check comes before it. A failure after the draft exists leaves the
draft unpublished and the tag uncreated; the error names the draft.
[Recover an interrupted publication](workflows.md#recover-an-interrupted-publication)
says what to do with it.

The action does not adopt an existing draft, replace an asset, or add to a
published release. Once it has created the draft, every request names that
draft by its numeric identifier, never by tag. It publishes the notes file as
written and does not ask GitHub to generate notes: release notes are authored,
as [Write the release notes](workflows.md#write-the-release-notes)
describes.

A glob in ` assets ` is a path relative to the workspace. Within one path segment
` * ` matches any run of characters and ` ? ` any one character, and neither
matches the dot at the start of a name. ` . ` segments are ignored. Nothing else
is a pattern: ` ** `, ` [ `, a backslash, a ` .. ` segment and an absolute path are
each an error, so a glob names only paths under the workspace.

Every request is a ` gh api ` call. ` gh ` adds the token to requests for the
GitHub host and does not repeat it when GitHub redirects an asset download to
the storage host that serves the bytes. A repository without immutable releases
fails the last step, after the release is published;
[Releases](settings.md#releases) lists the setting.
[Actions that run a Roc script](#actions-that-run-a-roc-script)
states what the action needs of the runner.

<a id="sbom-sidecars"></a>

## ` actions/sbom-sidecars `

Generates the SBOM sidecars of an explicit list of release files in the
read-only build job, with the standalone tool of the Blueprint release that
` Blueprint.roc ` names, and checks them before the test jobs and the publisher
see them. It implements the generation sequence of
[the SBOM chapter](software-bill-of-materials.md#generate-sidecars-with-the-shared-actions);
the composition stays the repository’s reviewed file.

``` yaml
- id: roc-sbom
  uses: lukewilliamboswell/roc-automation/actions/blueprint-tool@REVIEWED_FULL_SHA
  with:
    task: sbom-roc-path
- id: sidecars
  uses: lukewilliamboswell/roc-automation/actions/sbom-sidecars@REVIEWED_FULL_SHA
  with:
    files: |
      dist/${{ steps.bundle.outputs.name }}
    version: ${{ needs.validate.outputs.version }}
    target: source
    author: 'Organization: Example Ltd'
    roc-binary: ${{ steps.roc-sbom.outputs.executable-path }}
- uses: actions/upload-artifact@REVIEWED_FULL_SHA
  with:
    name: release-candidate
    path: ${{ steps.sidecars.outputs.assets }}
    if-no-files-found: error
```

| Input | Default | Meaning |
| --- | --- | --- |
| ` files ` | required | The release files, one relative path per line. A glob character, an absolute path, a ` .. ` segment, a symlink, a missing file or a sidecar name fails the step. |
| ` version ` | required | The release version every listed file belongs to |
| ` target ` | required | The Blueprint system of the listed files, or ` source ` for a source bundle. Call the action once per target. |
| ` author ` | required | The SPDX agent that authors the documents, ` Organization: ... ` or ` Person: ... ` |
| ` composition ` | ` sbom/composition.json ` | The reviewed composition |
| ` imports ` | empty | A file ` actions/fetch-dependency-sboms ` wrote, whose ` imported_documents ` join the composition before ` inspect ` |
| ` url-base ` | the release download URL | Where the files will be published; each subject URL is this base plus the file name |
| ` namespace-base ` | the repository’s ` sbom ` URL | The namespace is this base, the commit, the file name and ` revision-<revision> ` |
| ` revision ` | ` 1 ` | Document revision. A correction of a published SBOM increments it and is published at a new location. |
| ` spdx-2-3 ` | ` false ` | Also write ` <file>.spdx-2.3.json ` for consumers that read only SPDX 2 |
| ` archive-members ` | ` generate ` | For source ` .tar.zst ` bundles with a tool that lists archive members: generate a component for each member the composition does not account for, or ` reviewed ` to require the composition to account for every member itself |
| ` roc-binary ` | ` roc ` | The Roc compiler that runs the tool app bundle, used only when the release records no native tool binary for the runner |
| ` blueprint-binary ` | ` blueprint ` | The installed Blueprint executable |

Outputs are ` assets `, every listed file and its sidecars, one relative path
per line, for the artifact upload, the test jobs, the attestation step’s
` subject-path ` and ` actions/publish-release `; ` sidecars `, the sidecars alone;
` digests `, a ` sha256sum `-format listing of them; ` inventory `, the inspected
inventory the documents were rendered from; ` evidence `, a
` roc-automation-sbom-evidence-v1 ` envelope; and ` tool-url `, ` tool-sha256 `,
` compiler `, ` commit ` and ` created `, which release notes can record.
` run-attempt ` identifies the producing attempt for a later publication-only
retry; it is compared with the envelope rather than the retrying job attempt.

The evidence envelope lists every file and sidecar by name, path, size,
SHA-256, format, target and version, bound to the source repository and
commit and to the producing workflow, run and attempt, with the generator,
the digest of the inventory each subject was bound from, each evidence
manifest the tool wrote, the dependency states and the ` incomplete ` coverage
scopes. It is the producer’s handoff to the independent validator and publisher: a transfer
manifest, not a component inventory, and not an authorization. A privileged
job rechecks what it names against the files it receives.

` examples/sbom-chain/.github/workflows/release.yml ` is a complete release
that uses both actions for a bundle and a native executable per system, runs
the exact files, attests every asset including the sidecars, and hands the
explicit asset list to ` actions/publish-release `.

Each subject must be a regular workspace file with a unique name and no
existing sidecars. Versions use a three-part numeric core with optional
dot-separated prerelease identifiers containing ASCII letters, digits and
hyphens. Core numbers and numeric identifiers reject leading zeroes; build
metadata is unsupported. The checkout’s ` HEAD ` must equal the event commit.
Creation time comes from that commit, and the namespace includes it.

The platform URL in ` Blueprint.roc ` selects a reviewed record in
` actions/setup-blueprint/sbom-tools.json `. A recorded native tool is downloaded
privately and admitted by SHA-256. Otherwise the app bundle runs with the exact
compiler the record names; its content-addressed URL fixes the bundle bytes.
The ` members ` and ` manifest ` features determine the checks available from that
selected tool. Unrecorded tools and mismatched compilers fail before generation.

The action stages Spec, Lock and composition in a private directory. Imported
documents are appended; generated ` unavailable_sboms ` stubs cannot grant
exemptions. ` inspect ` validates the composition offline against the selected
target. A locked dependency needs a bound supplier document or a reviewed
unavailability entry.

For source ` .tar.zst ` subjects, tools with ` members ` enumerate the produced
archive before inspection. In ` generate ` mode, unaccounted members become
components identified by their SHA-256, with unknown supplier and license;
` reviewed ` mode requires the composition to account for every member. ZIPs,
PDFs and other subjects use composition-only inventories.

` bind ` hashes each final subject and writes the requested sidecars without
overwriting files. Tools with ` manifest ` also write the subject, sidecar and
inventory bindings to ` <file>.sbom-manifest.json `. ` check ` validates document
profiles, exact subject bytes and agreement between formats; manifest checks
also rehash its inventory and files. Optional SPDX 2.3 output is checked beside
the manifest. The summary reports digests, dependency states and incomplete
coverage scopes.

The job requires only ` contents: read `. A native tool needs no compiler; the
app-bundle path needs its fixed compiler and can download dependencies on first
use. Supply that compiler separately through a Fetch recipe and
[` actions/blueprint-tool `](#blueprint-tool), or through
` roc-lang/setup-roc ` in a job that does not also need the package nightly.

What the action establishes is bounded. The tool’s ` inspect ` decides what the
composition may claim, and the sidecars bind that claim to the final bytes.
Neither proves the composition is complete: an opaque build step’s output is
an ` incomplete ` coverage scope, as
[Declare what the inventory covers](software-bill-of-materials.md#declare-what-the-inventory-covers)
requires. The documents are not provenance; the repository’s attestation step
covers them as subjects, as Blueprint’s own release does.

<a id="fetch-dependency-sboms"></a>

## ` actions/fetch-dependency-sboms `

Finds the published SBOM of every dependency the Lock pins by file SHA-256 and
records it for ` inspect ` to verify. It is the one step of the SBOM chain that
reaches the network, so it holds no write token, runs no repository code and
reads the Lock only through ` blueprint lock show --json `.

``` yaml
- id: suppliers
  uses: lukewilliamboswell/roc-automation/actions/fetch-dependency-sboms@REVIEWED_FULL_SHA
  with:
    target: source
```

| Input | Default | Meaning |
| --- | --- | --- |
| ` target ` | required | The Blueprint system the composition describes, or ` source ` |
| ` composition ` | ` sbom/composition.json ` | The reviewed composition |
| ` output ` | ` sbom/imports.json ` | The imports file to write. An existing file is the reviewed record the fetch must reproduce. |
| ` contact ` | the repository’s security policy URL | Whom a recipient asks about a gap in this repository’s documents |
| ` missing ` | ` fail ` | ` fail ` or ` warn ` when a locked dependency has neither a published SBOM nor a reviewed ` unavailable_sboms ` entry |
| ` stale-opt-outs ` | ` fail ` | ` fail ` or ` warn ` when an ` unavailable_sboms ` entry names a dependency whose supplier now publishes a sidecar |
| ` blueprint-binary ` | ` blueprint ` | The installed Blueprint executable |

Outputs are ` imports `, the path of the written file, and the counts ` fetched `,
` missing `, ` unbindable ` and ` stale `.

For each component of the composition whose origin is ` source:<name> ` or
` program:<name>:<system> `:

1. A component the composition already imports a document for is left alone.


2. A source pinned by a tree or NAR digest has no file SHA-256, so no supplier
   SBOM can bind to it. It is reported as unbindable and nothing is fetched;
   pin the file by its published SHA-256 with ` SourceFile ` to chain it.

3. Otherwise the Lock gives the file’s URL and SHA-256. The action tries
   ` <url>.spdx.json `, then ` <url>.cdx.json `, beside each locked URL. Only an
   HTTPS address without userinfo or query is fetched, a redirect off HTTPS is
   refused, and a document over 1 MiB, Blueprint’s bound, is refused. A ` 404 `
   means the supplier publishes nothing there; any other failure fails the
   step rather than becoming an opt-out.

4. A found document is identified: the ` SpdxDocument ` identifier before ` # ` in
   SPDX 3, ` documentNamespace ` in SPDX 2, ` serialNumber ` in CycloneDX. What it
   says it describes is compared with the Lock before anything is recorded: a
   document about other bytes, another file name or a version the composition
   does not review fails the step and names the difference. ` inspect ` repeats
   the comparison later and remains the authority.

5. The entry records ` id ` ` <component>-sbom `, the URL, the document’s SHA-256
   and identity, the target the supplier’s document states for its subject,
   such as ` source ` for an architecture-independent bundle, the document
   bytes, the relation, the basis, the contact, and a ` subject ` that binds it
   to the component with the Lock’s SHA-256 and the reviewed version. A
   document that states no target takes the Lock’s system for a program and
   the inventory’s target for a file; a relabelled import is what ` inspect `
   refuses.

6. When nothing is published, no entry is written. The file carries a
   ready-to-paste ` unavailable_sboms ` stub whose reason and contact say
   ` REPLACE `, and the step fails with it, unless ` missing ` is ` warn `. The
   maintainer completes the stub in the composition; the action never invents
   an opt-out, and ` inspect ` refuses a stub pasted as it is.

7. When an ` unavailable_sboms ` entry names a dependency whose supplier does
   publish a sidecar, the opt-out is stale: the step fails, unless
   ` stale-opt-outs ` is ` warn `, and the document is not recorded until the
   opt-out is removed.

When the output file already exists, every document it records must be
fetched again with the same URL and SHA-256, and a recorded document the
supplier no longer publishes fails the step. Changed bytes are never adopted
silently; a maintainer reviews the new document and updates the record in a
pull request. The job summary lists every dependency as bound, fetched,
unavailable with its reviewed reason, unbindable, missing or a stale opt-out.

The file’s digest proves only that the bytes did not change between the
review and the build. It does not prove the supplier wrote them: a release
asset and its sidecar come from the same host and the same release, so a
substituted sidecar would need the same access as a substituted artifact.
Verifying the supplier’s attestation of the sidecar belongs to admission,
as [Verify released artifacts](artifact-verification.md#separate-admission-from-routine-use)
places it, and to the evidence work tracked in roc-automation’s issues; this
action does not call an attestation service.

<a id="publish-build-inputs"></a>

## ` actions/publish-build-inputs `

Publishes native linker inputs built on a same-repository pull request as an
immutable, content-addressed release, and commits only the resulting lock to
that pull request. The job that holds release credentials never checks out or
runs the pull request’s code.

| Input | Meaning |
| --- | --- |
| ` pull-request ` | Number of the same-repository pull request that holds the producer source |
| ` producer-workflow ` | Filename of the producer workflow to dispatch on that branch |
| ` producer-run ` | Optional: an existing successful producer run for the current head, used for recovery |
| ` candidate-artifact ` | Name of the artifact the producer uploads |
| ` lock-path ` | The only repository path the action may commit |
| ` release-prefix ` | Prefix of the content-derived release tag |

[Publish linker inputs from a pull request](linker-input-releases.md#publish-linker-inputs-from-a-pull-request)
specifies the producer, the caller workflow and its permissions, recovery, and
review.

