Skip to content

Publish a release against one artifact contract - #206

Merged
tamnd merged 1 commit into
mainfrom
release-artifact-contract
Aug 15, 2026
Merged

tamnd merged 1 commit into
mainfrom
release-artifact-contract

Conversation

@tamnd

@tamnd tamnd commented Aug 15, 2026

Copy link
Copy Markdown
Owner

The second DX0 item of the repository split: what a release publishes, written down once.

A release is a tag here and a run that drives the eight other repositories, and every one of them builds against what this one published. That makes the list of what gets published a contract rather than a step in a workflow. A binding that fetches model.json for the version it pins and finds nothing cannot tell a release that dropped the artifact from a version that never had it, and the failure surfaces in somebody else's CI a day later.

artifacts.toml is the list and .github/workflows/release.yml has none of its own. It assembles from the table and reads the directory back against it, in both directions like the two tables beside it: an artifact the contract publishes and the release does not hold is a consumer's failure tomorrow, and a file in the release that no row accounts for is something somebody will fetch that nothing promises.

Four kinds of row, because there are four ways an artifact comes to exist. A file is a path this tree already holds, zu.h and model.json being the two. A corpus is packed by the packer that already exists. A platform row is one artifact per tier 1 target, expanded against platforms.toml, so the seven move with that table rather than with this one. A later row is an artifact the contract names that nothing makes yet, with the milestone that will make it: cli.json with D1, gql.json and errors.json with D2.

The train is the skeleton dx/14 §6 asks for. The build is the same matrix every pull request runs, the assemble and the verify are real, and every publish step prints what it would do in the order it will do it, crates.io before the repositories that build against it and the Go tag last of the registries because pushing a tag cannot be taken back. A rehearsal runs on workflow_dispatch with a version rather than a tag, since a release train is exactly the machinery that must not run for the first time on the day of a release.

A platform's build now stages the library, the CLI, the import library where there is one and the header into a directory named for the target, and that directory is the artifact, so libzu-<target>.tar.zst unpacks to four files rather than four levels of somebody's build path. Both archives are written by one tar writer, moved out of the corpus packer into tarball.rs, because a second implementation of a format this dull would be a second set of headers to get subtly wrong in somebody else's language.

A local rehearsal of the assemble and the verify, with the seven platform directories faked:

libzu-x86_64-unknown-linux-gnu.tar.zst           2 KiB  platform
...
zu.h                                             4 KiB  file
model.json                                     275 KiB  file
conformance-0.5.0.tar.zst                       19 KiB  corpus
dist: 10 files for 0.5.0, and 3 the contract names that nothing makes yet (cli.json, gql.json, errors.json)

Twenty tests over the table, the assemble and the verify, and cargo bench -p xtask --bench artifacts holding the per-artifact cost of verifying flat as the contract grows past the ten names it has today. The check runs as a test, so it fires on the machine of whoever edited the table, and as a command in CI, because a command nothing exercises is a command that rots.

Part of #166.

A release is a tag here and a run that drives the eight other
repositories, and every one of them builds against what this one
published. That makes the list of what gets published a contract rather
than a step in a workflow: a binding that fetches model.json for the
version it pins and finds nothing cannot tell a release that dropped
the artifact from a version that never had it, and the failure surfaces
in somebody else's CI a day later.

artifacts.toml is the list, and release.yml has none of its own. It
assembles from the table and reads the directory back against it, so an
artifact that stopped being produced fails the release that dropped it
rather than the eight that wanted it. Four kinds of row, because there
are four ways an artifact comes to exist: a file the tree already
holds, the packed corpus, one artifact per tier 1 target expanded
against platforms.toml, and a row the contract names that nothing makes
yet with the milestone that will make it. Naming those three early is
the point rather than an oversight, since a consumer needs to know what
a release will eventually carry.

The train itself is the skeleton dx/14 section 6 asks for. The build is
the same matrix every pull request runs, the assemble and the verify
are real, and every publish step prints what it would do in the order
it will do it, crates.io before the repositories that build against it
and the Go tag last of the registries because pushing a tag cannot be
taken back. A rehearsal runs on workflow_dispatch with a version rather
than a tag, since a release train is exactly the machinery that must
not run for the first time on the day of a release.

A platform's build now stages the library, the CLI, the import library
where there is one and the header into a directory named for the
target, and that directory is the artifact, so libzu-<target>.tar.zst
unpacks to four files rather than four levels of somebody's build path.
Both archives are written by one tar writer, moved out of the corpus
packer to tarball.rs, because a second implementation of a format this
dull would be a second set of headers to get subtly wrong in somebody
else's language.

Twenty tests, a bench holding the per-artifact cost of verifying flat
as the contract grows, and the check runs as a test and as a command in
CI like the two tables beside it.
@tamnd
tamnd merged commit 5e10f3f into main Aug 15, 2026
27 checks passed
@tamnd
tamnd deleted the release-artifact-contract branch August 15, 2026 20:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant