Blueprint.roc declares every tool a contributor and CI need. Blueprint.lock pins them. Both are committed. The blueprint command from roc-blueprint realises that environment, the same way on a contributor’s machine and on every CI runner.

The environment holds the tooling compiler, the tools the scripts call, and the native toolchains a platform host or a test host needs. It does not hold the package compiler, which is never pinned.

Roc packages can use Programs, Fetch and Expect with no Nix source. Follow the complete Roc package adoption guide. Native platforms can declare Nix sources for their host toolchains and sandboxed builds.

Risks and controls for tooling selection§

A contributor and CI can run different tool versions, or a new compiler can break the scripts needed to diagnose the product. Keep tooling identity explicit and separate from the compiler under test. Blueprint recipes and locks select the tooling bytes; local source scripts declare the corresponding compiler. Verify configuration and run the same named tasks locally and in CI. Changing a lock is an admission decision, not a harmless cache refresh. The lock establishes selected identities; it does not establish that the selected upstream code is free of defects.

Declare the environment§

app [config] { pf: platform "<roc-blueprint release URL>" }

config = [
	Name("roc-example"),
	Packages("nix", From(NixPackages("github:NixOS/nixpkgs/nixos-unstable"))),
	Input("roc", "github:roc-lang/roc-overlay"),
	Environment(
		"dev",
		[
			Overlays(["roc"]),
			Command("roc-stable", "nix#rocpkgs.nightly-<date>-<commit>"),
			RocPackages([
				"https://github.com/roc-lang/basic-cli/releases/download/<version>/<hash>.tar.zst",
				"https://github.com/roc-lang/http/releases/download/<version>/<hash>.tar.zst",
			]),
			Tools(["nix#coreutils", "nix#gh", "nix#git", "nix#unzip", "nix#zip"]),
		],
	),
	Shell("default", [Use("dev")]),
	Task("check", [Use("dev"), Run(["scripts/check_all.roc"])]),
	Task("bundle", [Use("dev"), Run(["scripts/build_bundle.roc"])]),
	Task("test-bundle", [Use("dev"), Run(["scripts/test_bundle.roc"])]),
	Task("package-examples", [Use("dev"), Run(["scripts/package_examples.roc"])]),
	Task("package-docs", [Use("dev"), Run(["scripts/package_docs.roc"])]),
	Task("test-published", [Use("dev"), Run(["scripts/test_published.roc"])]),
]

In this example, Packages declares its Nix package source, Input declares the flake input and Overlays selects its role in this environment. Source-qualified names such as nix#gh select the declared package source. Each setting has one job:

Command("roc-stable", ...)

Names the exact nightly release used for tooling. The compiler comes from roc-overlay, which packages recorded nightlies under their release tags. Command exposes that one release as roc-stable without adding the tool’s own executables, so the environment provides no bare roc.

RocPackages

Declares the content-addressed Roc bundles to prepare before scripts start. For tasks and shells, Blueprint downloads missing bundles and uses roc unbundle to verify their URL hashes before publishing them into Roc’s package cache; these task-only bundles have no entries in Blueprint.lock. Sandboxed builds instead take their bundles from provider-locked sources. List every bundle, including dependencies of dependencies; roc-stable deps scripts/check_all.roc prints them. Blueprint does not read package headers and cannot add a missing one.

Tools

Every program a script starts, and every toolchain a build needs: for example pandoc, zig, a Rust toolchain or go. A tool that a script calls and the environment does not declare is a defect, because CI and contributors then run different versions.

Task

A named entry point. CI calls tasks, never script paths, so the workflow and a contributor run the same command.

The two files divide the work. Blueprint.roc states which release is the tooling compiler and which tools are needed, where a reviewer can read it. Blueprint.lock pins the overlay revision that supplies that release’s download URL and hash, package sources, fetched programs and bundle sources used by sandboxed builds. Task-only bundle identities remain in their committed URLs.

Commit the generated lock after review. Derived files and generated Nix stay in .blueprint/, which the repository’s .gitignore lists.

Get blueprint§

Blueprint needs Nix only for Nix-sourced tools and sandboxed builds. For those environments install Nix first. There are two ways to get the blueprint command. Whichever you use, take the same roc-blueprint release as the platform URL in the repository’s Blueprint.roc header.

With Nix§

Nix can run or install blueprint straight from the release tag, with nothing to download or verify by hand:

# run it once, without installing
nix run github:lukewilliamboswell/roc-blueprint/<version> -- run check

# or install it into your profile
nix profile install github:lukewilliamboswell/roc-blueprint/<version>

Nix builds the command from tagged source on first use.

As a release binary§

Each roc-blueprint release also attaches prebuilt blueprint-<system> binaries and a blueprint.sha256 file. Download the one for your system, check it against the published SHA-256, make it executable and put it on PATH.

CI always uses the release binary, through actions/setup-blueprint, which checks it against a reviewed SHA-256 on every run. Nobody builds the CLI from source in CI.

What the command needs§

The binary needs neither Python nor Roc installed. It embeds its fixed configuration compiler, the nightly its release notes record. On first use it installs the embedded archive into its content-addressed store and verifies the recorded hash before extraction. It reuses verified bytes on later runs and repairs a damaged archive from its embedded copy. Bootstrapping this compiler needs no GitHub request; project tools and packages can still need downloads. A compatible explicit ROC override takes precedence, then a compatible host roc. An override must match the compiler required by the Blueprint binary.

blueprint compiler-version reports the bundled compiler tag without evaluating a project, installing a compiler or writing a cache. It describes Blueprint itself even when a host override is selected. The report does not require the repository’s roc-stable pin to match: update that pin separately after testing the scripts that use it.

This compiler is separate from roc-stable for a reason: Blueprint is written in Roc and must run before the environment it describes exists. See Three compilers, three jobs.

Work locally§

A contributor needs blueprint, Nix when their selected tools/builds use it, and a current nightly installed as roc, the way Roc is normally installed. Nothing else has to be set up.

blueprint run check      # run a declared task in the pinned environment
blueprint shell          # or enter the environment and run scripts directly
blueprint tasks          # list the declared tasks

blueprint is the outer command. Run it from an ordinary shell: there is no nix develop step first and no nested shell.

Inside the environment roc-stable is the pinned compiler, and the contributor’s own roc remains visible for package work.

Use the environment in CI§

Every job that runs repository scripts sets up Blueprint first, then installs the latest nightly, then calls a task:

  1. Set up the verified blueprint binary, requesting Nix when needed, with actions/setup-blueprint.

  2. Install the latest nightly as roc with the official roc-lang/setup-roc action; see Install the nightly compiler.

  3. Run blueprint run <task>.

Choose runners for which the selected release has reviewed binaries in actions/setup-blueprint/releases.json, and verify that the declared tools also support those systems. Cross-compiling a host in the builder job does not establish that it runs on the target.

The job needs no cache account and no token for this. A Nix cache is an optimisation a repository may add; a cache that requires id-token: write widens a test job’s token and must be an explicit choice.

A change to Blueprint.lock changes the tools every build uses. Where a repository fingerprints native build inputs, Blueprint.lock is one of the fingerprinted files; see Keep rebuild selection narrow and complete.