bssh 2.0.1

Parallel SSH command execution tool for cluster management
Documentation
name: Release

on:
  release:
    types: [published, prereleased]
  workflow_dispatch:
    inputs:
      update_homebrew:
        description: 'Update Homebrew formula after build'
        required: false
        default: 'false'
        type: choice
        options: ['true','false']
      release_tag:
        description: 'Release tag to upload artifacts to (e.g. v1.2.3)'
        required: false

permissions:
  contents: write

jobs:
  build:
    name: Build ${{ matrix.target }}
    runs-on: ${{ matrix.os }}
    environment: packaging

    strategy:
      fail-fast: false
      matrix:
        include:
          # Linux x86_64 (glibc)
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-22.04
            artifact_name: bssh
            asset_name: bssh-linux-x86_64
            archive_ext: ".tar.gz"

          # Linux x86_64 (musl - static)
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            artifact_name: bssh
            asset_name: bssh-linux-x86_64-musl
            archive_ext: ".tar.gz"

          # Linux ARM64 (glibc)
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-22.04-arm
            artifact_name: bssh
            asset_name: bssh-linux-aarch64
            archive_ext: ".tar.gz"

          # Linux ARM64 (musl - static)
          - target: aarch64-unknown-linux-musl
            os: ubuntu-24.04-arm
            artifact_name: bssh
            asset_name: bssh-linux-aarch64-musl
            archive_ext: ".tar.gz"

          # macOS ARM64
          - target: aarch64-apple-darwin
            os: macos-14
            artifact_name: bssh
            asset_name: bssh-macos-aarch64
            archive_ext: ".zip"

    env:
      BIN_NAME: bssh
      BUNDLE_ID: ${{ vars.BUNDLE_ID }}

    steps:
      # 1) Checkout repository
      - name: Checkout code
        uses: actions/checkout@v4

      # 2) Cache Cargo build artifacts
      - name: Cache cargo
        uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
          restore-keys: |
            ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
            ${{ runner.os }}-cargo-${{ matrix.target }}-

      # 3) Install Rust toolchain
      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      # 4) Install musl tools only for aarch64 musl builds
      - name: Install musl tools (Linux musl only)
        if: contains(matrix.target, 'musl')
        run: |
          sudo apt update
          sudo apt install -y musl-tools

      # 5) Build release binaries (bssh, bssh-server, and bssh-keygen)
      - name: Build release binaries
        run: cargo build --release --target ${{ matrix.target }} --locked --bin bssh --bin bssh-server --bin bssh-keygen

      # 6) macOS code signing
      - name: Import Distribution certificate
        if: runner.os == 'macOS'
        uses: apple-actions/import-codesign-certs@v3
        with:
          p12-file-base64: ${{ secrets.DEV_ID_CERT_P12 }}
          p12-password: ${{ secrets.DEV_ID_CERT_PASSWORD }}

      - name: Code sign macOS binaries
        if: runner.os == 'macOS'
        run: |
          BIN_DIR=target/${{ matrix.target }}/release
          for bin in bssh bssh-server bssh-keygen; do
            codesign --force --timestamp --options runtime \
                     --sign "Distribution" "$BIN_DIR/$bin"
          done

      # 7) Package binaries (separate packages for bssh, bssh-server, and bssh-keygen)
      - name: Package Linux binaries (tar.gz)
        if: runner.os == 'Linux'
        run: |
          BIN_DIR=target/${{ matrix.target }}/release
          ASSET_BASE="${{ matrix.asset_name }}"
          SERVER_ASSET_BASE="${ASSET_BASE/bssh/bssh-server}"
          KEYGEN_ASSET_BASE="${ASSET_BASE/bssh/bssh-keygen}"

          # Package bssh
          mkdir -p package-bssh
          cp "$BIN_DIR/bssh" package-bssh/
          cp docs/man/bssh.1 package-bssh/
          tar -C package-bssh -czf "${ASSET_BASE}.tar.gz" .

          # Package bssh-server
          mkdir -p package-bssh-server
          cp "$BIN_DIR/bssh-server" package-bssh-server/
          cp docs/man/bssh-server.8 package-bssh-server/
          tar -C package-bssh-server -czf "${SERVER_ASSET_BASE}.tar.gz" .

          # Package bssh-keygen
          mkdir -p package-bssh-keygen
          cp "$BIN_DIR/bssh-keygen" package-bssh-keygen/
          cp docs/man/bssh-keygen.1 package-bssh-keygen/
          tar -C package-bssh-keygen -czf "${KEYGEN_ASSET_BASE}.tar.gz" .

      - name: Package macOS binaries (zip)
        if: runner.os == 'macOS'
        run: |
          BIN_DIR="target/${{ matrix.target }}/release"
          ASSET_BASE="${{ matrix.asset_name }}"
          SERVER_ASSET_BASE="${ASSET_BASE/bssh/bssh-server}"
          KEYGEN_ASSET_BASE="${ASSET_BASE/bssh/bssh-keygen}"

          # Package bssh
          mkdir -p package-bssh
          cp "$BIN_DIR/bssh" package-bssh/
          cp docs/man/bssh.1 package-bssh/
          ditto -c -k --sequesterRsrc package-bssh "${ASSET_BASE}.zip"

          # Package bssh-server
          mkdir -p package-bssh-server
          cp "$BIN_DIR/bssh-server" package-bssh-server/
          cp docs/man/bssh-server.8 package-bssh-server/
          ditto -c -k --sequesterRsrc package-bssh-server "${SERVER_ASSET_BASE}.zip"

          # Package bssh-keygen
          mkdir -p package-bssh-keygen
          cp "$BIN_DIR/bssh-keygen" package-bssh-keygen/
          cp docs/man/bssh-keygen.1 package-bssh-keygen/
          ditto -c -k --sequesterRsrc package-bssh-keygen "${KEYGEN_ASSET_BASE}.zip"

      # 8) Generate checksums
      - name: Generate checksums
        run: |
          ASSET_BASE="${{ matrix.asset_name }}"
          SERVER_ASSET_BASE="${ASSET_BASE/bssh/bssh-server}"
          KEYGEN_ASSET_BASE="${ASSET_BASE/bssh/bssh-keygen}"
          EXT="${{ matrix.archive_ext }}"

          for file in "${ASSET_BASE}${EXT}" "${SERVER_ASSET_BASE}${EXT}" "${KEYGEN_ASSET_BASE}${EXT}"; do
            if [[ "$RUNNER_OS" == "Linux" ]]; then
              sha256sum "$file" > "$file.sha256"
            else
              shasum -a 256 "$file" > "$file.sha256"
            fi
          done

      # 9) Upload release artifacts and checksums
      - name: Upload release artifacts
        if: github.event_name == 'release' || github.event_name == 'workflow_dispatch'
        run: |
          ASSET_BASE="${{ matrix.asset_name }}"
          SERVER_ASSET_BASE="${ASSET_BASE/bssh/bssh-server}"
          KEYGEN_ASSET_BASE="${ASSET_BASE/bssh/bssh-keygen}"
          EXT="${{ matrix.archive_ext }}"
          TAG="${{ github.event.release.tag_name || github.event.inputs.release_tag }}"

          gh release upload "$TAG" \
            "${ASSET_BASE}${EXT}" \
            "${ASSET_BASE}${EXT}.sha256" \
            "${SERVER_ASSET_BASE}${EXT}" \
            "${SERVER_ASSET_BASE}${EXT}.sha256" \
            "${KEYGEN_ASSET_BASE}${EXT}" \
            "${KEYGEN_ASSET_BASE}${EXT}.sha256" \
            --clobber
        env:
          GH_TOKEN: ${{ github.token }}

  # ============================================================================
  # Publish pre-release as official release (after all builds complete)
  # ============================================================================
  publish-release:
    name: Publish pre-release as official
    needs: [build]
    if: github.event_name == 'release' && github.event.release.prerelease
    runs-on: ubuntu-latest
    permissions:
      contents: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Convert pre-release to official release
        run: |
          TAG="${{ github.event.release.tag_name }}"
          gh release edit "$TAG" --prerelease=false
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  # ============================================================================
  # Microsoft Teams release notification (Power Automate Workflows webhook)
  # ============================================================================
  notify-teams:
    name: Notify Teams on release
    needs: [build]
    if: github.event_name == 'release'
    runs-on: ubuntu-latest
    permissions: {}

    steps:
      - name: Build Adaptive Card payload
        env:
          TAG:  ${{ github.event.release.tag_name }}
          NAME: ${{ github.event.release.name }}
          URL:  ${{ github.event.release.html_url }}
          BODY: ${{ github.event.release.body }}
          REPO: ${{ github.repository }}
        run: |
          TRIMMED=$(printf '%s' "$BODY" | head -c 2000)
          jq -n \
            --arg tag "$TAG" --arg name "$NAME" \
            --arg url "$URL" --arg body "$TRIMMED" --arg repo "$REPO" '
          {
            type: "message",
            attachments: [{
              contentType: "application/vnd.microsoft.card.adaptive",
              content: {
                "$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
                type: "AdaptiveCard",
                version: "1.5",
                body: [
                  { type: "TextBlock", size: "Large", weight: "Bolder",
                    text: ("🚀 " + $repo + " " + $tag + " released") },
                  { type: "TextBlock", text: $name, wrap: true, isSubtle: true },
                  { type: "TextBlock", text: $body, wrap: true }
                ],
                actions: [
                  { type: "Action.OpenUrl", title: "View release", url: $url }
                ]
              }
            }]
          }' > card.json

      - name: POST to Teams workflow
        if: env.WEBHOOK_URL != ''
        env:
          WEBHOOK_URL: ${{ secrets.TEAMS_RELEASE_NOTIFICATION_WORKFLOW_URL }}
        run: |
          curl -sSf -X POST \
            -H "Content-Type: application/json" \
            --data-binary @card.json \
            "$WEBHOOK_URL"