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:
Put the nightly’s tag in
blueprint-cli/compiler-version.Replace the entries of
compilersinblueprint-cli/Bootstrap.roc(see The bootstrap table) with the nightly’s.If the locked roc-overlay predates that nightly, run
nix flake update roc-overlay. UpdatecompilerArchivesin the flake from the official release metadata even if the overlay has not indexed it yet.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.Move the basic-cli release in
blueprint-cli/main.roc, the fixtures, the examples,scripts/src/SmokeBinary.rocandrocPackagesinflake.nixif the nightly needs a newer one.Move the nightly the manual’s examples and
Published.snippetfetch asroc-stable, so that a reader’s first project downloads one archive for both the compilerblueprintuses and the one its scripts run on.Set the hash of
rocGlueSpecinflake.nixto that of the nightly’s source archive, whichnix build .#roc-glue-specreports, and runscripts/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.
| System | Archive of the nightly |
|---|---|
x86_64-linux | roc_nightly-linux_x86_64-<date>-<commit>.tar.gz |
aarch64-linux | roc_nightly-linux_arm64-<date>-<commit>.tar.gz |
aarch64-darwin | roc_nightly-macos_apple_silicon-<date>-<commit>.tar.gz |
x86_64-darwin | roc_nightly-macos_x86_64-<date>-<commit>.tar.gz |
x86_64-windows | roc_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.
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
versioninblueprint-cli/main.rocandflake.nixto the release, and write its notes (see Write the release notes). Merge it.Run the Release workflow from
mainwith that version, for example0.5.0, or0.5.0-rc1for a release candidate. A version with a-in it is published as a pre-release.Approve the
releaseenvironment 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 requestrequires 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 toci.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,bindandcheck, with no Roc on the machine).scripts/test_release.roctests 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.
A
releaseenvironment, restricted tomain. The publish job names it.GitHub Pages with GitHub Actions as its source, and the
github-pagesenvironment restricted tomain.docs.ymldeploys to it.Immutable releases, and a tag ruleset under which a version tag cannot be moved or deleted.
The
mainruleset requires a pull request, signed commits, and these checks with the branch up to date:test(the aggregate job ofci.yml),Release dry run(that ofrelease.yml) andBuild the manual and site(docs.yml).The default workflow token is read-only, and GitHub Actions may not create or approve pull requests.
Private vulnerability reporting is enabled;
SECURITY.mdlinks to it.
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:
Pinned Roc.
blueprint-cli/compiler-versionselects the compiler, and the basic-cli release inblueprint-cli/main.rocmust be compatible with it.roc bundle --output-dirmust be on the same filesystem as the working directory. Otherwise the bundler fails withCrossDevice.Error unions in
blueprint-core/Sexpr.rocuse a named extension (..others). These preserve open error types without the compiler warning about a redundant bare...