Repository navigation
Fix: publish to crates.io from the release workflow, and verify the registry #531
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -105,3 +105,46 @@ jobs: | |
| with: | ||
| generate_release_notes: true | ||
| files: artifacts/* | ||
|
|
||
| # Publishing is part of releasing, not a separate manual step. v1.7.1 and | ||
| # v1.7.2 were both tagged, both ran this workflow green, and neither reached | ||
| # crates.io — because no job here published them and nothing said so. A job | ||
| # that fails loudly is the point: a release that did not publish is not a | ||
| # finished release, and should be red rather than quietly incomplete. | ||
| publish: | ||
| name: Publish to crates.io | ||
| needs: [release] | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: dtolnay/rust-toolchain@stable | ||
| - name: Publish | ||
| env: | ||
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | ||
| run: | | ||
| set -euo pipefail | ||
| if [ -z "${CARGO_REGISTRY_TOKEN:-}" ]; then | ||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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 AGENTS.md reference: AGENTS.md:L363-L363 Useful? React with 👍 / 👎. |
||
| # Read the registry back. cargo publish exiting 0 is not proof the crate | ||
| # is live: the index takes a moment, and a no-op publish also exits 0. | ||
| # CorvidLabs/hi hit this exact failure at 0.2.4/0.2.5. | ||
| - name: Verify the registry | ||
| run: | | ||
| set -euo pipefail | ||
| expected="${GITHUB_REF_NAME#v}" | ||
| for attempt in 1 2 3 4 5 6; do | ||
| 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'])") | ||
|
Comment on lines
+139
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When crates.io transiently refuses a connection, returns a rate-limit/server response, or emits non-JSON, AGENTS.md reference: AGENTS.md:L363-L363 Useful? React with 👍 / 👎.
Comment on lines
+140
to
+141
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When two valid releases overlap or an older release is backfilled after a newer one, crates.io can contain the expected version while AGENTS.md reference: AGENTS.md:L363-L363 Useful? React with 👍 / 👎. |
||
| echo "attempt $attempt: crates.io reports $actual, tag is $expected" | ||
| if [ "$actual" = "$expected" ]; then | ||
| echo "crates.io confirms $actual" | ||
| exit 0 | ||
| fi | ||
| sleep 10 | ||
| done | ||
| echo "::error::crates.io still reports $actual after publishing $expected. The publish did not take effect." >&2 | ||
| exit 1 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| { | ||
| "approvals": [ | ||
| { | ||
| "gate": "definition", | ||
| "actor": "user:0xLeif", | ||
| "timestamp": 1789781187, | ||
| "digest": "027e4f3c88ddcd03f9f107dd9a570e91ea7b5647d0d165a366659f208a3729e5", | ||
| "note": null, | ||
| "approved_scope": { | ||
| "schema_version": 1, | ||
| "change_id": "publish-to-crates-io-from-the-release-workflow-and-verify-the-registry", | ||
| "title": "Publish to crates.io from the release workflow and verify the registry", | ||
| "description": "Publish to crates.io from the release workflow and verify the registry", | ||
| "kind": "bug_fix", | ||
| "affected_specs": [], | ||
| "affected_paths": [ | ||
| ".github/workflows/release.yml", | ||
| "AGENTS.md" | ||
| ], | ||
| "no_spec_change": true, | ||
| "no_spec_change_rationale": "release.yml and AGENTS.md are outside source_dirs (src, templates); no canonical spec covers CI workflows or agent docs", | ||
| "acceptance_criteria": [ | ||
| "release.yml carries a publish job that runs cargo publish on a v* tag after the release job, fails loudly with an actionable message when CARGO_REGISTRY_TOKEN is absent rather than skipping, and then reads https://crates.io/api/v1/crates/fledge back and compares max_version to the tag, retrying while the index catches up; AGENTS.md documents both the job and the read-the-registry-back rule; the workflow remains valid YAML with jobs test, build, release, publish" | ||
| ], | ||
| "dependencies": [], | ||
| "supersedes": [], | ||
| "answers": { | ||
| "architecture_risk": "no", | ||
| "public_contract": "no" | ||
| } | ||
| }, | ||
| "approved_delta_digests": {} | ||
| } | ||
| ], | ||
| "reopenings": [] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| --- | ||
| id: publish-to-crates-io-from-the-release-workflow-and-verify-the-registry | ||
| state: implementing | ||
| type: bug_fix | ||
| base_commit: 31d5f45f7bca1b71e374e41792fabeb6359a9de4 | ||
| --- | ||
|
|
||
| # Publish to crates.io from the release workflow and verify the registry | ||
|
|
||
| ## Intent | ||
|
|
||
| Publish to crates.io from the release workflow and verify the registry | ||
|
|
||
| ## Affected Canonical Specs | ||
|
|
||
| - None | ||
|
|
||
| ## Acceptance Criteria | ||
|
|
||
| - release.yml carries a publish job that runs cargo publish on a v* tag after the release job, fails loudly with an actionable message when CARGO_REGISTRY_TOKEN is absent rather than skipping, and then reads https://crates.io/api/v1/crates/fledge back and compares max_version to the tag, retrying while the index catches up; AGENTS.md documents both the job and the read-the-registry-back rule; the workflow remains valid YAML with jobs test, build, release, publish | ||
|
|
||
| ## No-spec Rationale | ||
|
|
||
| release.yml and AGENTS.md are outside source_dirs (src, templates); no canonical spec covers CI workflows or agent docs |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| --- | ||
| change: publish-to-crates-io-from-the-release-workflow-and-verify-the-registry | ||
| artifact: context | ||
| --- | ||
|
|
||
| # Context | ||
|
|
||
| ## What led here | ||
|
|
||
| crates.io has fledge at **1.7.0**, but **v1.7.1 and v1.7.2 are both tagged** on GitHub. | ||
| Two releases were cut and never shipped, and nobody noticed. | ||
|
|
||
| The cause is not a failure. `release.yml` ran for both tags and **succeeded** — its runs | ||
| are green in the Actions history. It has three jobs: `test`, `build`, `release`. None of | ||
| them publishes. `grep -rn "cargo publish" .github/workflows/` returns nothing, and | ||
| publishing was documented nowhere: not in `AGENTS.md`, not in `CONTRIBUTING.md`, not in | ||
| `docs/`. | ||
|
|
||
| So publishing was a manual step that existed only in somebody's head, and the workflow | ||
| went green while the release was half-done. That is the worst shape a release process can | ||
| have: the signal you would check says everything is fine. | ||
|
|
||
| ## Decisions | ||
|
|
||
| **Automate rather than document-only.** A documented manual step is the thing that | ||
| already failed twice. The job runs on the same `v*` tag push as the rest of the release. | ||
|
|
||
| **Fail loudly when the token is missing, do not skip.** The repository currently has no | ||
| secrets at all, so `CARGO_REGISTRY_TOKEN` must be created before the next release. Until | ||
| then this job fails, and the release shows red. That is deliberate and it is the whole | ||
| point: a release that did not publish is not a finished release, and a skipped step is | ||
| indistinguishable from a passing one — which is precisely how 1.7.1 and 1.7.2 slipped by. | ||
|
|
||
| **Read the registry back.** `cargo publish` exiting 0 does not prove the crate is live: | ||
| the index takes a moment to update, and some no-op paths also exit 0. The job polls | ||
| `https://crates.io/api/v1/crates/fledge` and compares `max_version` against the tag, | ||
| retrying up to six times. CorvidLabs/hi hit this exact failure at 0.2.4/0.2.5 and its | ||
| own guidance now carries the same rule. | ||
|
|
||
| ## Open ends | ||
|
|
||
| - **The secret does not exist yet.** `gh secret list` is empty. Someone with repo admin | ||
| must add `CARGO_REGISTRY_TOKEN` or the next tag will fail at this job. | ||
| - **1.7.1 and 1.7.2 remain unpublished.** This change stops the bleeding; it does not | ||
| backfill. Publishing them retroactively is a separate decision — the versions are | ||
| tagged, so they can be published from those tags if wanted, or skipped in favour of | ||
| 1.8.0. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| { | ||
| "schema_version": 1, | ||
| "workflow_version": 2, | ||
| "workflow_origin_version": 2, | ||
| "id": "publish-to-crates-io-from-the-release-workflow-and-verify-the-registry", | ||
| "slug": "publish-to-crates-io-from-the-release-workflow-and-verify-the-registry", | ||
| "title": "Publish to crates.io from the release workflow and verify the registry", | ||
| "description": "Publish to crates.io from the release workflow and verify the registry", | ||
| "kind": "bug_fix", | ||
| "state": "verifying", | ||
| "canonical_applied": true, | ||
| "base_commit": "31d5f45f7bca1b71e374e41792fabeb6359a9de4", | ||
| "created_at": 1789780671, | ||
| "updated_at": 1789781199, | ||
| "affected_specs": [], | ||
| "affected_paths": [ | ||
| ".github/workflows/release.yml", | ||
| "AGENTS.md" | ||
| ], | ||
| "no_spec_change": true, | ||
| "no_spec_change_rationale": "release.yml and AGENTS.md are outside source_dirs (src, templates); no canonical spec covers CI workflows or agent docs", | ||
| "acceptance_criteria": [ | ||
| "release.yml carries a publish job that runs cargo publish on a v* tag after the release job, fails loudly with an actionable message when CARGO_REGISTRY_TOKEN is absent rather than skipping, and then reads https://crates.io/api/v1/crates/fledge back and compares max_version to the tag, retrying while the index catches up; AGENTS.md documents both the job and the read-the-registry-back rule; the workflow remains valid YAML with jobs test, build, release, publish" | ||
| ], | ||
| "selected_artifacts": [ | ||
| "context", | ||
| "testing", | ||
| "tasks" | ||
| ], | ||
| "dependencies": [], | ||
| "answers": { | ||
| "architecture_risk": "no", | ||
| "public_contract": "no" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| --- | ||
| change: publish-to-crates-io-from-the-release-workflow-and-verify-the-registry | ||
| artifact: tasks | ||
| --- | ||
|
|
||
| # Tasks | ||
|
|
||
| - [x] Confirm the gap: crates.io at 1.7.0, `v1.7.1` and `v1.7.2` both tagged | ||
| - [x] Establish the cause — read the Release runs for both tags (green), and confirm no | ||
| `cargo publish` exists in any workflow | ||
| - [x] Confirm publishing is undocumented in `AGENTS.md`, `CONTRIBUTING.md` and `docs/` | ||
| - [x] Add a `publish` job to `release.yml`, gated after `release`, failing loudly when | ||
| `CARGO_REGISTRY_TOKEN` is absent | ||
| - [x] Add the registry read-back with retry, comparing `max_version` to the tag | ||
| - [x] Document both in `AGENTS.md`, including the manual-publish check | ||
| - [x] Verify the workflow still parses and the job graph is correct |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| --- | ||
| change: publish-to-crates-io-from-the-release-workflow-and-verify-the-registry | ||
| artifact: testing | ||
| --- | ||
|
|
||
| # Testing | ||
|
|
||
| ## What was verified | ||
|
|
||
| | Check | Result | | ||
| |---|---| | ||
| | `release.yml` parses as YAML | passes; jobs are `test`, `build`, `release`, `publish` | | ||
| | `publish.needs` | `[release]` — publishes only after the GitHub release exists | | ||
| | Trigger unchanged | still `on: push: tags: v*`; no new trigger surface | | ||
| | No publish step existed before | `grep -rn "cargo publish" .github/workflows/` returned nothing | | ||
| | The gap is real | crates.io `max_version` = 1.7.0 while `v1.7.1` and `v1.7.2` are tagged | | ||
| | Prior runs did not fail | the Release workflow is green for both tags in the Actions history | | ||
|
|
||
| ## What cannot be verified here | ||
|
|
||
| The job itself cannot run until a `v*` tag is pushed **and** `CARGO_REGISTRY_TOKEN` | ||
| exists. Both halves are untestable in a PR: | ||
|
|
||
| - Without the secret, the first release after this merges will fail at the publish job | ||
| with the actionable message. That is the designed behaviour, not a regression — but it | ||
| does mean the first real exercise of this code is a release. | ||
| - The registry read-back loop is only exercised on a genuine publish. | ||
|
|
||
| This is an honest limitation. The mitigation is that the failure mode is loud and its | ||
| message names the exact fix, rather than silent. | ||
|
|
||
| ## Rejection signals | ||
|
|
||
| - A tagged release whose workflow is green while crates.io still reports the previous | ||
| version — the original bug, and what this job exists to make impossible. | ||
| - The publish job skipping (rather than failing) when the token is absent. | ||
| - `cargo publish` succeeding and the job passing without the registry confirming the new | ||
| version. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| { | ||
| "schema_version": 1, | ||
| "attempts": [ | ||
| { | ||
| "timestamp": 1789781198, | ||
| "commit": "63ccfc1006a537ee683f7e927e38be898e2c0091", | ||
| "contract_digest": "027e4f3c88ddcd03f9f107dd9a570e91ea7b5647d0d165a366659f208a3729e5", | ||
| "execution_digest": "ff5c2a9543a08ce537f0bd52fc6fd44bb264233502c561befcff875ed84c4aeb", | ||
| "workspace_digest": "8a2478d8b7da9dbe7eea5a01f27de930641df0dd7de61d7030fc40d44c1f0c55", | ||
| "passed": true, | ||
| "commands": [ | ||
| { | ||
| "command": "specsync check (no spec in scope)", | ||
| "success": true, | ||
| "exit_code": 0 | ||
| } | ||
| ], | ||
| "requirement_ids": [] | ||
| } | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "timestamp": 1789781198, | ||
| "commit": "63ccfc1006a537ee683f7e927e38be898e2c0091", | ||
| "contract_digest": "027e4f3c88ddcd03f9f107dd9a570e91ea7b5647d0d165a366659f208a3729e5", | ||
| "execution_digest": "ff5c2a9543a08ce537f0bd52fc6fd44bb264233502c561befcff875ed84c4aeb", | ||
| "workspace_digest": "8a2478d8b7da9dbe7eea5a01f27de930641df0dd7de61d7030fc40d44c1f0c55", | ||
| "passed": true, | ||
| "commands": [ | ||
| { | ||
| "command": "specsync check (no spec in scope)", | ||
| "success": true, | ||
| "exit_code": 0 | ||
| } | ||
| ], | ||
| "requirement_ids": [] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For any manually pushed or mistyped
v*tag whose suffix differs fromCargo.toml, this command publishes the manifest's version before the later step compares crates.io withGITHUB_REF_NAME. For example, av1.8.1tag on a manifest still at1.8.0would irreversibly publish 1.8.0 and only then report failure. Read the package version and require it to equal${GITHUB_REF_NAME#v}before invokingcargo publish.AGENTS.md reference: AGENTS.md:L363-L363
Useful? React with 👍 / 👎.