> Documentation version: 3. [HTML page](platforms.html)

<a id="maintain-a-platform"></a>

# Maintain a platform

A platform follows the same model as a package: the same environment, layout,
examples archive and three workflows, and no compiler pin in the platform or its examples. This
chapter covers what a platform adds, which is a native host. A host brings
generated bindings, a target matrix, and build inputs that are too expensive or
too sensitive to rebuild on every pull request.

<a id="risk-native-targets"></a>

## Risks and controls for native targets

A cross-compiled host can link but fail on its advertised OS, and stale glue
can disagree with the compiler’s ABI. Treat native execution and interface
identity as separate evidence. Check generated bindings against the selected
nightly, bind native inputs to their source and target, and run the exact bundles
on every supported runner. Include a consumer link/run test rather than relying
on archive presence. The target matrix establishes those exercised combinations;
it does not establish that native host code or FFI behavior is memory-safe or
that every downstream application is compatible.

<a id="host-bindings"></a>

## Host bindings

A host is compiled against bindings that ` roc glue ` generates from
` platform/main.roc `: a Zig, Rust or C description of the types and entry points
the Roc side expects. The compiler generates them, so a new nightly can change
them when nothing in the repository changed.

The bindings are committed. The host build reads the committed file and never
runs ` roc glue ` itself, so a host builds the same way on every runner and a
reviewer sees every change to the interface as a diff.

<a id="check-the-bindings-in-pull-request-ci"></a>

### Check the bindings in pull-request CI

` ci.yml ` has one job that regenerates the bindings with the latest nightly and
requires the committed file to match:

1. Obtain the glue specification that belongs to the resolved nightly, from that
   nightly’s source archive, and verify the archive’s digest.

2. Run ` roc glue <spec> <output directory> platform/main.roc ` with the latest
   nightly.

3. Run ` git diff --exit-code ` on the committed bindings.


This check runs on pull requests and on pushes to the default branch. It does
not run in ` nightly.yml `: a released host is fixed, and what
[nightly compatibility](workflows.md#nightly-compatibility) asks is whether
applications still build and run against it.

The job is part of the required aggregate check. When a nightly changes the
generated bindings, every open pull request fails this job until the bindings
are upgraded. That is intended: the default branch should not accept changes on
top of an interface that no longer matches the compiler.

<a id="upgrade-the-bindings"></a>

### Upgrade the bindings

Regenerating the bindings is a manual task, because a changed interface usually
needs host code to change with it.

1. Run the bindings script with its update flag, using the latest nightly.


2. Review the diff. A changed layout or signature needs matching host changes.


3. Build the host for every target and run the examples.


4. Open a pull request containing the bindings and any host changes. Merge after
   ` ci.yml ` passes.

5. If the released platform no longer works with the new nightly,
   ` nightly.yml ` has already reported it. Release a new platform version.

<a id="cross-compile-once-run-on-every-target"></a>

## Cross-compile once, run on every target

A platform advertises a set of targets, and each workflow validates all of
them. The usual shape is one builder job that cross-compiles for every target,
followed by a matrix of runner jobs that execute the result on each one.

**One builder, a matrix of runners**

![One builder, a matrix of runners](images/diagram-e5246aecf89b7145ca974feb874ca1304f444069f98d28f585f9cccc1ae182ba.svg)

<a id="the-builder-job"></a>

### The builder job

- One job, on one runner, cross-compiles the host for every supported target.
  A single toolchain version then produces every host, and the expensive work
  happens in one place where its cache is easy to manage.

- It assembles the hosts with the locked linker inputs and bundles once, with
  the repository’s own bundle command. The shared
  [` release-package `](actions.md#release-package-platform) utility then
  runs the examples against the working tree, and its
  ` build VERSION --platform-bundle DIR ` checks that bundle, names it, and writes
  the examples archive, documentation and notes beside it. ` dist/ ` is uploaded
  as a workflow artifact.

- Where a target cannot be cross-compiled, because it needs an SDK or a
  toolchain that exists on one operating system only, that host is built by a
  job on that system and handed to the builder as an artifact. This is the
  exception to plan around, not the default.

<a id="the-runner-jobs"></a>

### The runner jobs

- One job per supported target downloads the bundle and runs applications
  against it. A runner job never rebuilds the host or the bundle, so every
  target tests the same bytes, and those are the bytes a release publishes.
  ` blueprint run release-test -- test-archive ` runs the archived examples,
  unchanged, against the bundle served from a loopback address with an empty
  package cache, as a first user would fetch it. Blueprint and the utility run
  on Windows too, so the Windows runners use the same task.

- One aggregate job requires every runner job to have passed. Its name is the
  required status check.

- Every target the platform’s ` targets ` section lists has a runner that
  executes it: ` macos-15-intel ` for ` x64mac `, ` macos-15 ` for ` arm64mac `,
  ` ubuntu-24.04 ` and ` ubuntu-24.04-arm ` for the Linux targets, ` windows-2025 `
  for ` x64win `. A target without one is cross-compiled only, and the
  platform’s README says so beside its list of supported targets, so that a
  user can tell a target that was linked and run from one that only compiled.

The purpose of the matrix is narrow: to show that the host and the linker
inputs for each target link and run correctly there. Cross-compilation proves a
host compiles for a target; only running on that target proves that it links,
starts, and talks to the operating system as expected.

<a id="what-the-runners-run"></a>

### What the runners run

Run the examples. They exercise the platform through its public API the way an
application does, and between them they usually reach enough of the host to
show that a target works. They are already written, already have recorded
expected output, and are what a user will try first.

Add dedicated tests where the examples leave a gap: a host service no example
uses, a code path that differs per operating system, a failure that should be
reported in a particular way. Keep them few and aimed at the host, because
everything in the matrix runs once per target.

What does not belong in the matrix is anything that does not depend on the
target. Unit tests of pure Roc code, formatting and the static checks run once,
on one runner.

<a id="runner-environments"></a>

### Runner environments

- State each runner’s runtime needs in the workflow: a display server, a
  software GPU, an audio sink, a browser. A simulated environment is reported as
  simulated; it is not evidence about the real one.

- An example that can be built but not run on a hosted runner is built and
  checked, and its expected-output specification records that it is not run.

The native toolchains come from ` Blueprint.roc `, as described in
[Declare the environment](environment.md#declare-the-environment), so a
contributor and every CI job compile the host with the same versions.

<a id="several-platforms-in-one-repository"></a>

## Several platforms in one repository

A repository that publishes more than one platform, such as a web platform and
a native platform that share an engine, gives each its own root, its own bundle
and its own examples archive, as in
[Several bundles in one release](workflows.md#several-bundles-in-one-release).
Sharing an engine does not make two platform APIs or their application roots
interchangeable. Give examples separate roots and package URLs even when they
share application modules.

<a id="template-repositories"></a>

## Template repositories

A template’s examples are the product: they are what a user copies. A template
follows the model without exception. Its examples depend on the working tree by
relative path, and each release’s examples archive is the starter a user
downloads.

<a id="native-build-inputs"></a>

## Native build inputs

Platforms need native builds before Roc can link an application, and native
builds are where CI time goes. A maintainer has a limited budget of runner
minutes and of patience, and spends it well only by planning what gets built
when, and by watching how that changes as the project grows. This section is
about building each thing once, reusing it until it changes, and testing and
publishing the same bytes.

<a id="no-binary-blobs-in-the-repository"></a>

### No binary blobs in the repository

As a general rule, a platform repository does not commit binaries. A committed
library or object file has no visible origin: a reviewer cannot tell what
source it came from, what built it, or whether it changed for a reason.

Binaries such as linker inputs are built by their own workflow, independent of
ordinary CI, and published as an immutable release that carries:

- the archives for every supported target;


- a build-provenance attestation for each of them;


- the licenses of everything inside; and


- an SBOM.


The software supply chain can then be followed and verified end to end: from a
source commit, through the workflow run that built it, to the exact bytes a
platform links.

Routine use goes through content hashes. The repository commits a lock that
names each archive, its release and its SHA-256, and every build verifies the
bytes against that lock. A hash check is local and fast, it makes the archives
safe to cache, and it needs no network service. The attestation API is reserved
for the moments that need it: admitting a new release into the lock, auditing
one, or an end user who wants to verify what they downloaded. It is not called
on every build.
[Separate admission from routine use](artifact-verification.md#separate-admission-from-routine-use)
sets out that division.

There are exceptions. A platform may depend on libraries that another project
already publishes, for example the prebuilt libraries of a graphics library it
wraps. Those may be vendored in the repository, so that builds do not make
repeated network calls on someone else’s project. A vendored binary records
where it came from: the upstream project, the release, the download URL, its
hash and its license. It is the upstream project’s published artifact,
unmodified, and never something the repository built and then committed.

<a id="two-releases-three-build-steps"></a>

### Two releases, three build steps

A platform’s host and its Roc source usually change together and are released
together: one platform release holds both. Linker inputs are different. They
change rarely, cost the most to build, and are published as their own release.

So there are two kinds of release, and three things that are built:

| What is built | How it is released | In an ordinary pull request |
| --- | --- | --- |
| Linker inputs | Their own immutable release, selected by a content lock | Download the locked archives and verify their hashes. Never rebuild them. |
| The host, such as ` libhost.a ` | As part of the platform release | Build it in its own job, and skip that job when the host’s fingerprint has not changed. |
| The platform bundle | The platform release: the Roc source with the host and linker inputs | Assemble from the outputs above and test. Bundling compiles nothing native. |

The host is not given a release lifecycle of its own. It is given a job of its
own. The point of the separation is that each step can be skipped when its
inputs have not changed.

<a id="skip-what-has-not-changed"></a>

### Skip what has not changed

Compute a content fingerprint of everything that can affect the host: its
source, generated bindings, dependency versions, toolchain, target and build
options. Use the fingerprint as a cache key.

- When the fingerprint matches a cached build, the host job restores it and
  finishes in seconds. A pull request that touches only Roc source, examples or
  documentation compiles no native code.

- When it does not match, the job builds the host and saves it under the new
  fingerprint.

- Later jobs take the host as an artifact from that job. They do not know or
  care whether it was built or restored.

[Keep rebuild selection narrow and complete](#keep-rebuild-selection-narrow-and-complete)
says what a fingerprint must include. A cache is storage, not a source of
trust: restored bytes come from the same repository’s own earlier job, and
anything that crosses a release boundary is verified against a locked hash.

Separate steps and caches pay off beyond runner minutes. A failure points at
the step that failed. A contributor without the native toolchain can still work
on the Roc side. Review is easier when a pull request shows which step it
invalidated.

<a id="watch-what-ci-costs"></a>

### Watch what CI costs

Record stage durations and investigate slow routine pull requests. Check for
fingerprints that include unrelated files, changing cache keys, duplicated
builds and unnecessary matrix jobs. Run the expensive native matrix when its
inputs change and before a release; use the cached path for other changes.

` libhost.a ` and ` host.lib ` are host outputs, not linker-input release assets.
Operating-system import libraries, runtime libraries, interface stubs and
resource objects required by ` platform/main.roc ` are linker inputs, even when
the platform repository holds their generation recipes. Classify by ownership
and invalidation, not by the fact that the final link receives the file.

Choose the smallest lifecycle that fits the project:

| Project shape | Lifecycle |
| --- | --- |
| Roc package | Bundle source. No native producer. |
| Package with a test-only host | Build the host in a read-only job of ` ci.yml `. It is never published. |
| Platform with a small host and no external inputs | Build the host in its own fingerprinted job, bundle once, test the bundle, then publish those bytes. |
| Platform with expensive external inputs | The same, with the linker inputs released independently and consumed through a reviewed lock. |

An existing binary is not a verified artifact until its build recipe, digest and
target coverage are established. An explicit source build remains valid.

<a id="separate-the-artifacts-by-what-can-invalidate-them"></a>

## Separate the artifacts by what can invalidate them

All of these files can eventually reach a linker, but they have different owners
and rebuild triggers. Name the categories explicitly in scripts, locks and docs.

| Artifact | What it contains | What requires a new build |
| --- | --- | --- |
| Project host and engine outputs, such as ` libhost.a `, ` libengine.a ` or a Wasm host library | Compiled project implementation for a target | Relevant source, generated inputs, dependency versions, toolchain, target ABI or build options change |
| External linker inputs, such as import libraries, runtime link files and macOS ` .tbd ` files | The external libraries or interfaces used by the final application link | Their upstream inputs, target contract, generation recipe or required interface catalog changes |
| Roc platform bundle | Platform API source plus the selected target outputs and linker inputs | Package source or selected input bytes change |
| Application root and companion modules | An application targeting a particular platform API and URL | Application source or selected package URL changes |

A host edit can require new host archives while reusing the existing external
linker inputs. Rebuild an engine archive only when its own inputs change; if a
producer groups multiple outputs, document that granularity. A new nightly
invalidates host outputs only when the compiler contributes to their build
identity; a change to the generated bindings is handled as described in
[Host bindings](#host-bindings).

For platforms that package macOS interfaces, review the required catalog before
generating and validating ` .tbd ` files. That generation belongs inside the external linker-input
cycle. When released separately, publish the generated files as immutable inputs.
For a static host archive, these files normally serve Roc’s final link rather
than host compilation. A producer that itself links executables or shared libraries
may also consume linker interfaces. Record its actual SDK, headers, interfaces and
native build tools in its identity; do not assume the host build and the linker inputs have no dependencies.

![Diagram](images/diagram-06509994a94b481aed8cc76dd8566db500fb7274f3c5f0c39835678d57269758.svg)

An unresolved symbol during the application link is evidence to investigate.
Confirm the library, symbol and supported target before changing the catalog;
missing project symbols, mismatched archives and ABI errors can produce similar
failures. Do not invent an export or expand the catalog automatically to make a
test pass. Producer validation can catch mistakes earlier, but a successful host
build alone does not establish that Roc can link the final application.

<a id="make-content-identity-the-ordinary-consumer-contract"></a>

## Make content identity the ordinary consumer contract

Commit reviewed lock data with each dependency selection. Include the artifact
name, target, exact release location, cryptographic digest such as SHA-256, byte
size, and expected archive contents or extraction boundaries. Record available source
revision and producer build-input fingerprint separately from the archive digest.
The fingerprint answers whether a build can be reused; the digest identifies the
exact bytes a consumer accepts. Neither a version label nor a fingerprint alone
verifies a downloaded archive.

Recompute freshness fingerprints where the repository owns or vendors the producer
inputs, such as its own host source. For external binaries, consumers verify the
reviewed selection, digest, target and inventory; they need not fetch upstream
source or install its toolchain to recompute the producer’s fingerprint. Recorded
producer metadata remains a claim subject to the chosen provenance policy.

Final Roc bundles have a compiler-defined content identity: preserve the hash
that ` roc bundle ` puts in the filename, and use one filename in both localhost
and published URLs. Roc uses its
base58-encoded BLAKE3 hash to verify the downloaded bundle. An additional SHA-256
in release metadata is useful but cannot replace that URL hash. See the
[Roc bundle implementation](https://github.com/roc-lang/roc/blob/8d6e3a360087038712d72bc090eaccf586cd83f7/src/bundle/bundle.zig#L266-L275)
and [download contract](https://github.com/roc-lang/roc/blob/8d6e3a360087038712d72bc090eaccf586cd83f7/src/bundle/download.zig#L42-L63).
Application users consuming that bundle need no separate native-input fetcher.

Resolve locked artifacts from cache, mirror or release URL and verify size and
digest before extraction. Stage the complete declared inventory with bounded
sizes and portable regular-file paths; reject aliases, special files and missing
runtime or license files. Do not trust an extracted tree merely because its
archive verified. Required unavailable or stale inputs fail rather than silently
rebuilding or selecting another release.

The reviewed lock authorizes bytes; a checksum from the same mutable download
location does not. Follow
[artifact admission](artifact-verification.md#separate-admission-from-routine-use)
when selecting new inputs. Routine hash checks are local, including cache hits,
and need no attestation service. An uncached download still needs a source of bytes.

<a id="publish-provenance-without-confusing-it-with-integrity"></a>

## Publish provenance without confusing it with integrity

Publish provenance binding the artifact digest to its producer, workflow and
source revision. Verify those expected identities when adopting an input;
follow [the attestation policy](artifact-verification.md#appropriate-attestation-use)
for audit and offline verification. A packaging workflow attests to assembly,
not to how a downloaded dependency was compiled. Preserve original producer
evidence; neither provenance nor hashes prove reproducible builds or correctness.

Publish dependency releases with unique immutable identities, validated archives,
lock data, licenses and available provenance. Enable and verify hosting-service
immutability. Stage all assets in a draft, read back the complete inventory and
verify the tag before publication. An interrupted draft can resume with identical
bytes; changed candidates or incomplete published immutable releases require a
new identity. Revalidate starters when their final URLs change.

<a id="publish-a-pull-request-candidate-without-granting-it-release-authority"></a>

### Publish a pull-request candidate without granting it release authority

When a linker-input release must precede merge, an unprivileged same-repository
PR workflow builds, tests and attests it. A reviewed default-branch publisher
verifies the producer run, source, manifest, attestations and final bytes without
executing candidate code. It can add only the generated lock in a lease-guarded,
GitHub-signed commit. Merging that reviewed lock adopts the branch-built release;
do not rebuild or relabel it after merge.

` actions/publish-build-inputs ` implements this boundary. Its wrapper exposes only
a PR number and pins the controller to a full SHA. Component build identities and
input snapshots are inert producer claims preserved in the lock; the publisher
validates their structure, while read-only producer/admission jobs own build
equivalence checks. Source and final-byte admission still apply to the complete
manifest. See
[the producer and publisher contract](linker-input-releases.md#publish-linker-inputs-from-a-pull-request)
for schema, recovery, review and merge requirements.

<a id="choose-the-correct-maintainer-path"></a>

## Choose the correct maintainer path

Use the smallest lifecycle that corresponds to the changed ownership boundary:

| Change | Required maintainer action |
| --- | --- |
| Roc source, examples, tests, docs, or an unrelated workflow | Ordinary pull-request validation using the committed host and linker-input locks. No native dependency release. |
| A new nightly breaks the released platform | ` nightly.yml ` reports it. Fix the source and release a new platform version. Native inputs are rebuilt only if the compiler contributes to their fingerprint. |
| A new nightly changes the generated host bindings | [Upgrade the bindings](#upgrade-the-bindings), with any host change, in one pull request. |
| Host implementation, host dependency, host toolchain, host ABI, or exported project symbol | The host job rebuilds, because its fingerprint changed. The new host ships in the next platform release. Reuse the existing linker inputs unless their own inputs changed. |
| Linker-input source, upstream binary, interface catalog, resource source, generation recipe, target matrix, or toolchain | Build the complete linker-input candidate on a same-repository pull-request branch, then run the trusted publisher and review its signed lock commit. |
| ` Blueprint.lock `, where it is part of a build-input fingerprint | The same producer and publisher cycle, in the pull request that changes the lock. |
| ` platform/main.roc ` linker entries | Check classification first. A new project implementation symbol normally changes the host; a new external library or interface normally changes linker inputs. Validate that every referenced released path exists for every advertised target. |
| Platform release | Require a current linker-input lock, build or restore the host, assemble once, exercise applications on every target, and publish the exact tested bundles. |

For a stale linker-input lock, stage the material change in a same-repository
pull request first. An unprivileged producer run at that head can build, test
and attest a candidate without publishing. Then dispatch the trusted publisher
**from the default branch** with the pull-request number. It reruns the producer
at the exact head, verifies the candidate, publishes the immutable linker-input
release, and adds a signed lock-only commit to the pull request. Revalidate the
updated pull request against the released lock before merging. A platform bundle
is a separate, later release that uses the selected lock.
[Publish linker inputs from a pull request](linker-input-releases.md#publish-linker-inputs-from-a-pull-request)
defines the admission and review checks.

An input fingerprint mismatch must be explicit. For a host, the read-only host
job simply builds it, because host source commonly changes with the pull
request. For a locked linker-input release, routine
validation fails with instructions to run the producer and publisher cycle. It
must not bootstrap a replacement as a side effect of a cache miss. An implicit
rebuild would substitute unreviewed bytes for the dependency the lock selects,
make results depend on whichever runner missed its cache, and hide that the
declared dependency is stale.

<a id="keep-routine-pull-requests-ignorant-of-production"></a>

## Keep routine pull requests ignorant of production

Routine consumers need only the lock schema, a downloader/cache, digest and archive
validation, and the selected extracted files. They do not need a native SDK,
linker-input generator, publication token, attestation lookup, or knowledge of how
the archives were produced. Keep producer jobs in their own workflow and path
scope; do not call them from general CI merely to populate a cache.

Every cache read is untrusted until the archived bytes match the locked size and
SHA-256. A cache miss downloads the exact locked release asset. A download failure
fails the job. A digest mismatch removes or ignores the bad entry and fails closed;
it never selects another release. Extraction and manifest checks happen after the
archive hash succeeds and before files become visible to the build.

Derive cache keys from the locked content identity rather than a branch, workflow
run, or mutable release label. This prevents unrelated revisions from sharing a
slot merely because their human-readable names match and makes identical reviewed
bytes reusable across PRs. Still rehash a cache hit: cache keys select storage but
do not authenticate its contents, and a poisoned, truncated, or incorrectly
restored entry can otherwise bypass the download-time check.

<a id="preserve-reviewed-history-and-authority"></a>

## Preserve reviewed history and authority

Use merge commits for pull requests whose evidence names exact commits. A merge
commit keeps the attested producer commit and the publisher’s signed lock-only
commit reachable under their original identities. Squashing manufactures a
different commit and discards that correspondence. A maintainer may still tidy
an ordinary development branch before review; the automation must not depend on
history being rewritten after it has bound evidence to a SHA.

Keep the authorities distinct:

- producer and ordinary validation jobs may execute candidate code and are
  read-only;

- the trusted publisher job invokes only its full-SHA-pinned shared action,
  with release and lock-write permission;

- the platform release publisher consumes already selected dependencies and
  publishes only explicitly dispatched, tested platform bundles.

This split is what makes running candidate build code acceptable: a compromise
of that job cannot write a release or a branch. The publisher has powerful
credentials, so it accepts only inert declared files and never executes the pull
request. Least-privilege permissions reduce what is possible if the pinned
action or the hosting account is compromised; they do not make unreviewed code
safe to run in that job.

A shared action is tried from a reviewed roc-automation pull request by pinning
the caller to that pull request’s full commit SHA. A branch name or abbreviated
SHA is never pinned. If review changes the shared implementation, pin the new
commit and repeat the trial.

<a id="keep-rebuild-selection-narrow-and-complete"></a>

## Keep rebuild selection narrow and complete

Producers computing reuse identities must include every input that can affect an output:
relevant source paths and contents, generated inputs, dependency locks, build
recipes, tool versions, target configuration and build flags. Record file paths
and modes as needed to distinguish renames, deletions and symlinks. Validate the
permitted source-file kinds. Include relevant environment and SDK identities;
an unpinned runner image or ambient library can change output without a source edit.
Where the toolchain comes from Blueprint, ` Blueprint.lock ` is one of those inputs,
so a lock update that changes the toolchain makes the selected outputs stale.
Use a versioned fingerprint recipe and compare the same input definitions.
A whole-commit identity, together with the non-source inputs above, is a conservative
starting point, but causes needless rebuilds for unrelated edits. Narrow the source
selection only after testing the input inventory.
Include the source revision separately for traceability.

Treat workflow triggers and fingerprints as different controls. Path filters
decide when to run a producer audit; fingerprints decide whether the selected
published outputs still correspond to current inputs. A test-harness edit may
justify rerunning an audit without requiring a new dependency release. Trace shared
scripts and generated inputs transitively so a narrow filter cannot hide a real
build change. For repository-owned inputs, record the published fingerprint in lock
data or a digest-bound manifest so a shallow checkout can compare current inputs
without fetching an old producer commit. A stale host blocks reuse; PR validation
may explicitly build a new candidate in a read-only job without publishing it.

For a narrowed fingerprint, require meaningful checks for both directions: a relevant source, tool pin or
build-option change invalidates reuse, while an unrelated example or documentation
change preserves it. Check archive digest rejection and target/path confinement.
Exercise final linking against reused artifacts; a selector test alone does not

<a id="keep-final-release-preparation-small"></a>

## Keep final release preparation small

Once the linker inputs are locked and the host job has built or restored the
host, the rest of the release needs only the latest nightly to bundle and link
applications, the artifact verification step, and the runtime dependencies of
its tests. It downloads and hash-verifies the locked linker inputs. SDK
generators belong in the linker-input producer; a stale lock sends the
maintainer back to that cycle.

Bundle once, then pass those exact archives to the target runners. Each runner
serves the applicable bundles on a loopback address and runs the archived
examples against them, as in
[Build and test the exact archive](examples.md#build-and-test-the-exact-archive).
Validate every advertised target before publication. Check package size limits
and target contents early, so that publication does not discover that a bundle
cannot carry its advertised hosts.

Keep preparation, validation and publication connected by recorded artifact
digests and the exact source SHA. Record archive digests, selected dependency
identities, the source revision and the compiler versions in the release notes.
The remaining steps are those of every release; see
[The order of a release](workflows.md#the-order-of-a-release).

