cruzbit 1.2.0

A simple decentralized peer-to-peer ledger implementation
Documentation
name: Release

on:
  push:
    tags: ["v*.*.*"]
  workflow_dispatch: {}

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: true

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  tag-check:
    name: Tag Check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false

      - name: Validate tag matches package version
        if: github.event_name == 'push'
        env:
          TAG_NAME: ${{ github.ref_name }}
        run: |
          python3 - <<'PY'
          import os
          import pathlib
          import tomllib

          tag_name = os.environ["TAG_NAME"]
          if not tag_name.startswith("v"):
            raise SystemExit(f"expected v-prefixed tag, got {tag_name}")

          tag_version = tag_name[1:]
          cargo = tomllib.loads(pathlib.Path("Cargo.toml").read_text())
          package_version = cargo["package"]["version"]
          if tag_version != package_version:
            raise SystemExit(
                f"tag version {tag_version} does not match package version {package_version}"
            )
          PY

      - name: Skip tag validation for manual run
        if: github.event_name != 'push'
        run: echo "workflow_dispatch run; skipping version check"

  build:
    needs: tag-check
    name: Build (${{ matrix.target }}${{ matrix.feature && format('-{0}', matrix.feature) || '' }})
    runs-on: ${{ matrix.runner }}
    strategy:
      fail-fast: false
      matrix:
        include:
          # CPU builds
          - target: aarch64-apple-darwin
            runner: macos-14
          - target: x86_64-apple-darwin
            runner: macos-15-intel
          - target: aarch64-unknown-linux-gnu
            runner: ubuntu-24.04-arm
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-latest
          - target: x86_64-pc-windows-msvc
            runner: windows-latest

          # CUDA builds
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-latest
            feature: cuda
          - target: x86_64-pc-windows-msvc
            runner: windows-latest
            feature: cuda

          # OpenCL builds
          - target: x86_64-unknown-linux-gnu
            runner: ubuntu-latest
            feature: opencl
          - target: aarch64-unknown-linux-gnu
            runner: ubuntu-24.04-arm
            feature: opencl
          - target: x86_64-pc-windows-msvc
            runner: windows-latest
            feature: opencl
          - target: aarch64-apple-darwin
            runner: macos-14
            feature: opencl
          - target: x86_64-apple-darwin
            runner: macos-15-intel
            feature: opencl

    env:
      TAG_NAME: ${{ github.ref_name }}
      TARGET: ${{ matrix.target }}
      FEATURE: ${{ matrix.feature }}

    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false

      - uses: dtolnay/rust-toolchain@3c5f7ea28cd621ae0bf5283f0e981fb97b8a7af9
        with:
          toolchain: stable
          targets: ${{ matrix.target }}

      - uses: Swatinem/rust-cache@bc2d2e71bd35c5549942babaa51a89c586b981d1 # v2.8.1
        with:
          key: release-${{ matrix.target }}${{ matrix.feature && format('-{0}', matrix.feature) || '' }}

      - name: Install CUDA toolkit
        if: matrix.feature == 'cuda'
        uses: Jimver/cuda-toolkit@4bd727d5619dc6fa323b1e76c3aa5dca94f5ec6d # v0.2.21
        with:
          cuda: '12.5.0'
          method: 'network'

      - name: Install OpenCL (Linux)
        if: matrix.feature == 'opencl' && runner.os == 'Linux'
        run: |
          sudo apt-get update
          sudo apt-get install -y --no-install-recommends ocl-icd-opencl-dev opencl-headers

      - name: Install OpenCL (Windows)
        if: matrix.feature == 'opencl' && runner.os == 'Windows'
        shell: pwsh
        run: |
          vcpkg install opencl:x64-windows
          "CMAKE_TOOLCHAIN_FILE=$env:VCPKG_INSTALLATION_ROOT/scripts/buildsystems/vcpkg.cmake" `
            | Out-File -FilePath $env:GITHUB_ENV -Append

      - name: Build
        shell: bash
        run: |
          set -euo pipefail
          features=""
          if [ -n "$FEATURE" ]; then
            features="--features $FEATURE"
          fi
          cargo build --release --locked --target "$TARGET" --bins $features

      - name: Package binaries (Unix)
        if: runner.os != 'Windows'
        shell: bash
        run: |
          set -euo pipefail
          suffix=""
          if [ -n "$FEATURE" ]; then
            suffix="-$FEATURE"
          fi
          name="cruzbit-${TAG_NAME}-${TARGET}${suffix}"
          mkdir "$name"
          cp "target/$TARGET/release/client" "$name/client"
          cp "target/$TARGET/release/wallet" "$name/wallet"
          cp README.md LICENSE "$name/"
          chmod +x "$name/client" "$name/wallet"
          tar -czf "$name.tar.gz" "$name"
          echo "ASSET=$name.tar.gz" >> "$GITHUB_ENV"

      - name: Package binaries (Windows)
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          $suffix = ""
          if ($env:FEATURE) {
            $suffix = "-$env:FEATURE"
          }
          $name = "cruzbit-$env:TAG_NAME-$env:TARGET$suffix"
          New-Item -ItemType Directory -Path $name | Out-Null
          Copy-Item "target/$env:TARGET/release/client.exe" "$name/client.exe"
          Copy-Item "target/$env:TARGET/release/wallet.exe" "$name/wallet.exe"
          Copy-Item README.md, LICENSE $name
          Compress-Archive -Path $name -DestinationPath "$name.zip"
          "ASSET=$name.zip" | Out-File -FilePath $env:GITHUB_ENV -Append

      - uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
        with:
          name: dist-${{ matrix.target }}${{ matrix.feature && format('-{0}', matrix.feature) || '' }}
          path: ${{ env.ASSET }}

  release:
    name: Release
    needs: build
    if: github.event_name == 'push'
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
        with:
          persist-credentials: false

      - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
        with:
          path: dist
          pattern: dist-*
          merge-multiple: true

      - name: Generate checksums
        working-directory: dist
        run: sha256sum * > SHA256SUMS.txt

      - name: Extract release notes from changelog
        env:
          TAG_NAME: ${{ github.ref_name }}
        run: |
          python3 - <<'PY'
          import os
          import pathlib
          import re

          tag_name = os.environ["TAG_NAME"]
          version = tag_name[1:] if tag_name.startswith("v") else tag_name
          changelog = pathlib.Path("CHANGELOG.md").read_text()
          pattern = re.compile(rf"^## \[{re.escape(version)}\]\s+-\s+.+$")
          lines = changelog.splitlines()
          capture = []
          inside = False
          for line in lines:
            if line.startswith("## ["):
              if inside:
                break
              if pattern.match(line):
                inside = True
            if inside:
              capture.append(line)
          notes = "\n".join(capture).strip()
          if not notes:
            raise SystemExit(f"missing CHANGELOG entry for {version}")
          pathlib.Path("RELEASE_NOTES.md").write_text(notes + "\n")
          PY

      - name: Create release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG_NAME: ${{ github.ref_name }}
        run: |
          gh release create "$TAG_NAME" \
            --title "$TAG_NAME" \
            --notes-file RELEASE_NOTES.md \
            dist/*