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.