Move a compiler§

Nothing moves either compiler automatically. Each moves in one reviewed pull request, with whatever the new compiler breaks fixed in the same request.

To move the product compiler:

  1. Put the nightly’s tag in blueprint-cli/compiler-version.

  2. Replace the entries of compilers in blueprint-cli/Bootstrap.roc (see The bootstrap table) with the nightly’s.

  3. If the locked roc-overlay predates that nightly, run nix flake update roc-overlay. Update compilerArchives in the flake from the official release metadata even if the overlay has not indexed it yet.

  4. Set the overlay and a compiler it actually inventories in fixtures/roc-inputs.lock.json. This Nix-fixture compiler is independent of the product compiler when the overlay inventory lags the official nightly.

  5. Move the basic-cli release in blueprint-cli/main.roc, the fixtures, the examples, scripts/src/SmokeBinary.roc and rocPackages in flake.nix if the nightly needs a newer one.

  6. Move the nightly the manual’s examples and Published.snippet fetch as roc-stable, so that a reader’s first project downloads one archive for both the compiler blueprint uses and the one its scripts run on.

  7. Set the hash of rocGlueSpec in flake.nix to that of the nightly’s source archive, which nix build .#roc-glue-spec reports, and run scripts/check_glue.roc --update. Commit the bindings it changes together with whatever the host needs to build against them.

The bootstrap table§

A project has no lock until its Blueprint.roc has been read, so the digest of the compiler that reads it cannot come from one. blueprint carries it: compilers in blueprint-cli/Bootstrap.roc holds, for the nightly in blueprint-cli/compiler-version and each system blueprint runs on, the URL of the compiler archive in roc-lang/nightlies, its SHA-256, its size, its format and the path of roc inside it.

SystemArchive of the nightly
x86_64-linuxroc_nightly-linux_x86_64-<date>-<commit>.tar.gz
aarch64-linuxroc_nightly-linux_arm64-<date>-<commit>.tar.gz
aarch64-darwinroc_nightly-macos_apple_silicon-<date>-<commit>.tar.gz
x86_64-darwinroc_nightly-macos_x86_64-<date>-<commit>.tar.gz
x86_64-windowsroc_nightly-windows_x86_64-<date>-<commit>.zip

gh release view <tag> --repo roc-lang/nightlies --json assets reports each asset’s digest and size. The entries are data and nothing else: no rule derives a URL. scripts/test.roc static fails until every system blueprint runs on has an entry of the nightly in compiler-version.

With ROC unset and no matching roc on PATH, blueprint installs its system’s embedded archive into the per-user store and checks it against the entry before using it; it starts no provider to read a Blueprint.roc. The entry names the same download as Recipes.roc_nightly of that tag, so a project that fetches that nightly as roc-stable holds it once, and blueprint update records its digest from the store without a second download.

To move the scripts' compiler, change rocStableTag in flake.nix and the roc: "<tag>" field in the header of each scripts/*.roc to the same tag; scripts/test.roc static names any script that differs. Move the basic-cli release in those headers too if the nightly needs a newer one. A compiler other than the one a header names refuses the script, and roc fmt rewrites the field to the nightly that runs it, so the scripts are formatted with roc-stable fmt scripts. scripts/smoke_binary.roc and scripts/stubs/tool.roc are the exceptions: the product compiler builds them, so their headers must suit both compilers and carry no roc field.

The required checks are listed under Repository settings the workflows rely on. Keep their names aligned with the workflows when changing CI.

basic-cli 0.26.0-rc2 requires the October 12 nightly or later. This repository pins nightly-2026-10-28-bad769b for the product and nightly-2026-10-13-b17d287 for maintenance scripts, with roc-fuzz 0.4.3.

The complete suite requires x86_64 Linux and a running Nix daemon; the development shell supplies the rest. zig build builds the platform host for all five targets, so on Apple Silicon macOS the CLI’s tests, the native CLI and the evaluation of a Blueprint.roc work too. Sandboxed builds, and so the scenarios, builds and package groups, remain x86_64 Linux only.

Release§

One release holds the platform bundle, the roc-blueprint-core bundle it depends on, the standalone Roc SBOM reporting app as a source bundle and as a blueprint-sbom-<system> binary per system, the blueprint binaries, the checksums of both kinds of binary, the examples archive, the documentation archive and the manual. Each executable and Roc source bundle has artifact-bound SBOM sidecars: SPDX 3.0.1 (.spdx.json), SPDX 2.3 (.spdx-2.3.json) and CycloneDX 1.7 (.cdx.json). It is made by .github/workflows/release.yml, run by hand from main with a version. Every pull request runs the same workflow as a dry run, under the version 0.0.1-dry-run, which is never published.

  1. In one pull request, move the product compiler to the latest official Roc nightly using the steps above. Resolve any compatibility failures and run the complete suite, including native and Nix-free checks on all five released systems. Set version in blueprint-cli/main.roc and flake.nix to the release, and write its notes (see Write the release notes). Merge it.

  2. Run the Release workflow from main with that version, for example 0.5.0, or 0.5.0-rc1 for a release candidate. A version with a - in it is published as a pre-release.

  3. Approve the release environment if it asks.

A release candidate reports its own version, so version must be set to X.Y.Z-rc1 for it and to X.Y.Z for the final release: the two are different commits. Both use the notes in docs/releases/X.Y.Z.adoc.

What the workflow does§

validate

Requires the default branch, then scripts/check_version.roc request requires a well-formed version, its notes, that no tag or release of it exists, that it is the version the CLI and the flake report, and that the product compiler pin names the latest official nightly.

build

Cross-builds the CLI and SBOM tool binaries (scripts/build_release.roc), runs the whole test suite on them, bundles the core, platform and reporting app for the version (scripts/bundle.roc), checks the version against the public API (scripts/check_version.roc bump), archives the examples (scripts/package_examples.roc) and the SBOM fixture set with its case contract (scripts/package_sbom_fixtures.roc), renders the manual with the release’s platform URL and archives the site (scripts/docs_snippet.roc, scripts/package_docs.roc), renders the notes (scripts/release_notes.roc), generates artifact-bound SPDX and CycloneDX sidecars (scripts/release_sbom.roc), independently validates their schemas, subjects, known inputs and agreement on one package graph, and gathers the assets (scripts/release_assets.roc). On a pull request the test suite is left to ci.yml.

test

Runs each binary on its own kind of machine: the CLI through the smoke program, and the SBOM tool over the committed fixtures (.github/actions/sbom-tool: inspect, bind and check, with no Roc on the machine). scripts/test_release.roc tests the examples archive there against the two bundles.

publish

The only job that may write to the repository. It checks out nothing and runs no script of the repository. It attests the tested assets and verifies the source and workflow identity of all five native binaries, retaining the attestation bundle as a release asset. It creates a draft release at the tested commit, uploads every asset, downloads each back and compares it with the tested file, and only then publishes. It rechecks the latest nightly before publication. If a newer nightly appeared during the build, the job fails and leaves the draft unpublished: update the compiler pin and repeat the tests before releasing. It does not run on a pull request.

site

For a final release, rebuilds and deploys the manual, which then names the new release. A release candidate does not change the site.

The machines that run the arm64 Linux and macOS binaries have Nix and deliberately no Roc, so build_release.roc also cross-builds scripts/smoke_binary.roc for each system and they run that (dist/smoke-<system> dist/blueprint-<system>). The project the smoke test writes pins what it resolves: packages at the nixpkgs revision in fixtures/consumer/inputs.lock, the overlay at the roc-overlay revision in flake.lock and the compiler at the nightly in blueprint-cli/compiler-version, all compiled into the smoke program. A release therefore does not depend on what moved upstream that day. .github/workflows/floating.yml runs the same test weekly with --floating, which resolves what a new user’s project would: a branch of nixpkgs, the overlay’s default branch and its newest nightly. With --no-nix the same program writes a project that takes nothing from Nix and requires a machine that has none; the no-nix job of ci.yml runs it on all five released systems (see The job with no Nix).

build_release.roc takes its compiler from the flake: nix build .#roc-cross is the x86_64 Linux compiler archive of the nightly in blueprint-cli/compiler-version with the darwin/ sysroot of the Apple Silicon archive beside it, which Roc needs to link macOS programs. Both are fetched by the URL and hash the locked the flake records. The cross compiler is not in the development shell.

The CLI binaries are compiled sequentially. Before each compilation, scripts/embed_compiler.roc <system> stages that target’s complete archive as blueprint-cli/compiler-archive, after checking its size and digest against Bootstrap.roc. Downloads are cached in .cache/compilers/; the staged archive is ignored by Git. The local host’s archive is restored afterwards, including when compilation fails. The SBOM tool embeds no compiler, so its five binaries compile in parallel with the smoke programs.

Each binary embeds only its own system’s archive. On first use it installs that archive into the existing per-user store and verifies its recorded digest before extraction; see The bootstrap table. This removes the compiler download at runtime. Project packages and tools can still require the network. Release binaries grow by roughly 69–73 MB.

Write the release notes§

Write the release’s notes by hand in docs/releases/X.Y.Z.adoc and include the file in docs/releases.adoc, newest first; scripts/test.roc static fails when the two disagree. The notes say what changed for a user deciding whether and how to upgrade: what a user can or must now do, the symptom each fix removes, and before and after for anything breaking. They are not a list of commits.

The file opens with = roc-blueprint X.Y.Z and names the bundles as {platform-url} and {core-url}, because the URLs hold hashes that do not exist until the release builds them. scripts/release_notes.roc X.Y.Z dist/notes.md renders the notes as the release body with the real URLs, and adds what the release was built from: the Roc nightly and the Zig version, the roc-overlay revision and the hashes of the two official compiler archives, the flake’s nixpkgs revision, the core bundle, the standalone SBOM tool as the blueprint-sbom-<system> binaries (the recommended way for CI to run it) and as its app bundle URL, the Roc packages compiled into the CLI as roc deps blueprint-cli/main.roc resolves them, and the linker-input release with the SHA-256 of link-inputs.lock.json. It fails on a placeholder that is unused or unknown. The committed file keeps its placeholders and is not rewritten after the release. A dry run renders the newest notes there are.

The two bundles§

During development the platform uses core: "../blueprint-core/main.roc". roc bundle only packs files below the entry point’s directory, so scripts/bundle.roc [VERSION] bundles the core package first, then a staged copy of the platform whose core: is the URL that core bundle will have in the release of VERSION. With no version it is the dry run.

Roc identifies a package by its URL minus the version and hash, so two archives named only by their hashes under one tag would look like one package served with two hashes, and the compiler refuses that as conflicting package contents. The core archive is therefore published as roc-blueprint-core-<hash>.tar.zst: the prefix is part of its identity. The platform’s archive keeps the name roc bundle gave it, <hash>.tar.zst.

The release does not exist while the bundles are tested, so the script serves both itself, from a port the operating system assigns, at the paths the release will give them. The compiler is told to load the core bundle from there (--replace-dep) while it checks a few of the configuration fixtures and runs examples/all-settings/Blueprint.roc. It gets an empty package cache for this, so it must download both bundles. The modules and linker inputs packed are those in the directories and in the targets: section of blueprint-platform/main.roc.

Before bundling the platform, scripts/bundle.roc makes the check of scripts/link_inputs.roc check, so only linker inputs matching link-inputs.lock.json are packed. Their licences and dependency.json inventory go into the bundle under linker-inputs/.

scripts/test.roc package runs the dry run on every commit, with the examples archive and its test. A change that adds a Spec field needs nothing more: the core bundle carrying the field is released with the platform that uses it.

The reporting tool: binaries and app bundle§

scripts/build_release.roc cross-builds scripts/sbom.roc as blueprint-sbom-<system> for every released system, beside the CLI binaries and in the same blueprint.sha256, after the CLI’s lines. The product compiler builds it, so like the smoke program it names no roc field in its header (product_built in scripts/src/StaticChecks.roc); roc-stable builds the same file for the unit tests. Each binary is smoke-tested on its own kind of machine (.github/actions/sbom-tool) and has its own sidecars, which name the wrapper, the core and the basic-cli host and record that no compiler archive is embedded. The release notes name the binary as the recommended way for CI to run the tool.

The same bundling command stages scripts/sbom.roc and its filesystem helper as a standalone app, using the released core URL and adding the product compiler pin to its header. Its archive is named roc-blueprint-sbom-<hash>.tar.zst, so it has a distinct package identity. A separate smoke test downloads that app and exercises inspect and bind in a directory with an invalid Blueprint.roc; neither operation may evaluate the project. The release notes include its immutable URL. See Artifact-bound software bills of materials for its supported inputs and coverage contract.

The version check§

scripts/check_version.roc bump runs roc bump for both packages against the latest release, and a release fails unless its version bumps at least as far as the public API changed. The core package is compared from the working tree. The platform is compared as bundled: in the working tree it names the core package by path, which roc bump refuses in a public type, so the script unpacks the platform bundle and puts the unpacked core bundle in a package cache of its own, where the compiler finds it by hash although its URL does not exist yet. A dry run reports the required bump and expects nothing.

The platform exposes a type of the core package, and a package’s URL is part of that type’s identity, so the two versions move together.

Asset names other repositories depend on§

The names of the binaries and of their checksum file are a contract. roc-automation’s setup-blueprint action downloads blueprint-x86_64-linux, blueprint-aarch64-linux, blueprint-aarch64-darwin, blueprint-x86_64-darwin and blueprint-x86_64-windows.exe from the release a caller names, exactly as published. Callers copy the SHA-256 values from blueprint.sha256. Renaming an asset, archiving the binaries or changing the checksum file breaks every caller. scripts/release_assets.roc fails the build unless exactly those six names and the five blueprint-sbom-<system> binaries are present, and the publish job checks again before it uploads. A binary for another system is an addition; it does not change these.

blueprint.sha256 lists the five CLI binaries first, then the five SBOM tool binaries. The setup action never reads the file: it admits blueprint-<system> by a digest reviewed into its own releases.json, so the lines that follow the CLI’s are safe for it, and a maintainer copying digests takes the line of the binary’s name. The CLI’s lines keep their order and place.

The platform bundle is the one .tar.zst asset named by its hash alone, and the core bundle the one that starts roc-blueprint-core-. The manual and the version check find the previous release’s bundles that way.

roc-blueprint-sbom-fixtures-<version>.zip is the SBOM fixture set of tests/sbom/fixtures/ with its CONTRACT.md, which states what each case proves and the exact refusal each tampered input earns. roc-automation tests its SBOM actions against it with the release’s standalone tool (roc-blueprint-sbom-<hash>.tar.zst), so a case may be added but not renamed or removed without a note in the release’s notes. scripts/package_sbom_fixtures.roc fails unless the archive holds exactly the files the SBOM suite asserts, and the suite fails when the contract, the committed inputs or the asset’s file list drift from the cases. See Exercise the stable consumer contract.

Verify release provenance§

New releases attach roc-blueprint-provenance.sigstore.json and publish GitHub attestations for the exact tested artifacts. The signer is this repository’s release workflow, running from main on GitHub-hosted runners. The attestation describes the build in that workflow run; it does not claim a SLSA assurance level or reproducible builds.

For example, after downloading a release’s binary and checksum file:

sha256sum --ignore-missing -c blueprint.sha256
gh attestation verify ./blueprint-aarch64-darwin \
  --repo lukewilliamboswell/roc-blueprint \
  --signer-workflow lukewilliamboswell/roc-blueprint/.github/workflows/release.yml \
  --source-ref refs/heads/main \
  --source-digest <release-commit> \
  --signer-digest <release-commit> \
  --deny-self-hosted-runners

Replace <release-commit> with the full source commit of the immutable release. The verification checks that the subject digest matches the file you downloaded. Review the source revision and producing workflow before committing the binary’s SHA-256 and URL in a downstream lock or registry.

To verify using retained evidence, add --bundle roc-blueprint-provenance.sigstore.json to that command. An air-gapped verifier also needs a previously retained trusted root, supplied through --custom-trusted-root; see gh attestation verify --help.

Verification belongs to release publication and deliberate adoption. Routine builds rehash their downloaded and cached files against the reviewed pin locally. They need no attestation lookup. Existing releases, including 0.6.0-rc1, are unchanged and gain no retrospective provenance.

Repository settings the workflows rely on§

These are set by hand in the repository’s settings.

Every workflow sets permissions: {} and states permissions per job, every checkout sets persist-credentials: false, and every action is pinned to a full commit; scripts/test.roc static checks the first two.

To adopt a newer basic-cli release, change the URL in blueprint-cli/main.roc and fixtures/consumer/main.roc, then update the matching entry in rocPackages in flake.nix with the new URL and hash (nix store prefetch-file <url>). Add any new transitive dependency to the same list. Rerun scripts/test.roc. If you change the weaver URL, update rocPackages in flake.nix to match.

What remains for Windows§

Issue #40 asks for blueprint run on Windows. What needs nothing from upstream is in place: a task whose environment has no Nix tool runs with no provider, from fetched programs, expected tools and Roc packages, and the planner, the store’s location and Recipes.roc_nightly have their Windows forms. The Windows target and release binary are built in CI; the Nix-free smoke test runs on Linux, macOS and Windows.

The upstream prerequisites are now released: basic-cli 0.26.0-rc2 supplies its Windows MSVC host and runtime, the pinned Roc nightly links from listed inputs, and the locked roc-platform-template-zig release supplies this platform’s startup and import libraries.

zig build builds targets/x64win/host.lib; the platform bundle carries it and the verified Windows linker inputs. scripts/build_release.roc builds blueprint-x86_64-windows.exe and its smoke program. The no-nix CI job runs that program on windows-latest, fetching the compiler, updating a lock, reporting requirements and building and running a basic-cli program twice. Windows skips chmod and exposes fetched programs as copies, since a symbolic link needs a privilege there and a hard link would let a consumer write into the store. Archives use the machine’s bsdtar.

The full Nix suites remain Linux-only. Windows 10 and 11 desktop execution has not been verified here; the CI runner checks Windows Server. Tasks must invoke their interpreter explicitly, for example Run(["roc-stable", "hello.roc"]).

Upstream workarounds still in place§

Compile-time configuration lowering is restored with Roc nightly-2026-09-23-c7852fd: roc check Blueprint.roc now rejects whole-config errors, including a missing Name or duplicate shells. Older compilers crashed while bundling a top-level constant dependent on the app’s config; the local-Spec and released-Spec bundle tests guard against that regression. blueprint check retains a compiler check for diagnostics and reuses the Spec already loaded for CLI parsing, rather than running the configuration again. The loaded Spec is still needed for shared semantic validation and provider compatibility checks. Runtime validation also protects consumers from external Spec; it does not make major-1 platforms compatible with the current CLI. Loading the Spec still executes Roc, so retain embedded compiler bootstrap and support for the ROC override. The Nix wrapper supplies the archive tools bootstrap needs, including on a machine whose PATH holds only Nix.

These remaining dependencies and workarounds still apply: