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:

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 jobToken accessRuns repository code
ci.yml testcontents: readYes, including pull-request code
release.yml build and testcontents: readYes
release.yml publishcontents: write, behind a protected environmentNo: it runs actions/publish-release on artifacts produced by earlier jobs
Shared release-package.yml buildcontents: readYes: 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 testcontents: readThe 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 publishcontents: write, id-token: write, attestations: write, behind the release environmentNo: 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 deploypages: write, id-token: writeNo: it deploys an artifact built by a read-only job
nightly.yml testcontents: readYes: published examples and optional default-branch native prerequisites
nightly.yml selection and aggregationcontents: readNo: pinned shared runner and inert release/result metadata
nightly.yml reportissues: writeNo: 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:

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.