name: release
on:
push:
tags:
- "v*"
workflow_dispatch:
inputs:
release_version:
description: "Release version (e.g. v1.2.3)"
required: true
create_release:
description: "Create GitHub Release"
required: true
default: "true"
upload_artifacts:
description: "Upload build artifacts"
required: true
default: "true"
publish_crate:
description: "Publish crate to crates.io"
required: true
default: "true"
env:
BIN_NAME: sendmer
IROH_FORCE_STAGING_RELAYS: "1"
jobs:
release-context:
name: Resolve Release Tag
runs-on: ubuntu-latest
outputs:
release_version: ${{ steps.version.outputs.version }}
steps:
- name: Determine release version
id: version
env:
RELEASE_VERSION_INPUT: ${{ github.event.inputs.release_version }}
shell: bash
run: |
if [ "${GITHUB_EVENT_NAME}" = "push" ]; then
VERSION="${GITHUB_REF#refs/tags/}"
else
VERSION="${RELEASE_VERSION_INPUT}"
fi
if [ -z "$VERSION" ]; then
echo "Release version is empty"
exit 1
fi
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then
echo "Release version must be a semver tag such as v1.2.3: $VERSION"
exit 1
fi
printf 'version=%s\n' "$VERSION" >> "$GITHUB_OUTPUT"
echo "Release version: $VERSION"
quality-gate:
name: Release Quality Gate
needs: release-context
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.release-context.outputs.release_version }}
- name: Verify checked out release tag
env:
RELEASE_VERSION: ${{ needs.release-context.outputs.release_version }}
shell: bash
run: |
TAG_REF="refs/tags/${RELEASE_VERSION}"
TAG_COMMIT="$(git rev-parse "${TAG_REF}^{commit}")"
HEAD_COMMIT="$(git rev-parse HEAD)"
if [ "${TAG_COMMIT}" != "${HEAD_COMMIT}" ]; then
echo "Checked out commit ${HEAD_COMMIT} does not match ${RELEASE_VERSION} (${TAG_COMMIT})"
exit 1
fi
echo "Release tag verified: ${RELEASE_VERSION} (${TAG_COMMIT})"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Cargo fmt
run: cargo fmt --all -- --check
- name: Cargo clippy
run: cargo clippy --locked --workspace --all-targets --all-features -- -D warnings
- name: Cargo check
run: cargo check --workspace --all-features --bins
- name: Cargo package
run: cargo package --locked --no-verify
- name: Cargo test
run: cargo test --locked --workspace --all-features --bins --tests --examples
create-release:
name: Create GitHub Release
needs: [release-context, quality-gate]
runs-on: ubuntu-latest
outputs:
release_version: ${{ needs.release-context.outputs.release_version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.release-context.outputs.release_version }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Verify release version matches Cargo package
env:
RELEASE_VERSION: ${{ needs.release-context.outputs.release_version }}
shell: bash
run: |
CRATE_VERSION="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; data = json.loads(sys.stdin.buffer.read().decode("utf-8-sig")); print(next(package["version"] for package in data["packages"] if package["name"] == "sendmer"))')"
EXPECTED_VERSION="v${CRATE_VERSION}"
if [[ "${RELEASE_VERSION}" != v* ]]; then
echo "Release version must start with v: ${RELEASE_VERSION}"
exit 1
fi
if [[ "${RELEASE_VERSION}" != "${EXPECTED_VERSION}" ]]; then
echo "Release version ${RELEASE_VERSION} does not match Cargo package ${EXPECTED_VERSION}"
exit 1
fi
echo "Release version verified: ${RELEASE_VERSION}"
- name: Ensure GitHub release exists
if: github.event_name == 'push' || github.event.inputs.create_release == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ needs.release-context.outputs.release_version }}
run: |
VERSION="$RELEASE_VERSION"
if gh release view "$VERSION" >/dev/null 2>&1; then
echo "Release $VERSION already exists, skip create."
else
gh release create "$VERSION" \
--verify-tag \
--title "$VERSION" \
--generate-notes
fi
build-release:
name: Build Release Binaries
needs: create-release
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-musl
- os: ubuntu-latest
target: aarch64-unknown-linux-musl
use_cross: true
- os: macos-latest
target: x86_64-apple-darwin
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-latest
target: x86_64-pc-windows-msvc
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ needs.create-release.outputs.release_version }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.target }}
- name: Install cross (Linux ARM64)
if: matrix.use_cross == true
run: cargo install cross
- name: Install musl tools
if: runner.os == 'Linux' && contains(matrix.target, 'musl') && matrix.use_cross != true
run: sudo apt-get update && sudo apt-get install -y musl-tools
- name: Build binary
shell: bash
run: |
if [ "${{ matrix.use_cross }}" = "true" ]; then
cross build --locked --release --target ${{ matrix.target }}
else
cargo build --locked --release --target ${{ matrix.target }}
fi
- name: Package artifact
id: package
env:
BIN_NAME: ${{ env.BIN_NAME }}
RELEASE_VERSION: ${{ needs.create-release.outputs.release_version }}
RUST_TARGET: ${{ matrix.target }}
shell: bash
run: |
VERSION="$RELEASE_VERSION"
NAME="${BIN_NAME}-${VERSION}-${RUST_TARGET}"
SOURCE_COMMIT="$(git rev-parse HEAD)"
mkdir "$NAME"
printf 'sendmer_version=%s\nrust_target=%s\nsource_commit=%s\n' \
"$VERSION" "$RUST_TARGET" "$SOURCE_COMMIT" > "$NAME/BUILD-METADATA.txt"
test -s "$NAME/BUILD-METADATA.txt"
grep -Fqx "sendmer_version=$VERSION" "$NAME/BUILD-METADATA.txt"
grep -Fqx "rust_target=$RUST_TARGET" "$NAME/BUILD-METADATA.txt"
grep -Fqx "source_commit=$SOURCE_COMMIT" "$NAME/BUILD-METADATA.txt"
if [ "${{ runner.os }}" = "Windows" ]; then
cp "target/${RUST_TARGET}/release/${BIN_NAME}.exe" "$NAME/"
test -s "$NAME/${BIN_NAME}.exe"
powershell Compress-Archive "$NAME/*" "$NAME.zip"
ASSET="$NAME.zip"
else
cp "target/${RUST_TARGET}/release/${BIN_NAME}" "$NAME/"
test -x "$NAME/${BIN_NAME}"
tar czf "$NAME.tar.gz" -C "$NAME" .
ASSET="$NAME.tar.gz"
fi
test -s "$ASSET"
CHECKSUM="${ASSET}.sha256"
if [ "${{ runner.os }}" = "Windows" ]; then
HASH="$(powershell -NoProfile -Command "(Get-FileHash -Algorithm SHA256 -LiteralPath '${ASSET}').Hash.ToLowerInvariant()")"
printf '%s %s\n' "$HASH" "$(basename "$ASSET")" > "$CHECKSUM"
elif [ "${{ runner.os }}" = "macOS" ]; then
shasum -a 256 "$ASSET" > "$CHECKSUM"
else
sha256sum "$ASSET" > "$CHECKSUM"
fi
test -s "$CHECKSUM"
CHECKSUM_HASH="$(awk '{ print $1 }' "$CHECKSUM")"
CHECKSUM_ASSET="$(awk '{ print $2 }' "$CHECKSUM")"
[[ "$CHECKSUM_HASH" =~ ^[0-9a-fA-F]{64}$ ]]
test "$CHECKSUM_ASSET" = "$(basename "$ASSET")"
echo "asset=$ASSET" >> "$GITHUB_OUTPUT"
echo "checksum=$CHECKSUM" >> "$GITHUB_OUTPUT"
- name: Upload release asset
if: github.event_name == 'push' || github.event.inputs.upload_artifacts == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_VERSION: ${{ needs.create-release.outputs.release_version }}
RELEASE_ASSET: ${{ steps.package.outputs.asset }}
RELEASE_CHECKSUM: ${{ steps.package.outputs.checksum }}
shell: bash
run: |
gh release upload "$RELEASE_VERSION" "$RELEASE_ASSET" "$RELEASE_CHECKSUM" --clobber
publish-crate:
name: Publish crate to crates.io
needs: [quality-gate, create-release, build-release]
runs-on: ubuntu-latest
if: github.event_name == 'push' || github.event.inputs.publish_crate == 'true'
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ needs.create-release.outputs.release_version }}
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Publish crate if version is missing
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
CRATE_NAME: ${{ env.BIN_NAME }}
shell: bash
run: |
CRATE_VERSION="$(cargo metadata --no-deps --format-version 1 | python3 -c 'import json, sys; data = json.loads(sys.stdin.buffer.read().decode("utf-8-sig")); print(next(package["version"] for package in data["packages"] if package["name"] == "sendmer"))')"
API_URL="https://crates.io/api/v1/crates/${CRATE_NAME}/${CRATE_VERSION}"
HTTP_STATUS="$(curl --silent --show-error --output /dev/null --write-out '%{http_code}' \
-H 'User-Agent: sendmer-release-workflow' "$API_URL")"
case "$HTTP_STATUS" in
200)
echo "${CRATE_NAME} ${CRATE_VERSION} already exists on crates.io, skip publish."
;;
404)
cargo publish --locked
;;
*)
echo "crates.io lookup for ${CRATE_NAME} ${CRATE_VERSION} returned HTTP ${HTTP_STATUS}"
exit 1
;;
esac
release-summary:
name: Release Run Summary
if: ${{ !cancelled() }}
needs: [release-context, quality-gate, create-release, build-release, publish-crate]
runs-on: ubuntu-latest
steps:
- name: Write job results to summary
env:
RELEASE_VERSION: ${{ needs.release-context.outputs.release_version }}
CONTEXT_RESULT: ${{ needs.release-context.result }}
QUALITY_RESULT: ${{ needs.quality-gate.result }}
CREATE_RESULT: ${{ needs.create-release.result }}
BUILD_RESULT: ${{ needs.build-release.result }}
PUBLISH_RESULT: ${{ needs.publish-crate.result }}
shell: bash
run: |
{
echo "## Release workflow summary"
echo "- tag: \`${RELEASE_VERSION:-unknown}\`"
echo "- run: ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
echo ""
echo "| job | result |"
echo "| --- | --- |"
echo "| release-context | ${CONTEXT_RESULT} |"
echo "| quality-gate | ${QUALITY_RESULT} |"
echo "| create-release | ${CREATE_RESULT} |"
echo "| build-release | ${BUILD_RESULT} |"
echo "| publish-crate | ${PUBLISH_RESULT} |"
} >> "$GITHUB_STEP_SUMMARY"