ci: publish from a tag, so crates.io always maps to a commit - #113
Merged
Merged
Conversation
rrrodzilla
force-pushed
the
ci/16-tag-driven-publish
branch
2 times, most recently
from
July 31, 2026 22:56
6481878 to
578befe
Compare
During the assessment behind this issue, local main was seven commits behind the crates.io state: 0.5.0, 0.4.0, and 0.2.0 had been published from a workstation while the tree still said 0.4.0, 0.3.0, and 0.1.1. Release tags existed, but nothing tied a published version to a commit anyone could look at. Publishing now happens in CI and only in CI, keyed on <crate>-v<version> tags, the format already in this repository's tag history. Before anything is uploaded the workflow requires that the tag parses, names a crate this workspace contains, matches that crate's Cargo.toml version, sits on an ancestor of main, and has a successful CI run for its exact commit. It then packages the crate before publishing it, so a missing README or a stale path dependency fails while the version is still recoverable. Authentication is crates.io trusted publishing over OIDC. There is no long-lived token in the repository's secrets; the job mints a short-lived one per run. The crates.io side must be configured once per crate before its first release through here, which the workflow header spells out. One crate per tag rather than one tag for the workspace: a dependent cannot be packaged until the version it requires is on the index, so ordering is real and making it explicit beats a job that leaves a release half-published. A tag is written by a person, so it is validated against a shape before any use and reaches the shell only as an environment variable, never interpolated into a command. Closes #16
rrrodzilla
force-pushed
the
ci/16-tag-driven-publish
branch
from
July 31, 2026 22:59
578befe to
3401670
Compare
rrrodzilla
added a commit
that referenced
this pull request
Jul 31, 2026
Concurrency was grouped by workflow and ref only, so a scheduled run and a push to main both landed in CI-refs/heads/main and an ordinary merge cancelled whatever was in flight. Observed while dispatching the new jobs: the push from merging #113 at 23:02:19 cancelled the workflow_dispatch run started at 23:00:24. Latent since fuzz was added, and made much worse by #15: kani and miri are the longest jobs in the file, so the window in which a merge can kill them is minutes wide, and the weekly schedule fires regardless of what else is landing. It fails silently, too — a cancelled run is not a red X anyone chases, so the proofs would have quietly stopped running. Adding the event name gives each event type its own lane. Cancelling superseded runs of the same event still happens, which is the part worth keeping: push again to a pull request and the previous run for that branch goes away. Closes #114
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #16. (#11, which blocked it, is merged.)
During the assessment behind this issue, local
mainwas seven commits behind the crates.io state: 0.5.0, 0.4.0, and 0.2.0 had been published from a workstation while the tree still said 0.4.0, 0.3.0, and 0.1.1. Release tags existed, but nothing tied a published version to a commit anyone could look at.Built to the three choices made on the issue: a plain tag-driven workflow, trusted publishing over OIDC, one crate per tag.
The gate
.github/workflows/release.ymlfires on*-v*tags and refuses to publish unless all of these hold:<crate>-v<semver>Cargo.tomlversionmainCIrun for that exact SHA concludedsuccesscargo package -p <crate>succeedsThen, and only then,
cargo publish.Auth
rust-lang/crates-io-auth-action, pinned tov1.0.5— upstream publishes no movingv1tag to float on, and this action is the only thing standing between an OIDC token and a publish. No long-lived credential in the repo's secrets; a short-lived token is minted per run.Injection surface
A tag name is attacker-influenceable in the general case and hand-written in practice, so it is matched against a shape before use and reaches the shell only via
env:, never interpolated into arun:command. Tested:The derived crate name is constrained to
[a-z][a-z0-9-]*, so it cannot traverse out of the workspace when used as a path.One bug found and fixed while testing
The CI-status check originally used
gh api <endpoint> -f head_sha=... -f event=push. That 404s:gh apiswitches to POST as soon as a field is given, and this endpoint has no POST. With-X GETit returnssuccessagainst the real repository. Verified againstorigin/main's actual SHA.The manifest-version extraction was also checked against all six crates and returns the right value for each.
What you need to do before the first release
I cannot do this part — it needs crates.io account access. There are six publishable crates, not five as an earlier draft of this description said (
agent-uri-cliis publishable and already on crates.io at 0.1.0; onlyagent-uri-evalispublish = false):agent-uriagent-uri-attestationagent-uri-dhtagent-uri-cliagent-uri-attestation-wellknownagent-uri-dht-libp2pThe last two have no crates.io settings page to configure, because the crate does not exist yet. Each needs one manual
cargo publishto claim the name; every release after that comes from here.For each crate that does exist, go to Settings → Trusted Publishing on crates.io and add:
Until a crate is registered, its tag will run the workflow, pass every gate, and be refused at the publish step. The workflow header carries these values so they are not only in this description.
The
crates-ioGitHub environment is also where a required reviewer can be added if a release should need a second pair of eyes.Ordering
One crate per tag, pushed in dependency order — a dependent cannot even be packaged until the version it requires is on the index:
This is documented in a new Releasing section in the README alongside the workflow header.
Note that this ordering matters immediately: the version bumps from #27 are not yet on crates.io, which is why
cargo packagecurrently succeeds only foragent-uri. The published state is behind by as much as ten minor versions inagent-uri-dht's case — every version in between exists only in git history. Nothing outside this repository depends on these crates yet, so catching up is bookkeeping rather than a migration.