-
Notifications
You must be signed in to change notification settings - Fork 111
Automate crates.io releases #1770
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
Merged
spacebear21
merged 8 commits into
payjoin:master
from
spacebear21:automate-cargo-releases
Jul 30, 2026
Merged
Changes from all commits
Commits
Show all changes
8 commits
Select commit
Hold shift + click to select a range
fbfa31c
Align test-utils requirement on mailroom with its version
spacebear21 3e2ed49
Add release script library and invariant checks
spacebear21 a7f09a3
Add changelog extraction script
spacebear21 c5a7110
Add tag and publication verification scripts
spacebear21 cbcef72
Add the crates.io release workflow
spacebear21 0a6c218
Narrow the mailroom image tag filter
spacebear21 38f6ade
Rewrite the minor release checklist
spacebear21 99a1aa7
Add spacebear release signing key
spacebear21 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,207 @@ | ||
| name: Release | ||
|
|
||
| # A pull request that bumps a release crate's version is checked for | ||
| # consistency and publishability. Pushing a `<crate>-<version>` tag verifies | ||
| # the tag, waits for CI at that commit, then publishes to crates.io through | ||
| # the `crates-release` environment (required reviewer) with keyless OIDC, | ||
| # cuts the GitHub release, and confirms the upload. | ||
|
|
||
| on: | ||
| pull_request: | ||
| paths: | ||
| - payjoin/Cargo.toml | ||
| - payjoin-cli/Cargo.toml | ||
| - payjoin-mailroom/Cargo.toml | ||
| push: | ||
| tags: | ||
| - payjoin-[0-9]* | ||
| - payjoin-cli-[0-9]* | ||
| - payjoin-mailroom-[0-9]* | ||
|
|
||
| permissions: | ||
| contents: read | ||
|
|
||
| jobs: | ||
| check-bump: | ||
| name: Check release version bump | ||
| if: github.event_name == 'pull_request' | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Install nix | ||
| uses: DeterminateSystems/determinate-nix-action@main | ||
| - name: Use nix cache | ||
| uses: DeterminateSystems/magic-nix-cache-action@main | ||
| - name: Check the bump is consistent and publishable | ||
| env: | ||
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | ||
| run: nix --quiet develop .#release -c ./contrib/release/check-bump.sh "$BASE_SHA" | ||
|
|
||
| verify-tag: | ||
| name: Verify release tag | ||
| if: github.event_name == 'push' | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| crate: ${{ steps.meta.outputs.crate }} | ||
| version: ${{ steps.meta.outputs.version }} | ||
| prerelease: ${{ steps.meta.outputs.prerelease }} | ||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v6 | ||
| with: | ||
| fetch-depth: 0 | ||
| - name: Fetch master | ||
| run: git fetch --no-tags origin +refs/heads/master:refs/remotes/origin/master | ||
| - name: Install nix | ||
| uses: DeterminateSystems/determinate-nix-action@main | ||
| - name: Use nix cache | ||
| uses: DeterminateSystems/magic-nix-cache-action@main | ||
| - name: Compute tag metadata | ||
| id: meta | ||
| env: | ||
| TAG: ${{ github.ref_name }} | ||
| run: | | ||
| source contrib/release/crates.sh | ||
| version="$(version_from_tag "$TAG")" | ||
| { | ||
| echo "crate=$(crate_from_tag "$TAG")" | ||
| echo "version=$version" | ||
| is_prerelease "$version" && echo "prerelease=true" || echo "prerelease=false" | ||
| } >>"$GITHUB_OUTPUT" | ||
| - name: Verify tag | ||
| env: | ||
| TAG: ${{ github.ref_name }} | ||
| run: nix --quiet develop .#release -c ./contrib/release/verify-tag.sh "$TAG" | ||
|
|
||
| wait-for-ci: | ||
| name: Wait for CI | ||
| if: github.event_name == 'push' | ||
| needs: verify-tag | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| checks: read | ||
| contents: read | ||
| steps: | ||
| # Wait only for the correctness checks (rust.yml, format.yml) at the | ||
| # tagged commit. Scoping by regexp avoids waiting on this workflow's | ||
| # own downstream publish jobs, which would deadlock. | ||
| - name: Wait for test, lint, and format checks | ||
| uses: lewagon/wait-on-check-action@v1.9.0 | ||
| with: | ||
| ref: ${{ github.sha }} | ||
| check-regexp: ^(Test|Lint|Format) | ||
| repo-token: ${{ secrets.GITHUB_TOKEN }} | ||
| wait-interval: 30 | ||
|
|
||
| publish: | ||
| name: Publish to crates.io | ||
| if: github.event_name == 'push' | ||
| needs: [verify-tag, wait-for-ci] | ||
| runs-on: ubuntu-latest | ||
| environment: crates-release | ||
| permissions: | ||
| id-token: write | ||
| attestations: write | ||
| contents: read | ||
| env: | ||
| RUSTUP_TOOLCHAIN: stable | ||
| CRATE: ${{ needs.verify-tag.outputs.crate }} | ||
| VERSION: ${{ needs.verify-tag.outputs.version }} | ||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v6 | ||
| - name: Install toolchain | ||
| uses: dtolnay/rust-toolchain@stable | ||
| - name: Use cache | ||
| uses: Swatinem/rust-cache@v2 | ||
| - name: Prepare lockfile | ||
| run: cp Cargo-recent.lock Cargo.lock | ||
| - name: Package the crate | ||
| run: cargo package --locked -p "$CRATE" | ||
| - name: Attest build provenance | ||
| uses: actions/attest-build-provenance@v4 | ||
| with: | ||
| subject-path: target/package/${{ needs.verify-tag.outputs.crate }}-${{ needs.verify-tag.outputs.version }}.crate | ||
| - name: Authenticate to crates.io | ||
| id: auth | ||
| uses: rust-lang/crates-io-auth-action@v1 | ||
| - name: Publish to crates.io | ||
| env: | ||
| CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }} | ||
| run: cargo publish --locked -p "$CRATE" | ||
| - name: Upload packaged crate | ||
| uses: actions/upload-artifact@v4 | ||
| with: | ||
| name: crate | ||
| path: target/package/${{ needs.verify-tag.outputs.crate }}-${{ needs.verify-tag.outputs.version }}.crate | ||
|
|
||
| github-release: | ||
| name: Create GitHub release | ||
| if: github.event_name == 'push' | ||
| needs: [verify-tag, publish] | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: write | ||
| env: | ||
| CRATE: ${{ needs.verify-tag.outputs.crate }} | ||
| VERSION: ${{ needs.verify-tag.outputs.version }} | ||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v6 | ||
| - name: Install nix | ||
| uses: DeterminateSystems/determinate-nix-action@main | ||
| - name: Use nix cache | ||
| uses: DeterminateSystems/magic-nix-cache-action@main | ||
| - name: Download packaged crate | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: crate | ||
| path: dist | ||
| - name: Generate SHA256SUMS | ||
| run: (cd dist && sha256sum ./*.crate >SHA256SUMS) | ||
| - name: Extract changelog section | ||
| id: changelog | ||
| run: | | ||
| echo "Extracting the ## $VERSION section of $CRATE/CHANGELOG.md" | ||
| { | ||
| echo "body<<CHANGELOG_EOF" | ||
| nix --quiet develop .#release -c ./contrib/release/extract-changelog.sh "$CRATE" "$VERSION" | ||
| echo "CHANGELOG_EOF" | ||
| } >>"$GITHUB_OUTPUT" | ||
| - name: Create release | ||
| uses: softprops/action-gh-release@v3 | ||
| with: | ||
| tag_name: ${{ github.ref_name }} | ||
| name: Release ${{ github.ref_name }} | ||
| body: ${{ steps.changelog.outputs.body }} | ||
| generate_release_notes: true | ||
| prerelease: ${{ needs.verify-tag.outputs.prerelease }} | ||
| files: | | ||
| dist/*.crate | ||
| dist/SHA256SUMS | ||
|
|
||
| verify-published: | ||
| name: Verify publication | ||
| if: github.event_name == 'push' | ||
| needs: [verify-tag, publish] | ||
| runs-on: ubuntu-latest | ||
| env: | ||
| CRATE: ${{ needs.verify-tag.outputs.crate }} | ||
| VERSION: ${{ needs.verify-tag.outputs.version }} | ||
| steps: | ||
| - name: Checkout repo | ||
| uses: actions/checkout@v6 | ||
| - name: Install nix | ||
| uses: DeterminateSystems/determinate-nix-action@main | ||
| - name: Use nix cache | ||
| uses: DeterminateSystems/magic-nix-cache-action@main | ||
| - name: Download packaged crate | ||
| uses: actions/download-artifact@v4 | ||
| with: | ||
| name: crate | ||
| path: dist | ||
| - name: Verify crates.io and docs.rs | ||
| run: nix --quiet develop .#release -c ./contrib/release/verify-published.sh "$CRATE" "$VERSION" "dist/$CRATE-$VERSION.crate" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # Pull request check. For each release crate whose version changed relative | ||
| # to the base commit, confirm the bump is consistent (check-invariants) and | ||
| # the crate still publishes (cargo publish --dry-run). 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. | ||
| set -euo pipefail | ||
| DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| # shellcheck source=contrib/release/crates.sh | ||
| source "$DIR/crates.sh" | ||
| cd "$REPO_ROOT" | ||
| # shellcheck source=contrib/lockfile.sh | ||
| source contrib/lockfile.sh | ||
|
|
||
| [ "$#" -eq 1 ] || { | ||
| echo "usage: check-bump.sh <base-sha>" >&2 | ||
| exit 1 | ||
| } | ||
| base="$1" | ||
|
|
||
| # A crate's [package] version at a git ref (the only line-anchored `version`). | ||
| version_at() { | ||
| git show "$1:$2/Cargo.toml" 2>/dev/null | sed -n 's/^version = "\(.*\)"/\1/p' | head -1 | ||
| } | ||
|
|
||
| RELEASE_METADATA="$(cargo_metadata)" | ||
| echo "Comparing release crate versions against $base" | ||
| changed=() | ||
| for crate in $RELEASE_CRATES; do | ||
| [ "$(version_at "$base" "$crate")" = "$(manifest_version "$crate")" ] || changed+=("$crate") | ||
| done | ||
|
|
||
| if [ "${#changed[@]}" -eq 0 ]; then | ||
| echo "No release crate version changed" | ||
| exit 0 | ||
| fi | ||
|
|
||
| echo "Version changed for: ${changed[*]}" | ||
| "$DIR/check-invariants.sh" "${changed[@]}" | ||
|
|
||
| use_lockfile Cargo-recent.lock | ||
| for crate in "${changed[@]}"; do | ||
| unpublished="" | ||
| while IFS=$'\t' read -r dep req; do | ||
| [ -n "$dep" ] || continue | ||
| v="${req#^}" | ||
| v="${v#=}" | ||
| crate_published "$dep" "$v" || unpublished="$unpublished $dep $v" | ||
| done < <(sibling_deps "$crate") | ||
| if [ -n "$unpublished" ]; then | ||
| echo "Skipping $crate dry-run; sibling not on crates.io yet:$unpublished" | ||
| continue | ||
| fi | ||
| echo "Dry-run publishing $crate" | ||
| cargo publish --dry-run --locked -q -p "$crate" | ||
| echo "$crate packages and publishes cleanly" | ||
| done |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| #!/usr/bin/env bash | ||
| # | ||
| # Check that each release crate is internally consistent: every dependent's | ||
| # version requirement on it matches its manifest version, both tracked lock | ||
| # files record that version, and its CHANGELOG.md has a section for it. | ||
| # Offline, a few seconds. Checks all release crates, or the ones named. | ||
| set -euo pipefail | ||
| # shellcheck source=contrib/release/crates.sh | ||
| source "$(dirname "${BASH_SOURCE[0]}")/crates.sh" | ||
|
|
||
| crates="${*:-$RELEASE_CRATES}" | ||
| RELEASE_METADATA="$(cargo_metadata)" | ||
| status=0 | ||
| problem() { | ||
| echo "$*" >&2 | ||
| status=1 | ||
| } | ||
|
|
||
| # Print a crate's version as recorded in a Cargo lock file. | ||
| lockfile_version() { | ||
| grep -A1 "^name = \"$2\"\$" "$1" | sed -n 's/^version = "\(.*\)"/\1/p' | ||
| } | ||
|
|
||
| echo "Checking version requirements on: $crates" | ||
| # Version requirements that do not match the depended-on crate's version. | ||
| mismatches="$(cargo_metadata | jq -r --argjson t "$(printf '%s' "$crates" | jq -R 'split(" ")')" ' | ||
| (.packages | map({key: .name, value: .version}) | from_entries) as $ver | ||
| | .packages[] as $p | ||
| | $p.dependencies[] | ||
| | select(.path == null and (.name | IN($t[]))) | ||
| | select((.req | ltrimstr("^") | ltrimstr("=")) != $ver[.name]) | ||
| | "\($p.name) requires \(.name) \(.req), expected \($ver[.name])" | ||
| ')" | ||
| [ -z "$mismatches" ] || problem "$mismatches" | ||
|
|
||
| for crate in $crates; do | ||
| version="$(manifest_version "$crate")" | ||
| echo "Checking $crate $version: lock files and CHANGELOG.md" | ||
| for lock in Cargo-minimal.lock Cargo-recent.lock; do | ||
| recorded="$(lockfile_version "$REPO_ROOT/$lock" "$crate")" | ||
| [ "$recorded" = "$version" ] || problem "$lock records $crate ${recorded:-<missing>}, expected $version" | ||
| done | ||
| grep -qxF "## $version" "$REPO_ROOT/$crate/CHANGELOG.md" || problem "$crate/CHANGELOG.md has no ## $version section" | ||
| done | ||
|
|
||
| [ "$status" -eq 0 ] && echo "Release invariants hold for: $crates" | ||
| exit "$status" |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
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.
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.