Releasing
The three crates in this repository are published to crates.io independently. Each has its own version, its own CHANGELOG, and its own release tag, because each makes its own promise to the people who depend on it — a fix to the read client is not a reason to renumber the capture protocol.
All three are live: tapes-capture, tapes-harnesses, and tapes-client
each published 0.1.0 on 2026-08-13, so cargo add tapes-client resolves
against the real index and every release after the first follows the
procedure below. The upload step is guarded by two locks — see
The hold.
Dependency order
Section titled “Dependency order”Only one edge exists between these crates:
tapes-capture ──depended on by──> tapes-harnessestapes-client (depends on neither, and neither depends on it)tapes-harnesses declares tapes-capture with both a path and a version.
Cargo uses the path in this workspace and the version everywhere else, which is
what makes the crate publishable at all — but it also means the required
tapes-capture version must already be on crates.io before tapes-harnesses
can publish. Releasing them in the wrong order does not corrupt anything; the
tapes-harnesses publish simply fails to resolve its dependency.
So:
tapes-capturefirst, whenever its version has changed.tapes-harnessesafter, and only after that release is live.tapes-clientwhenever — it is not on either side of the edge.
Bumping tapes-capture means bumping the version in the tapes-capture
dependency entry in crates/tapes-harnesses/Cargo.toml too, in the same change
that bumps the crate. A stale requirement there is not a build failure in this
workspace — the path dependency satisfies it locally — so nothing catches it
until a consumer resolves against crates.io.
Cutting a release
Section titled “Cutting a release”- Bump
versionin that crate’scrates/<crate>/Cargo.toml. - Move its
CHANGELOG.mdentries fromUnreleasedinto a version heading with the date. - If the crate is
tapes-captureand the version changed, update thetapes-capturedependency version incrates/tapes-harnesses/Cargo.toml. - Land that as a normal PR.
cargo package --workspace --lockedruns on it like it runs on every PR. - Dispatch the Cut Release workflow (
.github/workflows/cut-release.yaml) frommain, choosing the crate. It reads the version the manifest already carries at main’s tip, refuses a tag that already exists, and pushes<crate>-v<version>pointing at that tip — so the tag matches the manifest by construction, which is the mistake hand-typed tags invite. Tagging by hand remains the fallback:
git tag tapes-client-v0.1.0git push origin tapes-client-v0.1.0The tag scheme is <crate>-v<version>, one crate per tag. The release workflow
resolves the tag back to a crate, refuses a tag that does not name one of the
three, and fails if the tagged version does not match that crate’s manifest
— a tag is a claim about the source, and an unchecked claim is how a version
number that exists nowhere in the tree ends up on crates.io.
What the release workflow does
Section titled “What the release workflow does”.github/workflows/release.yaml, on a <crate>-v* tag:
- Resolves the tag to a crate and version.
- Checks the version against the manifest.
- Re-runs the gates against the tagged tree — fmt, clippy (default and all
features), tests, the contract seal, and
cargo package --workspace. A tag can point at a commit that never sat onmain, so a green branch says nothing about these bytes. - Publishes that one crate — unless the upload is held.
The hold
Section titled “The hold”Two independent locks guard the upload, and the publish step runs only when both are open:
| lock | opens when | applies to |
|---|---|---|
repository variable PUBLISH_ENABLED |
it is set to exactly true |
every run |
workflow input confirm |
it is retyped to match the tag | manual workflow_dispatch runs only |
While PUBLISH_ENABLED is unset, every step before the upload still runs, so
pushing a release tag is a full rehearsal: it tells you whether that crate
would have published, and changes nothing. The job summary names which lock is
closed. The hold was opened for the 0.1.0 releases; its current state is
visible in one place, the repository settings.
To lift the hold, create the repository variable PUBLISH_ENABLED = true
(Settings → Secrets and variables → Actions → Variables). Deleting it re-arms
the hold. It is a variable rather than a code change on purpose — the state of
the hold is then visible in one place in the repository settings, and flipping
it leaves an audit entry that editing a workflow file does not.
Publishing also needs the CARGO_REGISTRY_TOKEN secret, a crates.io API token
scoped to publishing these crates. Scoping that secret to a GitHub
Environment with required reviewers is worth doing: it adds a human approval
to each upload, which neither lock above provides.
Keeping the crates publishable
Section titled “Keeping the crates publishable”.github/workflows/release-checks.yaml runs on every PR and every push to
main, so publishability cannot rot between releases:
cargo package --workspace --lockedpackages all three crates and verifies each one compiles standalone, outside the workspace.--workspacematters: it builds a temporary registry from the packaged crates, which is the only waytapes-harnessescan be verified against atapes-capturethat is not yet on crates.io. A per-cratecargo package -p tapes-harnessesfails today for that reason, and that failure is about the invocation rather than about the crate.cargo publish --dry-runfor all three crates, which exercises the upload-preparation path — manifest normalisation, the path-to-registry dependency rewrite, the index lookups — without uploading.tapes-harnessesjoined the list oncetapes-capturewas live on crates.io, because its dry-run resolves the publishedtapes-capturerelease.- An explicit check that the vendored data files are inside the packages.
These files are
include_str!d, so a missing one is a compile error rather than a silent hole — but the check names the file, which a compile error in a packaged tarball does not.
One thing worth knowing when reading a failure there: a packaged crate does not
carry the root manifest’s [patch.crates-io]. The verification builds are the
only place in this repository that compiles netsock -> libproc against the
real crates.io release rather than the patched revision, so a problem the patch
exists to work around surfaces there first.