Fix: publish to crates.io from the release workflow, and verify the registry - #531
Conversation
…egistry
crates.io has fledge at 1.7.0, but v1.7.1 and v1.7.2 are both tagged. Two
releases were cut and never shipped, and nobody noticed.
The cause is not a failure. release.yml ran for both tags and succeeded — both
runs are green in the Actions history. It has three jobs (test, build, release)
and none of them publishes; `grep -rn "cargo publish" .github/workflows/`
returns nothing. Publishing was documented nowhere: not AGENTS.md, not
CONTRIBUTING.md, not docs/. It was a manual step living in someone's head, and
the signal you would check said everything was fine.
Adds a publish job on the same v* tag trigger, after release:
- Fails loudly when CARGO_REGISTRY_TOKEN is absent rather than skipping. A
skipped step is indistinguishable from a passing one, which is exactly how
1.7.1 and 1.7.2 slipped by. A release that did not publish is not a
finished release and should be red.
- Reads the registry back. `cargo publish` exiting 0 does not prove the crate
is live — the index lags and some no-op paths also exit 0. The job polls
crates.io and compares max_version to the tag, retrying while the index
catches up. CorvidLabs/hi hit this exact failure at 0.2.4/0.2.5.
AGENTS.md documents both, including the manual read-back check.
Two open ends, recorded in the change's context.md:
- The repository has no secrets at all today, so CARGO_REGISTRY_TOKEN must be
created before the next release or the tag will fail at this job.
- 1.7.1 and 1.7.2 remain unpublished. This stops the bleeding; it does not
backfill.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M9GwmgSChrXYZhLjUYejwP
First change on the workflow-v2 lifecycle: closes through review + finalize rather than the legacy accept + archive, and finalize runs before merge so the verification commit is not orphaned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M9GwmgSChrXYZhLjUYejwP
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: b43acddd5b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| echo "::error::CARGO_REGISTRY_TOKEN is not set, so this tag was built and released but NOT published to crates.io. Add the secret in Settings > Secrets and variables > Actions, then re-run this job." >&2 | ||
| exit 1 | ||
| fi | ||
| cargo publish |
There was a problem hiding this comment.
Validate the tag before publishing the manifest
For any manually pushed or mistyped v* tag whose suffix differs from Cargo.toml, this command publishes the manifest's version before the later step compares crates.io with GITHUB_REF_NAME. For example, a v1.8.1 tag on a manifest still at 1.8.0 would irreversibly publish 1.8.0 and only then report failure. Read the package version and require it to equal ${GITHUB_REF_NAME#v} before invoking cargo publish.
AGENTS.md reference: AGENTS.md:L363-L363
Useful? React with 👍 / 👎.
| echo "::error::CARGO_REGISTRY_TOKEN is not set, so this tag was built and released but NOT published to crates.io. Add the secret in Settings > Secrets and variables > Actions, then re-run this job." >&2 | ||
| exit 1 | ||
| fi | ||
| cargo publish |
There was a problem hiding this comment.
Make the publish job safe to rerun
If the registry verification times out, the job is cancelled after upload, or a runner fails between these steps, rerunning the failed job starts here and tries to upload the same version again. Published crate versions cannot be overwritten, so cargo publish fails and the rerun never reaches the read-back step that could confirm the already-live crate (Cargo publishing documentation). Check the registry first and skip the upload only when this exact version already exists, while still running verification.
AGENTS.md reference: AGENTS.md:L363-L363
Useful? React with 👍 / 👎.
| actual=$(curl -sS -A "fledge-release-check" \ | ||
| "https://crates.io/api/v1/crates/fledge" \ | ||
| | python3 -c "import json,sys; print(json.load(sys.stdin)['crate']['max_version'])") |
There was a problem hiding this comment.
Retry failed registry queries inside the loop
When crates.io transiently refuses a connection, returns a rate-limit/server response, or emits non-JSON, curl or Python exits nonzero and set -euo pipefail terminates the step from this assignment immediately. Consequently, the six-attempt loop retries only a successful API response containing the previous version, not the transient API failures it is intended to tolerate; handle a failed query with continue and sleep rather than exiting.
AGENTS.md reference: AGENTS.md:L363-L363
Useful? React with 👍 / 👎.
| "https://crates.io/api/v1/crates/fledge" \ | ||
| | python3 -c "import json,sys; print(json.load(sys.stdin)['crate']['max_version'])") |
There was a problem hiding this comment.
Verify the published version instead of the maximum
When two valid releases overlap or an older release is backfilled after a newer one, crates.io can contain the expected version while max_version reports the newer version, causing this job to fail after a successful publication. Query the expected version directly (or inspect the returned versions for an exact match) so the check proves that this tag's crate exists without assuming releases complete strictly in version order.
AGENTS.md reference: AGENTS.md:L363-L363
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
✅ Corvin says...
_
<(^\ .oO(Caw! ^v^)
|/(\
\(\\
" "\\
"Looking sharp! Like a beak should be."
CI Summary
| Check | Status |
|---|---|
| Dependency Audit | ✅ Passed |
| Integration (3 OS) | ✅ Passed |
| Lint (fmt + clippy) | ✅ Passed |
| Spec Validation | ✅ Passed |
| Tests (3 OS) | ✅ Passed |
Powered by corvid-pet
Rebasing onto main brought in four commits the section did not list: #531, #533, #534 and #535. Add them where fledge's generator places them. Also drop the stale "## Unreleased" block. Both of its bullets describe #520 (c4abcb2), which ships in 1.8.0, so they move under that entry as sub-bullets instead of being advertised as unreleased. `fledge release` inserts the new section above an existing Unreleased block rather than consuming it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3ZZAEiUP7xRJPozhZb6rL
…s.io-publish change (#537) * Update: record the Windows CI evidence in the change record test (windows-latest) is green on this PR (ureq 3.3.0, run 36257383427) and on a workflow_dispatch probe of main + this fix + the v1.8.0 release commit (ureq 3.4.2, run 36257571327). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3ZZAEiUP7xRJPozhZb6rL * Update: record the v1.8.1 release PR's Windows run in the change record #532 was closed and replaced by #536 (v1.8.1), because 1.8.0 was already on crates.io. test (windows-latest) is green there with ureq 3.4.2 (run 36260828952), which closes the last open task. Also name #535 explicitly where the artifacts said "this PR". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3ZZAEiUP7xRJPozhZb6rL * chore(lifecycle): record definition approval for the isolation-test change Approved as user:0xLeif on Leif's explicit go in the orc session. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V3ZZAEiUP7xRJPozhZb6rL * chore(lifecycle): materialize accept-windows-refused-connection-wording-in-the-isolation-test * chore(lifecycle): record accept-windows-refused-connection-wording-in-the-isolation-test verification * chore(lifecycle): archive accept-windows-refused-connection-wording-in-the-isolation-test * chore(lifecycle): materialize publish-to-crates-io-from-the-release-workflow-and-verify-the-registry * chore(lifecycle): record publish-to-crates-io-from-the-release-workflow-and-verify-the-registry verification * chore(lifecycle): archive publish-to-crates-io-from-the-release-workflow-and-verify-the-registry --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
fledge 1.8.0 was published to crates.io on 2026-09-19 from 435b7c5, a release-branch head that never reached main. 1.8.1 is that release cut from main, with #531, #533, #534 and #535 on top. Bumps Cargo.toml and flake.nix to 1.8.1. Cargo.lock differs from the published 1.8.0 lockfile only in fledge's own version; no dependency moves. The bump was done by hand, because `fledge release` runs `cargo generate-lockfile`, which re-resolves every dependency. The changelog section covers every commit since v1.7.2, with a note on 1.8.0. The stale "Unreleased" block, which described #520 (shipped here), is folded into that entry. Claude-Session: https://claude.ai/code/session_01V3ZZAEiUP7xRJPozhZb6rL Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Summary
crates.io has fledge at 1.7.0, but v1.7.1 and v1.7.2 are both tagged. Two releases were cut and never shipped, and nobody noticed.
The cause is not a failure.
release.ymlran for both tags and succeeded — both runs are green in the Actions history. It has three jobs (test,build,release) and none of them publishes.grep -rn "cargo publish" .github/workflows/returns nothing, and publishing is documented nowhere: notAGENTS.md, notCONTRIBUTING.md, notdocs/. It was a manual step living in someone's head, and the signal you would check said everything was fine.The fix
A
publishjob on the samev*tag trigger, gated afterrelease:CARGO_REGISTRY_TOKENis absent, rather than skipping. A skipped step is indistinguishable from a passing one, which is exactly how 1.7.1 and 1.7.2 slipped by. A release that did not publish is not a finished release and should be red.cargo publishexiting 0 does not prove the crate is live — the index lags, and some no-op paths also exit 0. The job pollshttps://crates.io/api/v1/crates/fledgeand comparesmax_versionagainst the tag, retrying while the index catches up.CorvidLabs/hihit this exact failure at 0.2.4/0.2.5.AGENTS.mddocuments both, including the manual read-back check for anyone publishing by hand.Two open ends, recorded in the change's
context.mdgh secret listis empty), soCARGO_REGISTRY_TOKENmust be created before the next release or the tag will fail at this job. That failure is by design, but it will be the first thing you hit.Sequencing note for v1.8.0
This needs to merge before 1.8.0 is tagged, or 1.8.0 ships without a publish job and repeats the same failure.
maincurrently has 22 commits since v1.7.2 andCargo.tomlstill says 1.7.2.Verification
release.ymlparses; jobs aretest,build,release,publish, withpublish.needs: [release]on: push: tags: v*)fledge spec check— 33 specs, 0 errors, 0 warningsThe job itself cannot be exercised until a tag is pushed with the secret present — an honest limitation, mitigated by the failure being loud and naming its own fix.
🤖 Generated with Claude Code