Skip to content

Automate crates.io releases - #1770

Merged
spacebear21 merged 8 commits into
payjoin:masterfrom
spacebear21:automate-cargo-releases
Jul 30, 2026
Merged

Automate crates.io releases#1770
spacebear21 merged 8 commits into
payjoin:masterfrom
spacebear21:automate-cargo-releases

Conversation

@spacebear21

@spacebear21 spacebear21 commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

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.

  • A PR touching a release crate's Cargo.toml runs check-bump.sh: version requirements, both lock files, and the CHANGELOG.md section have to agree, then cargo publish --dry-run.
  • A tag <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.io trusted publishing on payjoin, payjoin-cli, and payjoin-mailroom: owner payjoin, repo rust-payjoin, workflow crates-release.yml, environment crates-release. A crate owner has to add each policy. The environment field binds the policy to the approval gate; without it, any run of that workflow file can mint a publish token.
  • A GitHub environment named exactly crates-release with 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.
  • Add additional signing keys to 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 with gpg --armor --export <key-id> > contrib/release/keys/<your-name>.asc

Until 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:

@DanGould

DanGould commented Jul 29, 2026

Copy link
Copy Markdown
Member

A tag <crate>-v<version> triggers verify-tag

I had the same structure at first, but @chavic pointed out our existing tag format is <crate>-[0-9]* with no v, and imo it makes sense to keep that. Is the regex that fancy?

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.
@spacebear21
spacebear21 force-pushed the automate-cargo-releases branch from af08b36 to c205003 Compare July 29, 2026 21:23
@coveralls

coveralls commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Coverage Report for CI Build 30577945891

Coverage remained the same at 86.494%

Details

  • Coverage remained the same as the base build.
  • Patch coverage: No coverable lines changed in this PR.
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 16200
Covered Lines: 14012
Line Coverage: 86.49%
Coverage Strength: 340.8 hits per line

💛 - Coveralls

spacebear21 added a commit to spacebear21/rust-payjoin that referenced this pull request Jul 29, 2026
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.
spacebear21 added a commit to spacebear21/rust-payjoin that referenced this pull request Jul 29, 2026
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.
@spacebear21
spacebear21 force-pushed the automate-cargo-releases branch 2 times, most recently from d6c6c26 to 878e195 Compare July 29, 2026 23:14
@spacebear21
spacebear21 marked this pull request as ready for review July 29, 2026 23:14
@spacebear21

Copy link
Copy Markdown
Collaborator Author

Here is the job that will run on version bump PRs, simulated with a dummy commit.

@DanGould DanGould left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

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.

@spacebear21

spacebear21 commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

this is a big pile of scripts to yolo. What's the plan for verifying the scripts?

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.

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

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.

@spacebear21
spacebear21 force-pushed the automate-cargo-releases branch from 878e195 to 99a1aa7 Compare July 30, 2026 20:09

@benalleng benalleng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

ACK 99a1aa7

Will immediately follow-up with my signing key PR

@spacebear21
spacebear21 merged commit 91bd383 into payjoin:master Jul 30, 2026
35 checks passed
@DanGould

Copy link
Copy Markdown
Member

#1777

benalleng added a commit that referenced this pull request Aug 3, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants