From 4eb7eb696093370ee62773ce6a3023ec3d680731 Mon Sep 17 00:00:00 2001 From: Benalleng Date: Tue, 22 Sep 2026 12:02:48 -0400 Subject: [PATCH 1/2] Add release helper scripts and trusted keys Co-authored-by: spacebear --- contrib/release/check-bump.sh | 56 +++++++++++++++++++++++++ contrib/release/crates.sh | 64 +++++++++++++++++++++++++++++ contrib/release/keys/benalleng.asc | 61 +++++++++++++++++++++++++++ contrib/release/keys/dangould.asc | 13 ++++++ contrib/release/keys/spacebear.asc | 13 ++++++ contrib/release/verify-published.sh | 56 +++++++++++++++++++++++++ contrib/release/verify-tag.sh | 51 +++++++++++++++++++++++ 7 files changed, 314 insertions(+) create mode 100755 contrib/release/check-bump.sh create mode 100755 contrib/release/crates.sh create mode 100644 contrib/release/keys/benalleng.asc create mode 100644 contrib/release/keys/dangould.asc create mode 100644 contrib/release/keys/spacebear.asc create mode 100755 contrib/release/verify-published.sh create mode 100755 contrib/release/verify-tag.sh diff --git a/contrib/release/check-bump.sh b/contrib/release/check-bump.sh new file mode 100755 index 0000000..1e81032 --- /dev/null +++ b/contrib/release/check-bump.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# +# Pull request check. When the crate version changed relative to the base +# commit, confirm the crate still publishes (cargo publish --dry-run) and +# the bump is large enough for the API changes since the base version +# (cargo semver-checks). No-ops when the version did not change. +set -euo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=contrib/release/crates.sh +source "$DIR/crates.sh" +cd "$REPO_ROOT" + +[ "$#" -eq 2 ] || { + echo "usage: check-bump.sh " >&2 + exit 1 +} +crate="$1" +base="$2" +is_released_crate "$crate" || { + echo "check-bump.sh: $crate is not a released crate ($RELEASED_CRATES)" >&2 + exit 1 +} + +# The [package] version at a git ref (the only line-anchored `version`). +version_at() { + git show "$1:$(crate_manifest "$crate")" 2>/dev/null | + sed -n 's/^version = "\(.*\)"/\1/p' | head -1 +} + +baseline="$(version_at "$base")" +version="$(manifest_version "$crate")" +echo "Comparing $crate version against $base" +if [ "$baseline" = "$version" ]; then + echo "Version did not change ($version)" + exit 0 +fi + +echo "Version changed: $baseline -> $version" + +echo "Dry-run publishing $crate" +cargo publish --dry-run -q -p "$crate" +echo "$crate packages and publishes cleanly" + +# Semver only binds between stable releases: any comparison involving a +# pre-release is classified as a major bump, which permits everything, so +# running the tool there proves nothing. +if is_prerelease "$version" || is_prerelease "$baseline"; then + echo "Skipping semver check for pre-release ($baseline -> $version)" + exit 0 +fi +if ! crate_published "$crate" "$baseline"; then + echo "Skipping semver check; baseline $baseline not on crates.io" + exit 0 +fi +echo "Checking $crate $version API against $baseline" +cargo semver-checks --baseline-version "$baseline" -p "$crate" diff --git a/contrib/release/crates.sh b/contrib/release/crates.sh new file mode 100755 index 0000000..00d0c3e --- /dev/null +++ b/contrib/release/crates.sh @@ -0,0 +1,64 @@ +#!/usr/bin/env bash +# +# Shared helpers for the release scripts. The workspace releases two crates: +# bitcoin-ohttp (member dir ohttp/) and bhttp (bhttp/), each released with +# its own - tag. + +# The crates these scripts release, in publish order. +RELEASED_CRATES="bitcoin-ohttp bhttp" + +REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" + +# Succeed if $1 is a released crate. +is_released_crate() { + case " $RELEASED_CRATES " in + *" $1 "*) ;; + *) return 1 ;; + esac +} + +# Print the workspace-relative manifest path of a released crate. +crate_manifest() { + case "$1" in + bitcoin-ohttp) printf 'ohttp/Cargo.toml' ;; + bhttp) printf 'bhttp/Cargo.toml' ;; + *) return 1 ;; + esac +} + +# Print the crate's manifest version. +manifest_version() { + cargo metadata --no-deps --format-version 1 \ + --manifest-path "$REPO_ROOT/$(crate_manifest "$1")" | + jq -r --arg c "$1" '.packages[] | select(.name == $c) | .version' +} + +# Print the crate a - release tag belongs to, or fail. +crate_from_tag() { + for crate in $RELEASED_CRATES; do + case "$1" in + "$crate"-[0-9]*) printf '%s' "$crate"; return 0 ;; + esac + done + return 1 +} + +# Print the version in a - release tag, or fail. +version_from_tag() { + 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 +} + +# Succeed if of the crate exists on crates.io. +crate_published() { + curl -sfL -o /dev/null -H "User-Agent: ohttp release tooling" \ + "https://crates.io/api/v1/crates/$1/$2" +} diff --git a/contrib/release/keys/benalleng.asc b/contrib/release/keys/benalleng.asc new file mode 100644 index 0000000..92d345c --- /dev/null +++ b/contrib/release/keys/benalleng.asc @@ -0,0 +1,61 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +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 +UMMRQCMlDeFCKn6Zic/y9omML7NB +=xZhb +-----END PGP PUBLIC KEY BLOCK----- diff --git a/contrib/release/keys/dangould.asc b/contrib/release/keys/dangould.asc new file mode 100644 index 0000000..4cebb0f --- /dev/null +++ b/contrib/release/keys/dangould.asc @@ -0,0 +1,13 @@ +-----BEGIN PGP PUBLIC KEY BLOCK----- + +mDMEZZcCPRYJKwYBBAHaRw8BAQdA0XA/rCxWBl0fmT6KZMhW5KuND3xF3Jnt1+Kp +d/U4jma0GERhbiBHb3VsZCA8ZEBuZ291bGQuZGV2PoiTBBMWCgA7FiEEaafonlu1 +mRILLuAkOHEzVgkiD1kFAmWXAj0CGwMFCwkIBwICIgIGFQoJCAsCBBYCAwECHgcC +F4AACgkQOHEzVgkiD1mp3wD/fc2bsdC6BuTiLfdcOSkgUX3IlO97GL998sP5cjJT +DkIA/3RXFhz1RxR7ow4SHc4/acG1UB5PMixVD6UQ6W703MMIuDgEZZcCPRIKKwYB +BAGXVQEFAQEHQD1ECC7I0/zHh7JqFwu8TKJSjo3VhdSavTy5TgmGu9lrAwEIB4h4 +BBgWCgAgFiEEaafonlu1mRILLuAkOHEzVgkiD1kFAmWXAj0CGwwACgkQOHEzVgki +D1kLfAD9EqNGoCVj05oXU5B2pT2sCQ9jKOy12DKvzQ3Qln1C2QQBAM0gTG6lfSIF +kTHp2utk9+Qlowc+gfiDtBmxocg+ihgP +=Pwkf +-----END PGP PUBLIC KEY BLOCK----- diff --git a/contrib/release/keys/spacebear.asc b/contrib/release/keys/spacebear.asc new file mode 100644 index 0000000..fc996a2 --- /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 0000000..56c5b3a --- /dev/null +++ b/contrib/release/verify-published.sh @@ -0,0 +1,56 @@ +#!/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 +# shellcheck source=contrib/release/crates.sh +source "$(dirname "${BASH_SOURCE[0]}")/crates.sh" + +[ "$#" -eq 3 ] || { + echo "usage: verify-published.sh " >&2 + exit 1 +} +crate="$1" +version="$2" +file="$3" +ua="ohttp release verify-published" +interval="${POLL_INTERVAL:-10}" +die() { + echo "verify-published: $*" >&2 + exit 1 +} + +is_released_crate "$crate" || die "$crate is not a released crate ($RELEASED_CRATES)" + +# 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 0000000..5d51126 --- /dev/null +++ b/contrib/release/verify-tag.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# +# Release gate. Confirms a - tag is annotated, signed by a +# key in contrib/release/keys/, an ancestor of origin/main, and matches the +# crate's manifest version. +# +# 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/main" +git -C "$REPO_ROOT" merge-base --is-ancestor "$tag" origin/main 2>/dev/null || + die "$tag is not an ancestor of origin/main" + +echo "Checking the $crate manifest version is $version" +manifest="$(manifest_version "$crate")" +[ "$manifest" = "$version" ] || die "$crate manifest version is $manifest, tag says $version" + +echo "$tag is cleared for release" From 0ce7a0fa6af38be60e6dfdf31b821c694ac1e559 Mon Sep 17 00:00:00 2001 From: Benalleng Date: Tue, 22 Sep 2026 12:06:06 -0400 Subject: [PATCH 2/2] Add crates.io trusted publishing workflow Co-authored-by: spacebear --- .github/workflows/crates-release.yml | 185 +++++++++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 .github/workflows/crates-release.yml diff --git a/.github/workflows/crates-release.yml b/.github/workflows/crates-release.yml new file mode 100644 index 0000000..b27ff3c --- /dev/null +++ b/.github/workflows/crates-release.yml @@ -0,0 +1,185 @@ +name: Release + +# A pull request that bumps a released crate's version is checked for +# consistency and publishability. Pushing a - tag verifies +# the tag, waits for CI at that commit, then publishes the crate to +# crates.io through the `release` environment (required reviewer) with +# keyless OIDC, cuts the GitHub release, and confirms the upload. + +on: + pull_request: + paths: + - ohttp/Cargo.toml + push: + tags: + - 'bitcoin-ohttp-[0-9]*' + +permissions: + contents: read + +jobs: + check-bump: + name: Check ${{ matrix.crate }} version bump + if: github.event_name == 'pull_request' + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + matrix: + crate: [bitcoin-ohttp, bhttp] + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Use cache + uses: Swatinem/rust-cache@v2 + - name: Install cargo-semver-checks + uses: taiki-e/install-action@v2 + with: + tool: cargo-semver-checks + - name: Check the bump is consistent and publishable + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + run: ./contrib/release/check-bump.sh "${{ matrix.crate }}" "$BASE_SHA" + + verify-tag: + name: Verify release tag + if: github.event_name == 'push' + runs-on: ubuntu-24.04 + outputs: + crate: ${{ steps.meta.outputs.crate }} + version: ${{ steps.meta.outputs.version }} + prerelease: ${{ steps.meta.outputs.prerelease }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + with: + fetch-depth: 0 + - name: Fetch main + run: git fetch --no-tags origin +refs/heads/main:refs/remotes/origin/main + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Compute tag metadata + id: meta + env: + TAG: ${{ github.ref_name }} + run: | + source contrib/release/crates.sh + crate="$(crate_from_tag "$TAG")" + version="$(version_from_tag "$TAG")" + { + echo "crate=$crate" + echo "version=$version" + is_prerelease "$version" && echo "prerelease=true" || echo "prerelease=false" + } >>"$GITHUB_OUTPUT" + - name: Verify tag + env: + TAG: ${{ github.ref_name }} + run: ./contrib/release/verify-tag.sh "$TAG" + + wait-for-ci: + name: Wait for CI + if: github.event_name == 'push' + needs: verify-tag + runs-on: ubuntu-24.04 + permissions: + checks: read + contents: read + steps: + # Wait only for the correctness checks (check.yml) at the tagged + # commit. Scoping by regexp avoids waiting on this workflow's own + # downstream publish jobs, which would deadlock. + - name: Wait for CI checks + uses: lewagon/wait-on-check-action@v1.9.0 + with: + ref: ${{ github.sha }} + check-regexp: ^Continuous + 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-24.04 + environment: 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@v4 + - name: Install toolchain + uses: dtolnay/rust-toolchain@stable + - name: Use cache + uses: Swatinem/rust-cache@v2 + - name: Package the crate + run: cargo package -p "$CRATE" + - name: Attest build provenance + uses: actions/attest-build-provenance@v2 + with: + subject-path: target/package/${{ env.CRATE }}-${{ env.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 -p "$CRATE" + - name: Upload packaged crate + uses: actions/upload-artifact@v4 + with: + name: crate + path: target/package/${{ env.CRATE }}-${{ env.VERSION }}.crate + + github-release: + name: Create GitHub release + if: github.event_name == 'push' + needs: [verify-tag, publish] + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - name: Download packaged crate + uses: actions/download-artifact@v4 + with: + name: crate + path: dist + - name: Generate SHA256SUMS + run: (cd dist && sha256sum ./*.crate >SHA256SUMS) + - name: Create release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.ref_name }} + name: Release ${{ github.ref_name }} + 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-24.04 + env: + CRATE: ${{ needs.verify-tag.outputs.crate }} + VERSION: ${{ needs.verify-tag.outputs.version }} + steps: + - name: Checkout repo + uses: actions/checkout@v4 + - name: Download packaged crate + uses: actions/download-artifact@v4 + with: + name: crate + path: dist + - name: Verify crates.io and docs.rs + run: ./contrib/release/verify-published.sh "$CRATE" "$VERSION" "dist/$CRATE-$VERSION.crate"