From 63ccfc1006a537ee683f7e927e38be898e2c0091 Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Fri, 18 Sep 2026 19:26:35 -0600 Subject: [PATCH 1/2] Fix: publish to crates.io from the release workflow, and verify the registry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) Claude-Session: https://claude.ai/code/session_01M9GwmgSChrXYZhLjUYejwP --- .github/workflows/release.yml | 43 +++++++++++++++++ .../approvals.json | 36 ++++++++++++++ .../change.md | 24 ++++++++++ .../context.md | 47 +++++++++++++++++++ .../state.json | 34 ++++++++++++++ .../tasks.md | 16 +++++++ .../testing.md | 38 +++++++++++++++ AGENTS.md | 6 +++ 8 files changed, 244 insertions(+) create mode 100644 .specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/approvals.json create mode 100644 .specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/change.md create mode 100644 .specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/context.md create mode 100644 .specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/state.json create mode 100644 .specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/tasks.md create mode 100644 .specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/testing.md diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8f13c01..b1e4afeb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 + # 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'])") + 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 diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/approvals.json b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/approvals.json new file mode 100644 index 00000000..14054918 --- /dev/null +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/approvals.json @@ -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": [] +} diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/change.md b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/change.md new file mode 100644 index 00000000..c0d4ef85 --- /dev/null +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/change.md @@ -0,0 +1,24 @@ +--- +id: publish-to-crates-io-from-the-release-workflow-and-verify-the-registry +state: approved +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 diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/context.md b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/context.md new file mode 100644 index 00000000..e1a289dd --- /dev/null +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/context.md @@ -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. diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/state.json b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/state.json new file mode 100644 index 00000000..a7164566 --- /dev/null +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/state.json @@ -0,0 +1,34 @@ +{ + "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": "approved", + "base_commit": "31d5f45f7bca1b71e374e41792fabeb6359a9de4", + "created_at": 1789780671, + "updated_at": 1789781187, + "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" + } +} diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/tasks.md b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/tasks.md new file mode 100644 index 00000000..2696ccfa --- /dev/null +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/tasks.md @@ -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 diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/testing.md b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/testing.md new file mode 100644 index 00000000..21c67ce1 --- /dev/null +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/testing.md @@ -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. diff --git a/AGENTS.md b/AGENTS.md index 6be7f6e1..8d91ca50 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -359,6 +359,12 @@ Or via fledge's own lanes: `fledge lanes run pre-commit` (fmt + lint + test + sp - Run `fledge spec check` before committing. It delegates to the `specsync` binary when installed (matching CI's export-coverage validation) and falls back to a structural check otherwise - No direct commits to main — use feature branches - Releases bump `Cargo.toml` and `flake.nix` together (see `[release].files` in `fledge.toml`); the Homebrew formula in `CorvidLabs/homebrew-tap` is updated by `post-release-formula.yml` +- Publishing to crates.io is done by the `publish` job in `release.yml`, which fires on the `v*` tag push like the rest of the release. It needs the `CARGO_REGISTRY_TOKEN` repository secret; without it the job fails loudly rather than skipping, because a release that did not publish is not a finished release. **This was added after v1.7.1 and v1.7.2 were both tagged, both ran the release workflow green, and neither reached crates.io — the workflow simply had no publish step and nothing said so.** +- Never treat `cargo publish` exiting 0 as proof the crate is live. The job reads `https://crates.io/api/v1/crates/fledge` back and compares `max_version` to the tag, retrying while the index catches up. If you ever publish by hand, do the same check: + ```sh + curl -s -A "release-check" https://crates.io/api/v1/crates/fledge \ + | python3 -c "import json,sys; print(json.load(sys.stdin)['crate']['max_version'])" + ``` ## Extending fledge for better agent support From b43acddd5be25235550aae10c46a24ceb5d6e71e Mon Sep 17 00:00:00 2001 From: 0xLeif Date: Fri, 18 Sep 2026 19:27:56 -0600 Subject: [PATCH 2/2] chore(lifecycle): record verification for the crates.io publish fix 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) Claude-Session: https://claude.ai/code/session_01M9GwmgSChrXYZhLjUYejwP --- .../change.md | 2 +- .../state.json | 5 +++-- .../verification-attempts.json | 21 +++++++++++++++++++ .../verification.json | 16 ++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) create mode 100644 .specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/verification-attempts.json create mode 100644 .specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/verification.json diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/change.md b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/change.md index c0d4ef85..cc5dbe6d 100644 --- a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/change.md +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/change.md @@ -1,6 +1,6 @@ --- id: publish-to-crates-io-from-the-release-workflow-and-verify-the-registry -state: approved +state: implementing type: bug_fix base_commit: 31d5f45f7bca1b71e374e41792fabeb6359a9de4 --- diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/state.json b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/state.json index a7164566..ef9b0c93 100644 --- a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/state.json +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/state.json @@ -7,10 +7,11 @@ "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": "approved", + "state": "verifying", + "canonical_applied": true, "base_commit": "31d5f45f7bca1b71e374e41792fabeb6359a9de4", "created_at": 1789780671, - "updated_at": 1789781187, + "updated_at": 1789781199, "affected_specs": [], "affected_paths": [ ".github/workflows/release.yml", diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/verification-attempts.json b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/verification-attempts.json new file mode 100644 index 00000000..c0acdd4f --- /dev/null +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/verification-attempts.json @@ -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": [] + } + ] +} diff --git a/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/verification.json b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/verification.json new file mode 100644 index 00000000..17c6d506 --- /dev/null +++ b/.specsync/changes/publish-to-crates-io-from-the-release-workflow-and-verify-the-registry/verification.json @@ -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": [] +}