via-cli 0.2.0

Run commands and API requests with 1Password-backed credentials without exposing secrets to your shell
Documentation
name: Build Binaries

on:
  pull_request:
  push:
    branches:
      - master
  workflow_dispatch:
    inputs:
      release_tag:
        description: "Optional v* tag to build and attach to its GitHub Release"
        required: false
        type: string

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    name: ${{ matrix.name }}
    runs-on: ${{ matrix.os }}
    permissions:
      contents: write

    strategy:
      fail-fast: false
      matrix:
        include:
          - name: Linux x86_64
            os: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
            archive: via-linux-x86_64.tar.gz
          - name: Linux arm64
            os: ubuntu-24.04-arm
            target: aarch64-unknown-linux-gnu
            archive: via-linux-arm64.tar.gz
          - name: macOS x86_64
            os: macos-15-intel
            target: x86_64-apple-darwin
            archive: via-macos-x86_64.tar.gz
          - name: macOS arm64
            os: macos-15
            target: aarch64-apple-darwin
            archive: via-macos-arm64.tar.gz
          - name: Windows x86_64
            os: windows-2025
            target: x86_64-pc-windows-msvc
            archive: via-windows-x86_64.zip
          - name: Windows arm64
            os: windows-11-arm
            target: aarch64-pc-windows-msvc
            archive: via-windows-arm64.zip

    steps:
      - name: Validate release tag
        if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag != '' }}
        shell: bash
        env:
          RELEASE_TAG: ${{ inputs.release_tag }}
        run: |
          case "$RELEASE_TAG" in
            v*) ;;
            *)
              echo "::error::release_tag must start with v"
              exit 1
              ;;
          esac

      - name: Checkout
        if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_tag == '' }}
        uses: actions/checkout@v4

      - name: Checkout release tag
        if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag != '' }}
        uses: actions/checkout@v4
        with:
          ref: ${{ inputs.release_tag }}

      - name: Install Rust
        run: |
          rustup toolchain install stable --profile minimal
          rustup default stable
          rustup target add ${{ matrix.target }}

      - name: Validate crate version matches tag
        if: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') }}
        shell: bash
        env:
          RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}
        run: |
          case "$RELEASE_TAG" in
            v*) ;;
            *)
              echo "::error::release_tag must start with v"
              exit 1
              ;;
          esac

          CRATE_VERSION="${RELEASE_TAG#v}"
          case "$CRATE_VERSION" in
            ''|*[!0-9A-Za-z.+-]*)
              echo "::error::release version may only contain ASCII letters, digits, '.', '+', or '-'"
              exit 1
              ;;
            [0-9]*) ;;
            *)
              echo "::error::release version must start with a digit"
              exit 1
              ;;
          esac

          MANIFEST_VERSION="$(cargo pkgid --locked -p via-cli)"
          MANIFEST_VERSION="${MANIFEST_VERSION##*@}"
          if [ "$MANIFEST_VERSION" != "$CRATE_VERSION" ]; then
            echo "::error::tag $RELEASE_TAG requires via-cli $CRATE_VERSION, but Cargo.toml/Cargo.lock resolve to $MANIFEST_VERSION"
            echo "::error::Run the Prepare Release workflow, merge its version-bump PR, then tag the merge commit."
            exit 1
          fi

      - name: Build
        run: cargo build --release --locked --target ${{ matrix.target }}

      - name: Package Unix binary
        if: runner.os != 'Windows'
        shell: bash
        run: |
          mkdir -p dist
          cp target/${{ matrix.target }}/release/via dist/via
          tar -C dist -czf ${{ matrix.archive }} via

      - name: Package Windows binary
        if: runner.os == 'Windows'
        shell: pwsh
        run: |
          New-Item -ItemType Directory -Force dist | Out-Null
          Copy-Item "target/${{ matrix.target }}/release/via.exe" "dist/via.exe"
          Compress-Archive -Path "dist/via.exe" -DestinationPath "${{ matrix.archive }}" -Force

      - name: Upload build artifact
        uses: actions/upload-artifact@v4
        with:
          name: ${{ matrix.archive }}
          path: ${{ matrix.archive }}
          if-no-files-found: error

      - name: Ensure GitHub Release exists
        if: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') }}
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
          RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}
        run: |
          if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
            gh release create "$RELEASE_TAG" --title "$RELEASE_TAG" --generate-notes || gh release view "$RELEASE_TAG" >/dev/null
          fi

      - name: Upload release asset
        if: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') }}
        shell: bash
        env:
          GH_TOKEN: ${{ github.token }}
          RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}
        run: |
          for attempt in 1 2 3; do
            if gh release upload "$RELEASE_TAG" "${{ matrix.archive }}" --clobber; then
              exit 0
            fi

            sleep $((attempt * 10))
          done

          echo "::error::Failed to upload ${{ matrix.archive }} to $RELEASE_TAG"
          exit 1

  checksums:
    name: Release Checksums
    if: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.release_tag != '') }}
    needs: build
    runs-on: ubuntu-24.04
    permissions:
      actions: read
      contents: write
    env:
      GH_TOKEN: ${{ github.token }}
      GH_REPO: ${{ github.repository }}
      RELEASE_TAG: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag || github.ref_name }}
      VIA_MINISIGN_SECRET_KEY: ${{ secrets.VIA_MINISIGN_SECRET_KEY }}

    steps:
      - name: Download build artifacts
        uses: actions/download-artifact@v4
        with:
          path: release-assets

      - name: Build SHA256SUMS
        shell: bash
        run: |
          shopt -s nullglob

          archives=()
          while IFS= read -r archive; do
            archives+=("$archive")
          done < <(find release-assets -type f \( -name 'via-*.tar.gz' -o -name 'via-*.zip' \) | sort)

          if [[ "${#archives[@]}" -eq 0 ]]; then
            echo "::error::No release archives found"
            exit 1
          fi

          for archive in "${archives[@]}"; do
            cp "$archive" .
          done

          sha256sum via-*.tar.gz via-*.zip > SHA256SUMS

      - name: Install minisign
        if: ${{ env.VIA_MINISIGN_SECRET_KEY != '' }}
        shell: bash
        run: |
          sudo apt-get update
          sudo apt-get install -y minisign

      - name: Sign SHA256SUMS
        if: ${{ env.VIA_MINISIGN_SECRET_KEY != '' }}
        shell: bash
        run: |
          printf '%s' "$VIA_MINISIGN_SECRET_KEY" > minisign.key
          chmod 600 minisign.key
          minisign -S -W -s minisign.key -m SHA256SUMS -x SHA256SUMS.minisig \
            -t "via $RELEASE_TAG SHA256SUMS"

      - name: Ensure GitHub Release exists
        shell: bash
        run: |
          if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
            gh release create "$RELEASE_TAG" --title "$RELEASE_TAG" --generate-notes || gh release view "$RELEASE_TAG" >/dev/null
          fi

      - name: Upload release verification assets
        shell: bash
        run: |
          files=(SHA256SUMS)
          if [[ -f SHA256SUMS.minisig ]]; then
            files+=(SHA256SUMS.minisig)
          fi

          for attempt in 1 2 3; do
            if gh release upload "$RELEASE_TAG" "${files[@]}" --clobber; then
              exit 0
            fi

            sleep $((attempt * 10))
          done

          echo "::error::Failed to upload release verification assets to $RELEASE_TAG"
          exit 1