This chapter states who may do what in the workflows and shared actions this manual describes, and why each boundary is drawn where it is. It is normative for the shared actions in roc-automation: a change to their behavior or permissions must keep these boundaries and arrive with regression tests for them.
Two rules carry most of the weight:
A job that runs repository or candidate code has no write access.
A job with write access runs only reviewed code at a full commit SHA, and handles candidate output as inert files.
Read-only permissions limit repository authority; they are not a process sandbox or proof that build outputs are benign. Code in a job can read its workspace, use its network access and influence its outputs. Run floating compilers and consumer code on disposable hosted runners without sensitive workload data or publication credentials. Clearing child token variables reduces exposure but does not isolate same-job processes from one another. Reviewed inputs, exact source/artifact binding and consumer-facing tests remain separate controls.
Risks and controls for execution authority§
A compiler, example or prerequisite script can be malicious or compromised, and generated output can try to cross into a job that can publish or edit issues. Separate execution from authority. Use read-only disposable test jobs, reviewed pinned controllers in write jobs, inert data handoffs and validation of source/run/artifact identities before writes. Security regressions exercise wrong identities and rejected transitions.
The three workflows§
| Workflow and job | Token access | Runs repository code |
|---|---|---|
ci.yml test | contents: read | Yes, including pull-request code |
release.yml build and test | contents: read | Yes |
release.yml publish | contents: write, behind a protected environment | No: it runs actions/publish-release on artifacts produced by earlier jobs |
Shared release-package.yml build | contents: read | Yes: one prerequisite program the caller names, the package’s check task, its examples and the compiler, beside the release-package utility its lock fixes |
Shared release-package.yml test | contents: read | The repository’s Blueprint, to provision the locked utility; otherwise only the archived examples from the build job’s artifact, against the bundles from the same artifact |
Shared release-package.yml publish | contents: write, id-token: write, attestations: write, behind the release environment | No: roc-automation’s own checkout at the caller’s pinned SHA, actions/attest and actions/publish-release on the build job’s artifact |
release.yml site deploy | pages: write, id-token: write | No: it deploys an artifact built by a read-only job |
nightly.yml test | contents: read | Yes: published examples and optional default-branch native prerequisites |
nightly.yml selection and aggregation | contents: read | No: pinned shared runner and inert release/result metadata |
nightly.yml report | issues: write | No: it runs actions/nightly-report |
The publish job accepts only a manual dispatch on the default branch, uses the event commit throughout, and consumes the artifacts of its own run. It never checks out the repository, so a script changed in the tested commit cannot run with release credentials.
The nightly compatibility workflow has no contents, pull-request, actions or
status write access in any job. It cannot commit, merge, dispatch or publish.
Its report job runs actions/nightly-report from the same immutable commit as
the consumer-pinned reusable workflow, through a $/ self-repository reference,
and checks out nothing. The controller and nested installer use the same
revision; no workspace action can replace them. It receives only the outcome of the examples step, the nightly tag and
the release tag from the test job, never text produced by the examples under
test. It edits a single tracking issue, identified by its exact title and a bot
author. Every workflow in a repository shares the GitHub Actions bot identity,
so that test excludes issues opened by people and does not prove which workflow
opened the issue.
No workflow in the model opens a pull request, approves one, or merges one. Every change to the repository is a maintainer’s pull request under the repository’s own rules.
What the jobs with write access run§
The publish job and the report job hold the tokens that can change a
repository’s releases and issues. Each runs one shared action,
actions/publish-release or actions/nightly-report, and each action is a Roc
script that runs from the action’s own copy of roc-automation, at the full
commit SHA the workflow names. What runs in those two jobs is therefore:
Nix, installed by an install action that the shared action references by full commit SHA; that action fixes the Nix version and downloads its installer from the Nix project over HTTPS;
the
blueprintbinary, for the release that roc-automation’s ownBlueprint.rocnames, admitted by its reviewed SHA-256 as Theblueprintbinary describes;the environment that roc-automation’s
Blueprint.lockpins:roc-stable, which runs the script, theghCLI, which makes every request, and coreutils, which providessha256sum;the content-addressed basic-cli platform and its dependencies, named by the reviewed script headers and
RocPackagesURLs; andthe script and its modules under
scripts/in roc-automation.
The one full SHA in the workflow’s uses: line fixes all of these. The install
action’s SHA, the recorded hashes of the binary, the lock and the script are all
files of that commit. The lock pins provider inputs by revision and content
digest; the task-only Roc bundles are pinned by their URL content hashes. Nix
takes the compiler by the hash its pinned overlay records, and takes the tools
from nixpkgs at the pinned revision, as built by its binary cache, whose
signatures it checks. Blueprint prepares missing task bundles through Roc’s
verified unbundling path, described below.
Neither job checks out the calling repository, and neither action runs any of
its code. The calling repository’s Blueprint.roc, lock and scripts play no
part. The report action reads no file from the job’s workspace. The publish
action reads the notes file and the asset files it is given, which an earlier
job built: it sends the notes as text, and it hashes, uploads and compares the
assets as bytes, and executes none of them.
The token is in the environment of the one step that runs the script, where
gh reads it. It is never a command-line argument and is never printed, and
the scripts check every input before the first request.
Nix does not hold the token. The install action would otherwise write the job’s default token into Nix’s configuration, for Nix to present to GitHub on its own fetches. These two actions clear that setting, and stop before installing anything further if Nix still reports a token. Every input Nix fetches is public and pinned by the lock, so it needs none.
Compilers§
Blueprint.roc names the release tag used for repository tooling, and
Blueprint.lock pins the overlay by revision and content digest. Changing the
tooling compiler is therefore a reviewed source change. Nix verifies the
compiler archive against the hash the pinned overlay revision records for that
tag. This is content integrity under a reviewed hash, as in
Released artifact verification;
it is not provenance for the upstream compiler build.
Task and shell bundles listed under RocPackages are not provider inputs in
Blueprint.lock. Their committed URLs name the content hash of each bundle.
Blueprint downloads a missing archive and invokes roc unbundle, which rejects
bytes that do not match that hash, before publishing the extracted package into
Roc’s package cache. A bundle a script names but the environment does not list
is fetched and checked by Roc itself. Sandboxed builds cannot download: their
RocPackages sources are provider-locked and materialized into a separate build
package cache.
The
Blueprint’s Roc package contract
distinguishes Roc’s extracted package cache from Blueprint’s verified artifact
store. Blueprint reuses a package directory that already has
main.roc without rehashing that directory. Reuse therefore assumes its contents
have not been modified since verified extraction; the content-addressed URL
does not attest to an existing extracted tree. The shared privileged actions
restore no candidate-produced package cache, run no candidate code and rely on
the disposable runner’s intact local cache. This does not establish integrity
of arbitrary pre-existing package caches on a maintainer’s machine.
The package compiler is the latest published nightly and is not pinned. It is
installed by the official roc-lang/setup-roc action, referenced by full
commit SHA, from a roc-lang/nightlies release over HTTPS. That action does not
check a nightly download against a digest, so the compiler’s integrity rests on
GitHub’s release hosting and the action’s pinned revision. This is accepted
because the nightly only ever runs in jobs that hold no write access. A workflow
resolves the moving release once and passes the tag to every job. Release notes
record the version used.
The blueprint binary§
actions/setup-blueprint downloads the prebuilt blueprint binary from a
roc-blueprint release. Nix installation is an explicit caller option, through
an action referenced by full commit SHA, for environments/builds that need it. It builds nothing. Before the binary runs, the action checks it against
a reviewed SHA-256 and fails on a mismatch. Routine CI verifies that reviewed
hash locally and does not call an attestation service. The hash establishes
which bytes run; it is not provenance for the roc-blueprint build.
The reviewed hashes are recorded in the action, one set for each roc-blueprint release, and are reviewed in the pull request that adds them. A caller references the action by full commit SHA, so the hashes it trusts are fixed by the same reference as the code that checks them. A caller may supply a hash of its own for its runner, which then takes the place of the recorded one.
A release’s binary must be used with that release’s platform URL in
Blueprint.roc, so the action takes the release from that URL. The file is
candidate content in a pull request, and that is safe: the version it names can
only choose among releases whose hashes are already reviewed. A version the
action does not record, with no hash from the caller, fails the step.
The action accepts only a release version, from its version input or from
Blueprint.roc, and builds the download URL itself, under the roc-blueprint
releases of this project, so neither can redirect the download. The binary is
written to a private temporary file and becomes executable and visible on PATH
only after its hash matches. A mismatch, a release with no reviewed hash for the
runner, or a system with no released binary fails the step and installs nothing.
Blueprint evaluates Blueprint.roc with its own fixed compiler, embedded in
the admitted executable and verified against its recorded SHA-256 before
extraction into the store, or an exactly matching host compiler. No GitHub
compiler download is needed for this bootstrap. The executable digest therefore
also fixes the embedded compiler archive; it does not prove that compiler
is free of defects. This compiler remains separate from fixed repository tooling
and the floating package compiler. Programs/Fetch/Expect environments need
no Nix; environments selecting Nix-sourced tools and sandboxed builds do.
The default executable cache is separate from the artifact store. Its exact key includes release, system and reviewed digest. The installer copies bounded regular bytes into a private file, hashes that copy and exposes an executable only on a match. Corrupt regular bytes cause a fresh verified download; symlink and nonregular cache entries are rejected. A cache hit never replaces hash admission. Cache visibility does not establish provenance, and no moving release lookup is needed to reuse the pinned executable.
Optional artifact caching restores only v1/artifacts, never extracted trees or
temporary task links. Keys separate logical tool subsets and include the lock,
installer/registry identity, Blueprint release, system and workflow. Blueprint
checks restored artifact bytes against their content identity and locked length
before use. Corrupt artifacts and their derived trees are discarded and refetched;
a mismatching download is not usable. Derived trees are rebuilt on a fresh
runner rather than trusted from a cache. GitHub cache-hit status is not admission.
Roc’s package cache is separate and is not included by this action.
actions/blueprint-tool runs a repository task and therefore belongs only in
read-only build jobs. The task resolves a temporary program link before its
lifetime ends; the action reports the actual file identity and can measure calls
through an optional host-side wrapper. It does not independently authenticate a
program merely because a task printed its path. The repository’s reviewed Fetch
recipe and lock, plus Blueprint’s verification during provisioning, supply that
admission. Privileged controllers never evaluate candidate Blueprint files or
consume candidate executable caches.
The setup action takes no token as an input. When requested, the Nix installer
can configure Nix with the job’s default token. The report/publisher actions
retain their explicit token-clearing checks described above. An optional cache
requiring id-token: write must remain an explicit caller choice; the artifact
cache here requires no such token permission. See
the complete Roc package adoption guide.
SBOM sidecars and supplier documents§
actions/sbom-sidecars and actions/fetch-dependency-sboms evaluate repository
configuration and run only in jobs with contents: read. The action revision
fixes reviewed SBOM tool records; native binaries are admitted by SHA-256 and
app bundles by their content hash and required compiler.
Supplier fetching uses HTTPS with a 1 MiB document bound. A recorded document must retain its digest and describe the locked artifact. Changed or missing bytes fail, and stale opt-outs require review. A document digest establishes which bytes were reviewed, not who authored them.
The shared package publish job rechecks the transferred envelope, Blueprint manifests and sidecar subject identities before attestation. Expected source, run and producing attempt come from workflow context and build outputs; publication retries retain the producer identity. The controller reads bounded regular files and executes no candidate code or Blueprint tool.
Tests transfer generated source/native records and reject wrong identities, changed bytes and unsafe paths. This establishes final-byte consistency, not composition completeness or provenance of producer claims. The SBOM chapter owns generation, coverage and supplier-admission guidance.
Documentation deployment§
Renderer compilation, source conversion and execution run with contents: read.
The executable cache is keyed by target, compiler, source and dependency/font
pins; restored binaries still undergo native acceptance tests. The deploy job
holds pages: write and id-token: write, runs only on the default branch, and
deploys the uploaded Pages artifact. It checks out no source, executes no
renderer and restores no candidate executable cache. A pull request can build
the site and cannot deploy it.
Downstream status inventory§
The README’s downstream version inventory is also built in the read-only
Documentation job. scripts/downstream_status.roc makes only GET requests,
reads immutable default-branch workflow and composite-action source as text,
and emits JSON badge data and an escaped HTML report into the Pages artifact.
It executes no downstream code. The daily schedule uses the same separate
deployment job and permissions as other documentation builds. Publication does
not commit to this repository or update a consumer. Unknown API evidence and
incomplete trees never establish that a consumer is current.
Compatibility evidence requires an eligible latest-pair run of the pinned shared nightly workflow. The dashboard verifies its immutable caller revision, aggregate-job origin, complete current-attempt inventory and pass freshness. The dashboard contract defines eligible scheduled/manual runs and stale results.
Shared release-compatibility runner§
Selection, testing and aggregation have only contents: read. Selection fixes
one release/compiler pair; each target verifies the selected archive’s identity,
size and SHA-256, then rejects unsafe paths, aliases, special files and resource
limit violations before extraction. Archived examples and native prerequisites
run only in the read-only test job. Prerequisites require schedule/manual
execution on the actual default branch and a checkout matching the event SHA.
Checkout credentials are disabled and child token variables are cleared.
The runner bounds child execution and output, cleans up descendants, preserves successful and failed diagnostics, and escapes workflow commands in console output. Tests cover archive admission, interruptions and actual native targets. Allowed compiler switches and suite fields are defined in the suite contract.
Aggregation checks every declared target’s source/compiler identity, counts and
run attempt as inert JSON. A single matrix output cannot establish a complete
pass. Reporting holds only issues: write, checks out nothing and receives a
constrained outcome and selected tags, never archive-generated diagnostic text.
Complete passes can close the issue; known failures can open/update it. Unknown,
missing, skipped or interrupted work cannot close it, and override probes do not
modify the latest-pair tracking issue. The runner has no pin-update, PR, merge,
dispatch, release or deployment authority.
Linker-input release publisher§
The pull-request linker-input publisher is a separate controller action for inert, fully declared files produced by a caller’s unprivileged build jobs. A privileged consumer workflow invokes the controller at an exact reviewed SHA. Caller source is never checked out, imported, sourced, or executed in its privileged job. Publication requires manual dispatch on the caller’s live default-branch commit. The controller validates the complete regular-file set and content identities, verifies the producer’s attestations, refuses mismatched existing tags/releases, re-downloads and hashes the complete draft asset inventory, and requires immutable-release enforcement after publication. See Trusted publisher contract for the caller contract.
Released artifact verification§
Consumers use reviewed content hashes as the runtime identity of released binary inputs. Routine CI verifies downloaded and cached bytes locally before extraction or execution. It must not depend on a remote signing, transparency, or attestation service when a reviewed hash is available. Provenance attestations are verified when a new release or lock is adopted, during an explicit audit, or by a downstream verifier. See Verify released artifacts for the complete admission and consumption policy.