A consistent layout lets the same checks, scripts and workflows apply to every repository, and lets a maintainer who knows one repository find their way in another.

Risks and controls for repository source boundaries§

A test can accidentally import a published dependency instead of the changed working tree, while source automation can accidentally depend on the broken product it must diagnose. Make source roles explicit. Product examples use relative working-tree dependencies; source tooling uses independently selected released dependencies and a fixed compiler. Header and source-policy checks verify those conventions, and behavioral tests verify the product itself. Static policy establishes declared structure, not which code paths execute or whether a package is correct.

The layout§

Blueprint.roc        development environment and named tasks
Blueprint.lock       pinned inputs
package/main.roc     a package; or platform/main.roc for a platform
examples/            applications that depend on the working tree
scripts/*.roc        executable automation entry points
scripts/src/*.roc    modules shared by those scripts
tests/               package-specific cases and expected output
docs/*.adoc          the manual, where the repository has one

A repository that publishes several packages or platforms gives each its own root directory, such as package/ and tzdb/package/, or platform-web/ and platform-gui/. Each root is bundled, versioned and released together with the others in one release; see Several bundles in one release.

Where a compiler pin belongs§

The roc: "..." header field appears in exactly one place: the scripts under scripts/, where it names the same release as roc-stable. Nothing else is pinned. The package, a platform, the examples and every other Roc application carry no roc field, and the repository has no .roc-version file and no .github/roc-nightly.json.

The rule for the package applies to platforms as much as packages. roc check exits with an error on a warning, and a dependency whose header pins a different nightly produces a version-mismatch warning. A pinned platform therefore fails every application that floats, and a package cannot float until every platform its examples use has dropped the field.

The check script enforces both halves. It fails when a roc field or .roc-version appears outside scripts/, and it fails when a script’s pin differs from the tooling compiler; see Keep the script pins in step.

Roc applications in the repository§

Every Roc application in the repository depends on the working tree by relative path. Pull-request CI checks them with the latest nightly compiler. That covers examples/ and also anything outside it: fuzz targets, benchmarks, documentation examples, test cases, and a site generator.

In the repository: the package is a relative path
app [main!] {
	cli: platform "https://github.com/roc-lang/basic-cli/releases/download/<version>/<hash>.tar.zst",
	ansi: "../package/main.roc",
}

The package is a relative path. Every other dependency, including the platform, is an immutable release URL with its content hash.

These are not what the nightly workflow tests. They change with every pull request and may use APIs that no release has. The nightly workflow runs the examples archive attached to the latest release. In the archive the same example names that release’s exact bundle URL, and nothing else differs:

In a release’s examples archive: the package is that release’s bundle
app [main!] {
	cli: platform "https://github.com/roc-lang/basic-cli/releases/download/<version>/<hash>.tar.zst",
	ansi: "https://github.com/<owner>/roc-ansi/releases/download/<tag>/<hash>.tar.zst",
}

The release workflow writes the second form when it builds the archive; the repository only ever contains the first. See The examples archive.

The single exception is scripts/. A script never depends on the repository’s own package by relative path, because a script must keep working while the package is broken.

An example that needs more than one file is a directory: examples/report/ holds main.roc, its sibling modules and any data files it reads. A local helper package that several examples share sits beside them and is referenced by relative path.

Use shared utilities through Blueprint tasks§

Use a released executable for an operation shared across repositories, with its recipe and named task in Blueprint. Local use and CI invoke the same task. The shared utility skeleton shows a working consumer and the producer boundaries. A consumer of a prebuilt tool does not need that tool’s source dependencies or implementation compiler.

Write a local script only for behavior or composition specific to the product. Keep common validation, example execution and release orchestration in their shared utilities rather than copying helper modules between repositories.

Write product-specific automation as Roc scripts§

A source entry point kept under scripts/ is an executable Roc application:

#!/usr/bin/env roc-stable
app [main!] {
	cli: platform "https://github.com/roc-lang/basic-cli/releases/download/<version>/<hash>.tar.zst",
	roc: "nightly-<date>-<commit>",
}

Product orchestration scripts are written in Roc. Small platform adapters and independent test harnesses may use the runtime their documented contract requires. A script that only wraps one command is replaced by a Blueprint Task that runs that command directly.

Keep the script pins in step§

A script is written for one compiler and one set of dependencies. Someone who runs it with a newer roc, instead of through blueprint, may find it broken for reasons that have nothing to do with their change. The roc field makes the compiler say so: a compiler other than the one the field names refuses the file and names the release it targets, which points at the cause before anyone debugs the script.

The field is useful only while it is true, so one cheap check keeps it true. The check script reads the tag from the Command("roc-stable", ...) setting in Blueprint.roc and requires every file under scripts/ that has an app header to carry exactly that tag in its roc field. It reads text and runs no compiler, so it costs nothing in pull-request CI.

Two things follow from the compiler enforcing the field:

Blueprint.roc remains the authority. The header fields are copies that the check holds to it, and Change the tooling compiler changes them together.

The check script§

One check task owns everything a pull request must pass. It uses shared checking utilities; a product-specific driver, conventionally scripts/check_all.roc, can compose additional checks when needed. Its scope includes the static checks in this chapter, roc fmt --check and roc-stable fmt --check, the script tests, roc test on the package root, every example and integration case, a smoke run of each benchmark and fuzz target, and a dry-run build of what a release would publish. What runs on every pull request describes each.

What is committed§

CommittedNot committed
Source, Blueprint.roc, Blueprint.lock.blueprint/ and anything else blueprint generates
Expected output and golden files under tests/Build output, bundles, archives
Generated data the package ships, with the recipe and inputs that produced itRendered API documentation and the built site
A platform’s generated host bindingsNative binaries, which are released independently and consumed through a content lock; see No binary blobs in the repository

Generated data that is committed has a script that regenerates it and a check that fails when the committed copy is stale. Rendered documentation is published as a release asset and a Pages deployment, never committed.

What goes in a bundle§

A bundle holds what a user of the package needs and nothing else: the package’s Roc modules and any data files they import. Fuzz targets, benchmarks and test modules stay outside the package root, so that roc bundle on the root produces the release artifact with no exclusion list.