A Roc package can run its checks and publish HTML, mirrored Markdown, an
llms.txt discovery index and a PDF using
released executables. Blueprint owns the fixed downloads and their reviewed
lock. The package compiler remains the latest nightly supplied by CI. No local
installer, renderer build or Nix environment is needed for this path.
The working example is in examples/pure-package/. It uses a Blueprint
release, the matching platform URL and a published roc-docs renderer from a
tools release. Upgrade a utility by changing its recipe and reviewing
its new lock entries. The utility release and fixed tooling compiler do not
control which nightly tests the package.
Risks and controls for utility provisioning§
A cache can return damaged bytes, a task can expose a temporary executable link, or a renderer recipe can accidentally choose the package compiler. Treat provisioning, persistent handoff and product testing as separate contracts. Blueprint rechecks locked downloads; the handoff resolves the executable’s real store path; the floating compiler remains a distinct host input. The cold/warm adoption jobs exercise those boundaries and compare actual executable identities. A cache hit proves only that storage returned an entry. Supported provisioning targets and external runtime programs still need explicit validation.
Copy the starter§
Copy examples/pure-package/ to the repository root, including .github/ and
.gitignore. Keep its Blueprint configuration and lock together. Replace the
package, example, expected output, notes and manual content, then review and
regenerate the lock when changing tool recipes.
Blueprint.roc
Blueprint.lock
.github/workflows/ci.yml renders the manual on a pull request
.github/workflows/release.yml the thin caller of the shared release workflow
.gitignore
package/main.roc
package/Greeting.roc
examples/hello.roc
tests/spec.json
docs/index.adoc
docs/releases/0.1.0.adoc
release.yml is the whole of the repository’s release and pull-request
checking; see
The shared release workflow for a Roc package.
The documentation workflow is:
name: Check package and shared utility
on:
pull_request:
workflow_dispatch:
permissions: {}
jobs:
docs:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: lukewilliamboswell/roc-automation/actions/setup-blueprint@1f3bc71102cf2d07c324b6009a41823efd21c9aa
with:
cache: 'true'
cache-workload: docs
- run: blueprint check
- run: blueprint run docs
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
with:
name: manual
path: .docs-out/
if-no-files-found: error
package:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: lukewilliamboswell/roc-automation/actions/setup-blueprint@1f3bc71102cf2d07c324b6009a41823efd21c9aa
- uses: roc-lang/setup-roc@f5a74bf90aa694b43ab4a7cbfdb19273bc6aa697
with:
version: nightly-new-compiler
- run: blueprint run check-package
The documentation job consumes a fixed released executable. The package job uses the current nightly; this single test job needs no matrix-wide moving-tag coordination. Uploading a CI artifact grants no publication authority. The starter’s declared Blueprint targets are Linux and Apple Silicon macOS; its workflow uses Linux. See the producer and interface skeleton when adding another shared operation.
Declare the utility and tasks§
Copy the example’s structure and choose your package name and PDF filename.
The renderer recipe names one release; {system} selects the release asset
for each declared system. roc-stable is a separately fetched fixed tool.
Bare roc is expected from the host and is never supplied by that recipe.
# Package testing uses the latest nightly supplied by CI as bare `roc`.
# Fixed tooling and released utilities have separate names and locked bytes.
app [config] { pf: platform "https://github.com/lukewilliamboswell/roc-blueprint/releases/download/0.8.0/FbJymjYdCxx9vqMcau7r4tQfeAbnVGqqJoYrLGqPCqKu.tar.zst" }
import pf.Recipes
config = [
Name("roc-package-example"),
Systems(["x86_64-linux", "aarch64-linux", "aarch64-darwin"]),
Environment("docs", [Programs([Fetch("roc-docs", Recipes.file("https://github.com/lukewilliamboswell/roc-automation/releases/download/tools-0.2.0/roc-docs-{system}"))]), Expect(["realpath", "sh"])]),
Environment("fixed-tools", [Programs([Fetch("roc-stable", Recipes.roc_nightly("nightly-2026-10-18-1df370f"))]), Expect(["realpath", "sh"])]),
Environment("package", [Expect(["roc"])]),
# The shared release workflow runs this utility against the `roc` on PATH; a
# maintainer runs the same task locally, such as `-- test-examples --update`.
Environment("release", [Programs([Fetch("release-package", Recipes.file("https://github.com/lukewilliamboswell/roc-automation/releases/download/tools-0.5.0/release-package-{system}"))]), Expect(["roc", "git", "gh", "tar", "unzip", "zip", "chmod", "touch", "env"])]),
Task("release-package", [Use("release"), Run(["release-package"])]),
Task("docs", [Use("docs"), Run(["roc-docs", "build", "--workspace", ".", "--pdf-filename", "example.pdf"])]),
Task("docs-path", [Use("docs"), Run(["sh", "-c", "realpath \"$(command -v roc-docs)\""])]),
Task("tooling-version", [Use("fixed-tools"), Run(["roc-stable", "version"])]),
Task("tooling-path", [Use("fixed-tools"), Run(["sh", "-c", "realpath \"$(command -v roc-stable)\""])]),
Task("check-package", [Use("package"), Run(["roc", "test", "package/main.roc"])]),
]
Expect declares sh and realpath, which the handoff tasks use, and the
package compiler, which CI supplies. Blueprint’s own archive/store operations
also use the host’s tar, ln and chmod. GitHub’s supported Linux and macOS
runners supply these. The shared setup and optional diagnostic actions use
Python 3.10 or later on the runner; the renderer executable uses none of these
tools to parse or render documents.
Install the release binary matching the platform URL, following Blueprint’s installation instructions. Then run these commands from the example directory, or from your repository after adopting its files:
blueprint --version
blueprint update
blueprint check
blueprint run docs
blueprint run tooling-version
Review and commit Blueprint.roc and Blueprint.lock. Check every utility URL,
per-system SHA-256, length and newly admitted download host. On an update,
compare the utility’s entries with the upstream release manifest. The setup
action’s full SHA fixes its reviewed Blueprint installer and checksum registry;
it reads the Blueprint release from the platform URL rather than another
workflow version pin.
Blueprint installs its embedded fixed configuration compiler into its
verified store before any Roc CLI is installed globally. No GitHub compiler
bootstrap download is needed. This compiler is separate from the downloaded
renderer and the floating package compiler; the example’s fixed roc-stable
recipe also remains a separate reviewed pin. Roc package bundles occupy Roc’s
package cache and are not covered by the artifact cache below. Task-only
RocPackages bundles are pinned by their URL hashes and verified on initial
unbundling, rather than listed in Blueprint.lock. Existing extracted package
directories are reused without rehashing, as
the compiler trust model explains.
The adoption checks read blueprint lock show --json format 2 and select fetched
programs by name and system to compare actual executable lengths and hashes.
This public report avoids depending on the lock’s internal representation.
Restore downloads before the first task§
In a read-only build job, call the shared setup action after checkout:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
persist-credentials: false
- uses: lukewilliamboswell/roc-automation/actions/setup-blueprint@1f3bc71102cf2d07c324b6009a41823efd21c9aa
with:
cache: 'true'
cache-workload: docs
- run: blueprint run docs
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9
with:
name: website-and-manual
path: .docs-out/site
if-no-files-found: error
The setup action installs a verified Blueprint binary without Nix by default.
Set install-nix: 'true' when a selected environment takes tools from a Nix
source or a sandboxed native build needs it. Existing configurations must follow
the Blueprint upgrade procedure, including
a full lock update when moving to Lock 2. Retain Nix while selected tools or
sandboxed builds depend on it. The privileged report/publisher actions retain their own reviewed tooling
and do not run this candidate repository configuration.
The cache contains only
${XDG_CACHE_HOME:-$HOME/.cache}/blueprint/v1/artifacts. It is restored before
tasks and saved by the cache action’s post step after a successful job.
It does not include v1/trees, .blueprint/run, compiler build outputs or
Roc’s package cache. Each restored artifact is rehashed by Blueprint before it
is used; its length and identity must match the lock. A damaged artifact and
its derived trees are removed and the release bytes are downloaded again.
An unavailable or mismatching replacement cannot become a usable tool.
An exact cache hit is a transfer optimization, not verification. Restoring it
still transfers cache bytes from GitHub. The Blueprint executable has a separate,
default-on cache keyed by release, system and reviewed digest; restored executable
bytes are rehashed before use. Disable it with cache-binary: false.
binary-cache-hit describes verified executable reuse; cache-hit describes
the artifact store. Neither changes a version pin. Ordinary GitHub branch and pull-request cache
visibility applies: a PR can restore an eligible default-branch cache and its
new cache is scoped to that PR; a failed job does not save a successful cache.
Cache keys include system, Blueprint release, installer/registry identity,
workflow, logical tool set and lock digest. cache-workload is required when
caching is enabled. Give different tool subsets different names, including
matrix variants that fetch different programs. For example, a docs job uses
docs and a tooling-check job uses fixed-tools. Jobs intentionally sharing
the same complete tool set may share that name. GitHub caches are immutable:
a compiler-only job must not claim the key intended to contain the renderer.
A changed lock or installer produces a new key.
The repository’s Blueprint adoption workflow tests cold docs and fixed-tool
jobs, then restores each exact cache on another fresh runner. It compares actual
executables with the lock and checks that warm tasks do not refetch utility or
fixed compiler archives. A Nix sentinel fails any accidental Nix invocation.
See
Blueprint’s store rules
for the detailed verification and rebuilding contract.
Pass an executable to a shared action§
Running blueprint run docs keeps the renderer’s options in the repository’s
task and needs no executable handoff. For a workflow using actions/build-docs,
use the example’s docs-path task and the shared handoff action:
- id: renderer
uses: lukewilliamboswell/roc-automation/actions/blueprint-tool@1f3bc71102cf2d07c324b6009a41823efd21c9aa
with:
task: docs-path
label: roc-docs
measure: 'true'
output-directory: .docs-out
- uses: lukewilliamboswell/roc-automation/actions/build-docs@1f3bc71102cf2d07c324b6009a41823efd21c9aa
with:
renderer-binary: ${{ steps.renderer.outputs.binary }}
pdf-filename: example.pdf
The task must print exactly one absolute executable path. command -v alone
returns a temporary link removed when the Blueprint step ends. Resolve it with
realpath inside the task, as the example does. The resolved store path and the
optional wrapper remain usable within that job. A new job restores artifact
bytes and runs the task again; it does not restore or reuse the old task link.
The handoff reports the actual executable’s SHA-256, length and provisioning
time. Blueprint’s successful Fetch task supplies lock verification; the
reported label is descriptive and is not another admission pin. The action
exports executable-path for the real file and binary for the optional
measurement wrapper. Renderer options remain task arguments or action inputs;
the handoff does not infer them from the task.
Measure stages and retain acceptance checks§
Optional measurements record wall time, maximum child-process RSS in KiB, exit status, and output file count/bytes in JSON. Provisioning includes verification and any downloads. The measured renderer command excludes the host Python wrapper’s RSS and output-directory scanning time. If the command starts an API compiler, its children contribute to the reported command cost.
Measure generated-input preparation and API generation as separately labelled
commands when comparing renderer costs. blueprint-tool exports
measure-script for these stages; invoke it with the host Python interpreter:
python3 "$MEASURE_SCRIPT" --label preparation --metrics "$RUNNER_TEMP/preparation.json" -- blueprint run prepare-docs
python3 "$MEASURE_SCRIPT" --label api --metrics "$RUNNER_TEMP/api.json" -- blueprint run api-docs
These task names are repository-defined stages, not built-in Blueprint APIs. Keep browser and independent PDF-reader acceptance outside the measurement. Review stable routes and anchors, exact source blocks, tables, figures, release-specific PDF filenames and API assembly before changing publication. Deploy jobs consume only the accepted site artifact and keep their write tokens separate from all repository tasks.
Keep the package nightly floating§
The package-check job installs the latest nightly with the official setup action
and then runs blueprint run check-package. It does not fetch a program called
roc from the fixed-tooling environment:
- uses: roc-lang/setup-roc@f5a74bf90aa694b43ab4a7cbfdb19273bc6aa697
with:
version: nightly-new-compiler
- run: blueprint run check-package
For multiple package-test jobs, resolve the moving tag once and pass it through, as the nightly workflow describes. Caching fixed tooling and released utilities never delays package-nightly updates. Native platforms can retain Nix toolchains and sandboxed build inputs while using the same released documentation utility and artifact deployment.