Skip to content

ci: publish from a tag, so crates.io always maps to a commit - #113

Merged
rrrodzilla merged 1 commit into
mainfrom
ci/16-tag-driven-publish
Jul 31, 2026
Merged

rrrodzilla merged 1 commit into
mainfrom
ci/16-tag-driven-publish

Conversation

@rrrodzilla

@rrrodzilla rrrodzilla commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Closes #16. (#11, which blocked it, is merged.)

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.

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.yml fires on *-v* tags and refuses to publish unless all of these hold:

Check Why it exists
Tag matches <crate>-v<semver> The tag is the only input, and it is written by hand
Crate exists in this workspace Otherwise the failure lands later and more confusingly
Tag version == that crate's Cargo.toml version This exact disagreement is how the repo and crates.io drifted apart
Tagged commit is an ancestor of main A tag can be pushed from any commit, including one never reviewed
A CI run for that exact SHA concluded success Without it, a tag on a commit whose tests failed still publishes
cargo package -p <crate> succeeds A missing README or stale path dep fails while the version is still recoverable

Then, and only then, cargo publish.

Auth

rust-lang/crates-io-auth-action, pinned to v1.0.5 — upstream publishes no moving v1 tag 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 a run: command. Tested:

accept  agent-uri-v0.6.0                      -> crate=agent-uri              version=0.6.0
accept  agent-uri-dht-libp2p-v0.5.0           -> crate=agent-uri-dht-libp2p   version=0.5.0
accept  agent-uri-attestation-wellknown-v0.2.0
accept  agent-uri-v1.0.0-rc.1                 -> version=1.0.0-rc.1
REJECT  agent-uri-v0.6.0; rm -rf /
REJECT  ../../etc-v1.0.0
REJECT  Agent-URI-v1.0.0
REJECT  agent-uri-v0.6
REJECT  v0.3.0

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 api switches to POST as soon as a field is given, and this endpoint has no POST. With -X GET it returns success against the real repository. Verified against origin/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-cli is publishable and already on crates.io at 0.1.0; only agent-uri-eval is publish = false):

Crate On crates.io This branch
agent-uri 0.5.0 0.6.0
agent-uri-attestation 0.4.0 0.8.0
agent-uri-dht 0.2.0 0.12.0
agent-uri-cli 0.1.0 0.5.1
agent-uri-attestation-wellknown not published 0.2.0
agent-uri-dht-libp2p not published 0.5.0

The last two have no crates.io settings page to configure, because the crate does not exist yet. Each needs one manual cargo publish to 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:

Repository owner   Govcraft
Repository name    agent-uri-rs
Workflow filename  release.yml
Environment        crates-io

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-io GitHub 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:

agent-uri
  └─ agent-uri-attestation
       ├─ agent-uri-dht ─── agent-uri-dht-libp2p
       ├─ agent-uri-attestation-wellknown
       └─ agent-uri-cli

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 package currently succeeds only for agent-uri. The published state is behind by as much as ten minor versions in agent-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.

@rrrodzilla
rrrodzilla force-pushed the ci/16-tag-driven-publish branch 2 times, most recently from 6481878 to 578befe Compare July 31, 2026 22:56
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
rrrodzilla force-pushed the ci/16-tag-driven-publish branch from 578befe to 3401670 Compare July 31, 2026 22:59
@rrrodzilla
rrrodzilla merged commit fac222c into main Jul 31, 2026
13 checks passed
@rrrodzilla
rrrodzilla deleted the ci/16-tag-driven-publish branch July 31, 2026 23:02
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
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.

Automate releases: tag-driven publish from CI

1 participant