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.
| 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 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:
installs Nix, with the install action
actions/setup-blueprintuses, at the same full commit SHA;downloads the
blueprintbinary that roc-automation’sBlueprint.rocnames and checks it against its recorded SHA-256, as Theblueprintbinary describes; andruns its task,
blueprint run nightly-reportorblueprint 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 }}
| 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 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 }}
| 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.
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.
| 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.
Check the inputs. The version, tag, target and
prereleasevalue must be well formed and agree with each other. The notes file must be a non-empty regular file of UTF-8 text.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.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.
Create a draft at the target commit, with the notes file as its body.
Upload every asset to the draft.
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.
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.
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
| 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, 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
| 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>:
A component the composition already imports a document for is left alone.
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
SourceFileto chain it.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. A404means the supplier publishes nothing there; any other failure fails the step rather than becoming an opt-out.A found document is identified: the
SpdxDocumentidentifier before#in SPDX 3,documentNamespacein SPDX 2,serialNumberin 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.inspectrepeats the comparison later and remains the authority.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 assourcefor an architecture-independent bundle, the document bytes, the relation, the basis, the contact, and asubjectthat 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 whatinspectrefuses.When nothing is published, no entry is written. The file carries a ready-to-paste
unavailable_sbomsstub whose reason and contact sayREPLACE, and the step fails with it, unlessmissingiswarn. The maintainer completes the stub in the composition; the action never invents an opt-out, andinspectrefuses a stub pasted as it is.When an
unavailable_sbomsentry names a dependency whose supplier does publish a sidecar, the opt-out is stale: the step fails, unlessstale-opt-outsiswarn, 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.
| 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 specifies the producer, the caller workflow and its permissions, recovery, and review.