> Documentation version: main. [HTML page](https://lukewilliamboswell.github.io/roc-blueprint/maintaining.html)

<a id="maintain-and-release"></a>

# Maintain and release

<a id="move-a-compiler"></a>

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

<a id="the-bootstrap-table"></a>

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

<a id="release"></a>

## 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](#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 `.

<a id="what-the-workflow-does"></a>

### 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](contributing.md#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](#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.

<a id="write-the-release-notes"></a>

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

<a id="the-two-bundles"></a>

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

<a id="the-reporting-tool-binaries-and-app-bundle"></a>

### 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](sbom.md#artifact-bound-software-bills-of-materials)
for its supported inputs and coverage contract.

<a id="the-version-check"></a>

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

<a id="asset-names-other-repositories-depend-on"></a>

### Asset names other repositories depend on

The names of the binaries and of their checksum file are a contract.
roc-automation’s
[` setup-blueprint `](https://lukewilliamboswell.github.io/roc-automation/actions.html#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](sbom.md#exercise-the-stable-consumer-contract).

<a id="verify-release-provenance"></a>

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

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

<a id="repository-settings-the-workflows-rely-on"></a>

### Repository settings the workflows rely on

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

- A ` release ` environment, restricted to ` main `. The publish job names it.


- GitHub Pages with **GitHub Actions** as its source, and the ` github-pages `
  environment restricted to ` main `. ` docs.yml ` deploys to it.

- Immutable releases, and a tag ruleset under which a version tag cannot be
  moved or deleted.

- The ` main ` ruleset requires a pull request, signed commits, and these
  checks with the branch up to date: ` test ` (the aggregate job of ` ci.yml `),
  ` Release dry run ` (that of ` release.yml `) and ` Build 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.md ` links 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.

<a id="what-remains-for-windows"></a>

## What remains for Windows

[Issue \#40](https://github.com/lukewilliamboswell/roc-blueprint/issues/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"]) `.

<a id="upstream-workarounds-still-in-place"></a>

## 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-version ` selects the compiler, and the basic-cli
  release in ` blueprint-cli/main.roc ` must be compatible with it.

- **` roc bundle --output-dir ` must be on the same filesystem as the working
  directory.** Otherwise the bundler fails with ` CrossDevice `.

- **Error unions in ` blueprint-core/Sexpr.roc ` use a named extension
  (` ..others `).** These preserve open error types without the compiler
  warning about a redundant bare ` .. `.

