sendmer 0.5.1

A cli tool to send directories over the network, with NAT hole punching
Documentation
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"