Automate crates.io releases - #1770
Conversation
I had the same structure at first, but @chavic pointed out our existing tag format is |
payjoin-mailroom's manifest is at 0.1.2, but payjoin-test-utils still declared a dependency requirement of 0.1.1 on it. Align the two so the forthcoming release invariant check, which requires every workspace member's requirement on a release crate to equal that crate's manifest version, holds on master.
af08b36 to
c205003
Compare
Coverage Report for CI Build 30577945891Coverage remained the same at 86.494%Details
Uncovered ChangesNo uncovered changes found. Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
Throwaway commit to make the `pull_request` trigger in crates-release.yml match. The workflow only fires on changes to a release crate's Cargo.toml, and PR payjoin#1770 touches none of them, so check-bump has never run against itself. Bumps payjoin-cli 1.0.0-rc.1 to 1.0.0-rc.2 as a consistent bump: manifest version, both tracked lock files, and a CHANGELOG section, which is what check-invariants.sh requires. payjoin-cli is a leaf crate, so no dependent version requirements need updating. Abandon this commit before merging.
Start the crates.io release tooling with a sourced helper library and an offline invariant checker, mirroring how contrib/lockfile.sh is sourced by contrib/test.sh. contrib/release/crates.sh defines the release crate set, reads versions from `cargo metadata` parsed with jq, maps a `<crate>-v<version>` tag back to its crate, and provides shared helpers for sibling dependencies and crates.io lookups. contrib/release/check-invariants.sh turns the checklist's consistency steps into machine checks: every workspace member's requirement on a release crate equals its manifest version, both tracked lock files record that version, and the crate's CHANGELOG.md carries a section for it. Add a `release` devshell pinning the tools these scripts need (a Rust toolchain, jq, gnupg, curl, git, and GNU grep/sed/awk/coreutils) so they run identically under `nix develop .#release -c` locally and in CI.
extract-changelog.sh prints a crate's CHANGELOG.md section for a given version, the text between its `## <version>` heading and the next `## ` heading with surrounding blank lines trimmed. The release workflow uses it to fill the GitHub release body from the hand-written changelog.
verify-tag.sh is the release gate. It confirms the tag is annotated, its GPG signature verifies against a throwaway keyring seeded only with contrib/release/keys/*.asc, the tagged commit is an ancestor of origin/master, the tag version matches the manifest, the release invariants hold, and every sibling release-crate dependency is already on crates.io. It reads the working tree, so it runs at the tagged commit, which the workflow guarantees. check-bump.sh runs on pull requests. For each release crate whose version changed relative to the PR base it checks the bump is consistent (check-invariants) and still publishes (cargo publish --dry-run), and no-ops when no release version changed. A sibling release crate not yet on crates.io is skipped, not failed, since a PR may bump two crates at once. verify-published.sh runs after upload. It polls crates.io and fails if the published checksum does not match the sha256 of the attested .crate, then polls docs.rs until it reports the docs built.
Wire the release scripts into CI as thin callers running in the pinned `release` devshell. A pull request that bumps a release crate's version runs check-bump.sh; the paths filter keeps it off unrelated PRs and the script no-ops when no version changed. On a pushed `<crate>-v<version>` tag it verifies the tag, waits for the test, lint, and format checks at the tagged commit, then publishes. Publishing runs in the `crates-release` environment so it pauses for a required reviewer, packages the crate, attests build provenance on the .crate, mints a short-lived crates.io token via keyless OIDC, and publishes. A follow-up job cuts the GitHub release with the changelog section plus auto-generated notes and the attested .crate, and a final job confirms crates.io reports a matching checksum and docs.rs built. wait-for-ci scopes its check wait by regexp rather than the whole commit, so it does not deadlock on this workflow's own downstream publish jobs.
The Docker image workflow triggered on `payjoin-mailroom-**`, which also matched any future subtag. Narrow it to `payjoin-mailroom-[0-9]*` to align with the release tag scheme and to keep it from firing on unrelated tags.
Update the release template for the new tooling and tag scheme. Tags now follow `<crate>-v<version>`, and the bump touches one crate plus the workspace requirements on it rather than every crate at once. The crates-release workflow checks the bump PR, verifies the tag, waits for CI, publishes to crates.io, cuts the GitHub release, and confirms the upload, so those steps leave the checklist. What remains is what a human must do: write the changelog, sign and push the tag, approve the crates-release environment, and announce.
Throwaway commit to make the `pull_request` trigger in crates-release.yml match. The workflow only fires on changes to a release crate's Cargo.toml, and PR payjoin#1770 touches none of them, so check-bump has never run against itself. Bumps payjoin-cli 1.0.0-rc.1 to 1.0.0-rc.2 as a consistent bump: manifest version, both tracked lock files, and a CHANGELOG section, which is what check-invariants.sh requires. payjoin-cli is a leaf crate, so no dependent version requirements need updating. Abandon this commit before merging.
d6c6c26 to
878e195
Compare
|
Here is the job that will run on version bump PRs, simulated with a dummy commit. |
DanGould
left a comment
There was a problem hiding this comment.
appraoch ACk here
A few review comments
- this is a big pile of scripts to yolo. What's the plan for verifying the scripts?
- rebasing un-verifies key commits from ben and I. Shall we re-attest? Or perhaps our ACK on the final commit is sufficient? Not sure best practice here
|
|
||
| Tag and publish new release: | ||
|
|
||
| - [ ] Check that the crate is publishable with `cargo publish --dry-run` from that crate's directory. |
There was a problem hiding this comment.
I think we still want this step because dry run covers things that aren't covered in a plain build, or is this gone because --dry-run is already part of the action as I see, and the assumption just that you'd re-tag? Seems like sanity before tag & retag is cheap insurance to leave here
There was a problem hiding this comment.
The idea is that the check-bump.sh runs automatically on any version bump PR and includes a --dry-run. This PR must be merged before the tag is made.
they were smoke tested locally but to test publishing an actual release i think we'll have to yeet it and see. Worst case scenario is something breaks and it fails to publish. It can't accidentally publish something bad without in-github action approvals.
I guess the clean way if we want key commits signed by the relevant person, is to drop those commits from this PR and have each of you open your own key-adding PRs as follow-ups. EDIT: I've dropped them so we can proceed with this. |
878e195 to
99a1aa7
Compare
Following a merge from #1770 I came across a nix fmt error whereby the shellcheck formatter needs external-sources enabled to be able to verify the formatting properly.
Replaces the manual publish flow with a Release workflow backed by scripts in contrib/release/. A human writes the changelog, signs the tag, approves the deploy, and announces. Everything mechanical is checked by CI.
<crate>-<version>triggers verify-tag (annotated, signed by a key in contrib/release/keys/, ancestor of master, matches the manifest), then waits for Test/Lint/Format at that commit, publishes through the crates-release environment with a short-lived OIDC token, cuts the GitHub release with the .crate, SHA256SUMS and changelog body, and confirms the crates.io checksum and docs.rs build.TODO
There are some additional setup steps we need to do before this actually works:
crates-releasewith the release managers as required reviewers. This is the only human gate between a pushed tag and a published crate. GitHub auto-creates a referenced environment with no protection rules, so the gate is real only once reviewers are added.contrib/release/keys/. Those keys are the only ones that are allowed to sign a release tag. I've added mine already in a commit here. @benalleng and @DanGould please add yours here too withgpg --armor --export <key-id> > contrib/release/keys/<your-name>.ascUntil the trusted publishing policies exist, a pushed tag fails at the publish step.
Disclosure: co-authored by Claude Code
Pull Request Checklist
Please confirm the following before requesting review:
AI
in the body of this PR.