diff --git a/.github/ISSUE_TEMPLATE/minor-release.md b/.github/ISSUE_TEMPLATE/minor-release.md index 9e40ddf97..a812f1b72 100644 --- a/.github/ISSUE_TEMPLATE/minor-release.md +++ b/.github/ISSUE_TEMPLATE/minor-release.md @@ -23,7 +23,8 @@ assignees: "" ### Checklist Release numbering must follow [Semantic Versioning]. These steps assume the current `master` -branch **development** version is _MAJOR.MINOR.0_. +branch **development** version is _MAJOR.MINOR.0_. Release-managed crates are `payjoin`, +`payjoin-cli`, and `payjoin-mailroom`; tags use the `-` scheme. #### On the day of the feature freeze @@ -32,9 +33,11 @@ Change the `master` branch to the next MINOR+1 version: - [ ] Switch to the `master` branch. - [ ] Create a new PR branch called `bump-CRATE-MAJOR-MINOR+1`, eg. `bump-CRATE-0-22`. - [ ] Bump the `bump-CRATE-MAJOR-MINOR+1` branch to the next development MINOR+1 version. - - Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0` for all crates in the workspace. + - Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0` for the crate being released, + and update every workspace member's version requirement on it to match. - Run `contrib/update-lock-files.sh` to apply upgrades to the Cargo lock files. - - Update the `CHANGELOG.md` file. + - Update the crate's `CHANGELOG.md` file, adding a `## MAJOR.MINOR+1.0` section that + summarizes the PRs merged since the last release tag. - The commit message should be "Bump CRATE version to MAJOR.MINOR+1.0". - [ ] Create PR for the `bump-CRATE-MAJOR-MINOR+1` branch to `master`. - Title PR "Bump CRATE version to MAJOR.MINOR+1.0". @@ -48,25 +51,13 @@ If any issues need to be fixed before the _MAJOR.MINOR+1.0_ version is released: #### On the day of the release -Tag and publish new release: - -- [ ] Check that the crate is publishable with `cargo publish --dry-run` from that crate's directory. -- [ ] Add a tag to the `HEAD` commit in the `master` branch. - - The tag name should be `CRATE-MAJOR.MINOR+1.0` +- [ ] Create a signed annotated tag on the `HEAD` commit in the `master` branch. + - The tag name should be `CRATE-MAJOR.MINOR+1.0`, eg. `payjoin-1.0.0`. - The first line of the tag message should be "Release CRATE-MAJOR.MINOR+1.0". - In the body of the tag message put a copy of the **Summary** and **Changelog** for the release. - - Make sure the tag is signed, for extra safety use the explicit `--sign` flag. -- [ ] Wait for the CI to finish one last time. -- [ ] Build the docs locally to ensure they are building correctly. + - Sign with a key committed under `contrib/release/keys/`, using the explicit `--sign` flag. - [ ] Push the new tag to the `payjoin/rust-payjoin` repo. -- [ ] Publish the crate in question to crates.io. -- [ ] Create the release on GitHub. - - Go to "tags", click on the dots on the right and select "Create Release". - - Set the title to `Release CRATE-MAJOR.MINOR+1.0`. - - In the release notes body put the **Summary** and **Changelog**. - - Use the "+ Auto-generate release notes" button to add details from included PRs. - - Until we reach a `1.0.0` release check the "Pre-release" box. -- [ ] Make sure the new release shows up on [crates.io] and that the docs are built correctly on [docs.rs]. +- [ ] Approve the `crates-release` environment when it requests a reviewer. - [ ] Announce the release, using the **Summary**, on Discord, Twitter, Nostr, and stacker.news. - [ ] Celebrate 🎉 diff --git a/.github/workflows/crates-release.yml b/.github/workflows/crates-release.yml new file mode 100644 index 000000000..a6bf2310c --- /dev/null +++ b/.github/workflows/crates-release.yml @@ -0,0 +1,207 @@ +name: Release + +# A pull request that bumps a release crate's version is checked for +# consistency and publishability. Pushing a `-` 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<>"$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" diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index b60f6f750..64a46c340 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -6,7 +6,7 @@ on: - payjoin-mailroom/** push: tags: - - payjoin-mailroom-** + - payjoin-mailroom-[0-9]* workflow_dispatch: jobs: diff --git a/contrib/release/check-bump.sh b/contrib/release/check-bump.sh new file mode 100755 index 000000000..7be846c98 --- /dev/null +++ b/contrib/release/check-bump.sh @@ -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 " >&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 diff --git a/contrib/release/check-invariants.sh b/contrib/release/check-invariants.sh new file mode 100755 index 000000000..0ef8b0b8e --- /dev/null +++ b/contrib/release/check-invariants.sh @@ -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:-}, 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" diff --git a/contrib/release/crates.sh b/contrib/release/crates.sh new file mode 100644 index 000000000..7c0a8b07d --- /dev/null +++ b/contrib/release/crates.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash +# +# Shared helpers for the release scripts, sourced like contrib/lockfile.sh. +# Versions come from `cargo metadata` parsed with jq, not from grepping +# manifests. + +RELEASE_CRATES="payjoin payjoin-cli payjoin-mailroom" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# `cargo metadata` for the workspace. Command substitutions run in a subshell +# that cannot write the cache back, so a caller that queries repeatedly sets +# it once in its own shell: RELEASE_METADATA="$(cargo_metadata)". +cargo_metadata() { + if [ -n "${RELEASE_METADATA:-}" ]; then + printf '%s' "$RELEASE_METADATA" + else + cargo metadata --no-deps --format-version 1 --manifest-path "$REPO_ROOT/Cargo.toml" + fi +} + +# Print a crate's manifest version. +manifest_version() { + cargo_metadata | jq -r --arg c "$1" '.packages[] | select(.name == $c) | .version' +} + +# Print the release crate a `-` tag belongs to, or fail. +crate_from_tag() { + local crate="${1%-[0-9]*}" + case " $RELEASE_CRATES " in + *" $crate "*) printf '%s' "$crate" ;; + *) return 1 ;; + esac +} + +# Print the version in a `-` tag. +version_from_tag() { + local crate + crate="$(crate_from_tag "$1")" || return 1 + printf '%s' "${1#"$crate"-}" +} + +# Succeed if the version has a semver pre-release suffix. +is_prerelease() { + case "$1" in + *-*) return 0 ;; + *) return 1 ;; + esac +} + +# Print "\t" for each sibling release-crate dependency of a crate. +sibling_deps() { + local rel + rel="$(printf '%s' "$RELEASE_CRATES" | jq -R 'split(" ")')" + cargo_metadata | jq -r --arg self "$1" --argjson rel "$rel" ' + .packages[] | select(.name == $self) | .dependencies[] + | select(.path == null and .name != $self and (.name | IN($rel[]))) + | "\(.name)\t\(.req)" + ' +} + +# Succeed if exists on crates.io. +crate_published() { + curl -sfL -o /dev/null -H "User-Agent: rust-payjoin release tooling" \ + "https://crates.io/api/v1/crates/$1/$2" +} diff --git a/contrib/release/extract-changelog.sh b/contrib/release/extract-changelog.sh new file mode 100755 index 000000000..8bd7549be --- /dev/null +++ b/contrib/release/extract-changelog.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash +# +# Print a crate's CHANGELOG.md section for a version: the lines between the +# `## ` heading and the next `## `, with blank edges trimmed. Used +# to fill the GitHub release body. +set -euo pipefail +# shellcheck source=contrib/release/crates.sh +source "$(dirname "${BASH_SOURCE[0]}")/crates.sh" + +[ "$#" -eq 2 ] || { + echo "usage: extract-changelog.sh " >&2 + exit 1 +} + +# awk matches the heading exactly (a version has regex-special dots); sed +# drops leading blank lines and the command substitution drops trailing ones. +section="$(awk -v h="## $2" ' + $0 == h { inside = 1; next } + inside && /^## / { exit } + inside +' "$REPO_ROOT/$1/CHANGELOG.md" | sed '/./,$!d')" + +[ -n "$section" ] || { + echo "extract-changelog: no ## $2 section in $1/CHANGELOG.md" >&2 + exit 1 +} +printf '%s\n' "$section" diff --git a/contrib/release/keys/spacebear.asc b/contrib/release/keys/spacebear.asc new file mode 100644 index 000000000..fc996a254 --- /dev/null +++ b/contrib/release/keys/spacebear.asc @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZPisAhYJKwYBBAHaRw8BAQdAG+NC2tjYBYU912vi6XP/OK7Jss3VP0FRzYjl +YXHhocW0HXNwYWNlYmVhciA8Z2l0QHNwYWNlYmVhci5kZXY+iJMEExYKADsWIQRy +SsuPzcTOAjP6wu/IDD31KdZ/FgUCZoRsqgIbAwULCQgHAgIiAgYVCgkICwIEFgID +AQIeBwIXgAAKCRDIDD31KdZ/FskkAP9v3xm+deKTOLyx/NeFS1wGoUGZkQ1s6f5a +DO6Nk+yWVAEA/DO6qOB3qbWrpjKRrxbt2uhLY4V54BsbaoHLvHpvJgK4OARk+KwC +EgorBgEEAZdVAQUBAQdAfNAOveqsjpUOZTA+4gx79bktSTfx8qaUiq9pE5TfXxED +AQgHiHgEGBYKACAWIQRySsuPzcTOAjP6wu/IDD31KdZ/FgUCZPisAgIbDAAKCRDI +DD31KdZ/Fn3VAP9AyiR+iokV5+AAPVUA1eEgfaX0cFNaKTuSX4uxKTDimwD/TLcn +NNJS5ZtPELqjXDIS94U+Le2pX124FeyW/VFGbQk= +=vJCs +-----END PGP PUBLIC KEY BLOCK----- diff --git a/contrib/release/verify-published.sh b/contrib/release/verify-published.sh new file mode 100755 index 000000000..9abc85ac9 --- /dev/null +++ b/contrib/release/verify-published.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# +# After publishing, confirm crates.io reports a checksum matching the +# attested .crate and docs.rs built the docs. Runs after the upload, so a +# mismatch is a loud alert, not a gate. Poll counts and interval are +# overridable via the environment; the defaults suit CI. +set -euo pipefail + +[ "$#" -eq 3 ] || { + echo "usage: verify-published.sh " >&2 + exit 1 +} +crate="$1" +version="$2" +file="$3" +ua="rust-payjoin release verify-published" +interval="${POLL_INTERVAL:-10}" +die() { + echo "verify-published: $*" >&2 + exit 1 +} + +# Poll a URL until its jq filter yields non-empty output; print it, or fail. +poll() { + local attempts="$1" url="$2" filter="$3" out i + for ((i = 0; i < attempts; i++)); do + out="$(curl -sfL -H "User-Agent: $ua" "$url" 2>/dev/null | jq -r "$filter" 2>/dev/null || true)" + [ -n "$out" ] && { + printf '%s' "$out" + return 0 + } + sleep "$interval" + done + return 1 +} + +[ -f "$file" ] || die "no such file: $file" +local_sha="$(sha256sum "$file" | cut -d' ' -f1)" + +echo "Waiting for $crate $version on crates.io (${CRATES_IO_ATTEMPTS:-30} checks, ${interval}s apart)" +published_sha="$(poll "${CRATES_IO_ATTEMPTS:-30}" \ + "https://crates.io/api/v1/crates/$crate/$version" '.version.checksum // empty')" || + die "$crate $version never appeared on crates.io" +[ "$published_sha" = "$local_sha" ] || + die "checksum mismatch: crates.io $published_sha vs local $local_sha" +echo "crates.io checksum matches the attested .crate ($local_sha)" + +echo "Waiting for docs.rs to build $crate $version (${DOCS_RS_ATTEMPTS:-60} checks, ${interval}s apart)" +poll "${DOCS_RS_ATTEMPTS:-60}" \ + "https://docs.rs/crate/$crate/$version/status.json" 'select(.doc_status == true) | "built"' >/dev/null || + die "docs.rs did not build $crate $version" +echo "docs.rs built $crate $version" diff --git a/contrib/release/verify-tag.sh b/contrib/release/verify-tag.sh new file mode 100755 index 000000000..511805422 --- /dev/null +++ b/contrib/release/verify-tag.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# Release gate. Confirms a `-` tag is annotated, signed by a +# key in contrib/release/keys/, an ancestor of origin/master, matches the +# crate's manifest version and the release invariants, and that its sibling +# release-crate dependencies are already on crates.io. +# +# Checks against the working tree, so run it at the tagged commit, which the +# release workflow does. +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=contrib/release/crates.sh +source "$DIR/crates.sh" + +[ "$#" -eq 1 ] || { + echo "usage: verify-tag.sh " >&2 + exit 1 +} +tag="$1" +die() { + echo "verify-tag: $*" >&2 + exit 1 +} + +crate="$(crate_from_tag "$tag")" || die "$tag is not a - release tag" +version="$(version_from_tag "$tag")" + +echo "Verifying $tag as a release of $crate $version" + +echo "Checking the tag is annotated" +[ "$(git -C "$REPO_ROOT" cat-file -t "$tag" 2>/dev/null)" = tag ] || + die "$tag is not an annotated tag" + +echo "Checking the tag is signed by a key in contrib/release/keys/" +# The throwaway keyring holds only trusted keys, so a successful +# verification against it proves the signer is trusted. +home="$(mktemp -d)" +trap 'rm -rf "$home"' EXIT +gpg --homedir "$home" --batch --quiet --import "$REPO_ROOT"/contrib/release/keys/*.asc 2>/dev/null || + die "no importable keys in contrib/release/keys/" +GNUPGHOME="$home" git -C "$REPO_ROOT" verify-tag "$tag" >/dev/null 2>&1 || + die "$tag is not signed by a trusted key" + +echo "Checking the tag is an ancestor of origin/master" +git -C "$REPO_ROOT" merge-base --is-ancestor "$tag" origin/master 2>/dev/null || + die "$tag is not an ancestor of origin/master" + +echo "Checking the $crate manifest version is $version" +RELEASE_METADATA="$(cargo_metadata)" +manifest="$(manifest_version "$crate")" +[ "$manifest" = "$version" ] || die "$crate manifest version is $manifest, tag says $version" + +"$DIR/check-invariants.sh" "$crate" || die "release invariants fail for $crate" + +echo "Checking sibling release-crate dependencies are on crates.io" +while IFS=$'\t' read -r dep req; do + [ -n "$dep" ] || continue + v="${req#^}" + v="${v#=}" + crate_published "$dep" "$v" || die "$dep $v is not on crates.io yet; publish it before $crate" +done < <(sibling_deps "$crate") + +echo "$tag is cleared for release" diff --git a/flake.nix b/flake.nix index 4e8cc538d..8a33ea980 100644 --- a/flake.nix +++ b/flake.nix @@ -452,6 +452,23 @@ inherit buildInputs; src = dummySrc; }; + + # Pinned tooling for the contrib/release scripts, so they run + # identically under `nix develop .#release -c` locally and in CI. + releaseDevShell = pkgs.mkShell { + name = "release"; + packages = with pkgs; [ + rustToolchains.stable + jq + gnupg + curl + git + gnugrep + gnused + gawk + coreutils + ]; + }; in { packages = @@ -462,6 +479,7 @@ }; devShells = devShells // { default = devShells.nightly; + release = releaseDevShell; python = pythonDevShell; javascript = javascriptDevShell; csharp = csharpDevShell; diff --git a/payjoin-test-utils/Cargo.toml b/payjoin-test-utils/Cargo.toml index e980f9eda..dd9b42b2b 100644 --- a/payjoin-test-utils/Cargo.toml +++ b/payjoin-test-utils/Cargo.toml @@ -33,7 +33,7 @@ http = { version = "1.3.1", optional = true } ohttp = { package = "bitcoin-ohttp", version = "0.6.0", optional = true } once_cell = "1.21.3" payjoin = { version = "1.0.0-rc.6", default-features = false } -payjoin-mailroom = { version = "0.1.1", features = ["_manual-tls"] } +payjoin-mailroom = { version = "0.1.2", features = ["_manual-tls"] } tar = "0.4.20" # Pin to 0.14.7 (last version to support our MSRV 1.85) rcgen = { version = "=0.14.7", optional = true }