> Documentation version: 3. [HTML page](pure-package.html)

<a id="adopt-a-pure-package-without-nix"></a>

# Adopt a Roc package without Nix

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.

<a id="risk-utility-provisioning"></a>

## 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.

<a id="copy-the-starter"></a>

## 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.

``` text
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](workflows.md#the-shared-release-workflow-for-a-pure-package).
The documentation workflow is:

``` yaml
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](shared-utilities.md#share-automation-through-executable-utilities)
when adding another shared operation.

<a id="declare-the-utility-and-tasks"></a>

## 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.

``` roc
# 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](https://lukewilliamboswell.github.io/roc-blueprint/getting-started.html).
Then run these commands from the example directory, or from your repository
after adopting its files:

``` sh
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](index.md#compilers) 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.

<a id="restore-downloads-before-the-first-task"></a>

## Restore downloads before the first task

In a read-only build job, call the shared setup action after checkout:

``` yaml
- 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](maintenance.md#upgrade-blueprint), 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](https://lukewilliamboswell.github.io/roc-blueprint/no-provider.html#the-store-of-downloads)
for the detailed verification and rebuilding contract.

<a id="pass-an-executable-to-a-shared-action"></a>

## 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:

``` yaml
- 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.

<a id="measure-stages-and-retain-acceptance-checks"></a>

## 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:

``` sh
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.

<a id="keep-the-package-nightly-floating"></a>

## 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:

``` yaml
- 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](workflows.md#nightly-compatibility) 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.

