Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

Comment on lines +140 to +141

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

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": []
}
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading