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.

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

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.

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

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

InputMeaning
install-nixDefaults to false. Enable for Nix-sourced tools and sandboxed builds.
cache-binaryDefaults to true. Reuse only executable bytes that match the selected release, system and reviewed digest.
cacheDefaults to false. Restore/save only Blueprint v1/artifacts bytes.
cache-workloadRequired with caching: a logical tool-set name, distinct for jobs or matrix variants that provision different subsets.
versionThe 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-linuxReviewed SHA-256 of the binary for x86_64 Linux runners. Overrides the recorded value.
sha256-aarch64-linuxReviewed SHA-256 of the binary for arm64 Linux runners. Overrides the recorded value.
sha256-aarch64-darwinReviewed SHA-256 of the binary for Apple Silicon macOS runners. Overrides the recorded value.
sha256-x86_64-darwinReviewed SHA-256 of the binary for Intel macOS runners. Overrides the recorded value.
sha256-x86_64-windowsReviewed 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 states what the checksum does and does not establish.

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.

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.

Install the nightly compiler§

The package compiler is installed by the official roc-lang/setup-roc action, not by an action in this repository. Reference it by full commit SHA like any other.

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

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

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

The release-package utility§

The utility behind the shared release workflow 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:

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.

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

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.

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 for the canonical archive, caller, inputs, diagnostic artifacts and failure handling.

actions/nightly-report§

Keeps the one tracking issue of a nightly compatibility workflow. It checks out nothing from the calling repository and runs none of its code.

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 }}
InputDefaultMeaning
outcomeRequiredOutcome of the step that ran the examples
nightly-tagRequiredThe Roc nightly the examples ran on, named in the issue text
release-tagRequiredThe release whose examples ran, named in the issue text
titleThe latest release fails on the latest Roc nightlyExact 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.

outcomeWhat the action does
successComments on the tracking issue and closes it. With no tracking issue open it does nothing, and action is none.
failureComments on the tracking issue. With no tracking issue open it opens one.
Anything elseFails 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 states what the calling workflow must provide, and Actions that run a Roc script what the action needs of the runner.

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.

- 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 }}
InputDefaultMeaning
docs-directorydocsSource directory, relative to the repository root
entrypointindex.adocThe manual’s entrypoint, relative to docs-directory
output-directory.docs-outGenerated output, relative to the repository root
pdf-filenamemanual.pdfFilename of the generated PDF
docs-versionunreleasedLabel passed to the sources as the docs-version attribute
api-entrypointemptyRoc package or platform root for the generated API reference
roc-commandrocRoc executable used when api-entrypoint is set
markdowntruePublish chapter .md files, HTML discovery links and llms.txt; set false to omit them
site-urlemptyHTTPS publication directory for PDF links, Markdown source links and absolute llms.txt chapter URLs
renderer-binaryenvironment or PATHPath or executable name of the precompiled renderer
title, author, languageemptyOptional overrides for document metadata
pdf-image-dpi144PDF 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. 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.

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.

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.

InputDefaultMeaning
versionrequiredThe release version, such as 0.12.0 or 0.12.0-rc1
prereleaserequiredExactly true or false. It must agree with whether version has a prerelease suffix.
notes-filerequiredPath of the rendered release notes, which become the body of the release
assetsrequiredGlobs of the tested files, one per line, relative to the workspace
tagversionThe 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 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 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 lists the setting. Actions that run a Roc script states what the action needs of the runner.

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; the composition stays the repository’s reviewed file.

- 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
InputDefaultMeaning
filesrequiredThe 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.
versionrequiredThe release version every listed file belongs to
targetrequiredThe Blueprint system of the listed files, or source for a source bundle. Call the action once per target.
authorrequiredThe SPDX agent that authors the documents, Organization: ... or Person: ...
compositionsbom/composition.jsonThe reviewed composition
importsemptyA file actions/fetch-dependency-sboms wrote, whose imported_documents join the composition before inspect
url-basethe release download URLWhere the files will be published; each subject URL is this base plus the file name
namespace-basethe repository’s sbom URLThe namespace is this base, the commit, the file name and revision-<revision>
revision1Document revision. A correction of a published SBOM increments it and is published at a new location.
spdx-2-3falseAlso write <file>.spdx-2.3.json for consumers that read only SPDX 2
archive-membersgenerateFor 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-binaryrocThe Roc compiler that runs the tool app bundle, used only when the release records no native tool binary for the runner
blueprint-binaryblueprintThe 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, 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 requires. The documents are not provenance; the repository’s attestation step covers them as subjects, as Blueprint’s own release does.

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.

- id: suppliers
  uses: lukewilliamboswell/roc-automation/actions/fetch-dependency-sboms@REVIEWED_FULL_SHA
  with:
    target: source
InputDefaultMeaning
targetrequiredThe Blueprint system the composition describes, or source
compositionsbom/composition.jsonThe reviewed composition
outputsbom/imports.jsonThe imports file to write. An existing file is the reviewed record the fetch must reproduce.
contactthe repository’s security policy URLWhom a recipient asks about a gap in this repository’s documents
missingfailfail or warn when a locked dependency has neither a published SBOM nor a reviewed unavailable_sboms entry
stale-opt-outsfailfail or warn when an unavailable_sboms entry names a dependency whose supplier now publishes a sidecar
blueprint-binaryblueprintThe 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 places it, and to the evidence work tracked in roc-automation’s issues; this action does not call an attestation service.

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.

InputMeaning
pull-requestNumber of the same-repository pull request that holds the producer source
producer-workflowFilename of the producer workflow to dispatch on that branch
producer-runOptional: an existing successful producer run for the current head, used for recovery
candidate-artifactName of the artifact the producer uploads
lock-pathThe only repository path the action may commit
release-prefixPrefix of the content-derived release tag

Publish linker inputs from a pull request specifies the producer, the caller workflow and its permissions, recovery, and review.