homeassistant-cli 0.2.0

Agent-friendly Home Assistant CLI with JSON output, structured exit codes, and schema introspection
Documentation
name: Release

on:
  push:
    tags:
      - 'v[0-9]*.[0-9]*.[0-9]*'
  workflow_dispatch:
    inputs:
      dry_run:
        description: 'Dry run mode (skip actual publishing)'
        required: false
        default: true
        type: boolean
      skip_crates_io:
        description: 'Skip publishing to crates.io (if already published)'
        required: false
        default: false
        type: boolean

permissions:
  contents: write

env:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

jobs:
  test:
    name: Run tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - uses: taiki-e/install-action@v2
        with:
          tool: nextest

      - name: Verify Cargo.lock is up to date
        run: cargo check --locked

      - name: Run tests
        run: make test

  build:
    name: Build ${{ matrix.target }}
    needs: test
    timeout-minutes: 30
    # Guard: self-hosted runners only execute on trusted events (tag push,
    # workflow_dispatch). Fork PRs must never dispatch jobs to self-hosted.
    if: github.event_name != 'pull_request'
    strategy:
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - os: [self-hosted, Linux, ARM64]
            target: aarch64-unknown-linux-gnu
          - os: windows-latest
            target: x86_64-pc-windows-msvc
          - os: [self-hosted, macOS, ARM64]
            target: x86_64-apple-darwin
          - os: [self-hosted, macOS, ARM64]
            target: aarch64-apple-darwin
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
        with:
          # Preserve target/ between runs on self-hosted runners for cargo
          # incremental rebuild. GitHub-hosted runners start clean each job
          # regardless of this setting, so this only affects self-hosted.
          clean: false

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

      - name: Install cross-compilation tools
        # Only needed on GitHub-hosted linux where the arm64 toolchain must
        # be cross-installed. The self-hosted arm64 runner is native.
        if: matrix.target == 'aarch64-unknown-linux-gnu' && runner.arch != 'ARM64'
        run: sudo apt-get update && sudo apt-get install -y gcc-aarch64-linux-gnu

      - name: Build binary (arm64 linux via cargo-zigbuild)
        # Build natively on the Pi runner with cargo-zigbuild targeting
        # glibc 2.28 for wide distro compatibility. The host toolchain
        # links against the Pi's glibc (2.41) which is too new; zig as
        # linker lets us pin the glibc floor.
        if: matrix.target == 'aarch64-unknown-linux-gnu'
        run: cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28

      - name: Build binary
        if: matrix.target != 'aarch64-unknown-linux-gnu'
        env:
          CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
        run: cargo build --release --target ${{ matrix.target }}

      - name: Verify binary
        if: ${{ !contains(matrix.target, 'aarch64-unknown-linux') }}
        shell: bash
        run: |
          if [[ "${{ runner.os }}" == "Windows" ]]; then
            ./target/${{ matrix.target }}/release/ha.exe --version
          else
            ./target/${{ matrix.target }}/release/ha --version
          fi

      - name: Create release archive
        shell: bash
        run: |
          VERSION=${GITHUB_REF#refs/tags/}
          ARCHIVE_NAME="homeassistant-cli-${VERSION}-${{ matrix.target }}"

          mkdir -p release-package

          if [[ "${{ runner.os }}" == "Windows" ]]; then
            cp "target/${{ matrix.target }}/release/ha.exe" release-package/ha.exe
            cd release-package
            powershell -command "Compress-Archive -Path ha.exe -DestinationPath ../${ARCHIVE_NAME}.zip"
            cd ..
            powershell -command "Get-FileHash -Path '${ARCHIVE_NAME}.zip' -Algorithm SHA256 | Select-Object -ExpandProperty Hash" > "${ARCHIVE_NAME}.zip.sha256"
          else
            cp "target/${{ matrix.target }}/release/ha" release-package/ha
            tar -czf "${ARCHIVE_NAME}.tar.gz" -C release-package ha

            if [[ "${{ runner.os }}" == "macOS" ]]; then
              shasum -a 256 "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
            else
              sha256sum "${ARCHIVE_NAME}.tar.gz" > "${ARCHIVE_NAME}.tar.gz.sha256"
            fi
          fi

          rm -rf release-package

      - name: Upload release archives
        uses: actions/upload-artifact@v4
        with:
          path: |
            homeassistant-cli-*-${{ matrix.target }}.tar.gz*
            homeassistant-cli-*-${{ matrix.target }}.zip*
          name: release-${{ matrix.target }}

  release:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - name: Publish to crates.io
        if: ${{ inputs.dry_run != true && inputs.skip_crates_io != true }}
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        run: cargo publish --locked

      - name: Test crates.io publish (dry run)
        if: ${{ inputs.dry_run == true && inputs.skip_crates_io != true }}
        run: |
          echo "DRY RUN: Would publish to crates.io"
          cargo publish --dry-run --locked

      - name: Skip crates.io publishing
        if: ${{ inputs.skip_crates_io == true }}
        run: echo "Skipping crates.io publishing as requested"

      - name: Download all artifacts
        uses: actions/download-artifact@v4
        with:
          path: artifacts

      - name: Create Release
        if: ${{ inputs.dry_run != true }}
        uses: softprops/action-gh-release@v2
        with:
          generate_release_notes: true
          files: |
            artifacts/release-*/homeassistant-cli-*.tar.gz
            artifacts/release-*/homeassistant-cli-*.tar.gz.sha256
            artifacts/release-*/homeassistant-cli-*.zip
            artifacts/release-*/homeassistant-cli-*.zip.sha256

      - name: Dry Run Summary
        if: ${{ inputs.dry_run == true }}
        run: |
          echo "Dry run complete. Artifacts built but nothing published."
          echo "Archives:"
          find artifacts/release-* -type f | sort

  update-homebrew:
    needs: release
    if: ${{ !inputs.dry_run }}
    runs-on: ubuntu-latest
    steps:
      - uses: actions/download-artifact@v4
        with:
          path: /tmp/artifacts

      - name: Compute SHA256 hashes
        id: hashes
        run: |
          for target in x86_64-apple-darwin aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
            sha=$(cat /tmp/artifacts/release-${target}/homeassistant-cli-${{ github.ref_name }}-${target}.tar.gz.sha256 | awk '{print $1}')
            key=$(echo "${target}" | tr '-' '_')
            echo "${key}=${sha}" >> "$GITHUB_OUTPUT"
          done

      - name: Update Homebrew formula
        env:
          GH_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
        run: |
          VERSION="${{ github.ref_name }}"
          VERSION_NUM="${VERSION#v}"

          cat > /tmp/homeassistant-cli.rb << 'FORMULA'
          class HomeassistantCli < Formula
            desc "CLI for Home Assistant"
            homepage "https://github.com/rvben/homeassistant-cli"
            version "VERSION_NUM"
            license "MIT"

            on_macos do
              if Hardware::CPU.arm?
                url "https://github.com/rvben/homeassistant-cli/releases/download/VERSION/homeassistant-cli-VERSION-aarch64-apple-darwin.tar.gz"
                sha256 "SHA_AARCH64_APPLE_DARWIN"
              else
                url "https://github.com/rvben/homeassistant-cli/releases/download/VERSION/homeassistant-cli-VERSION-x86_64-apple-darwin.tar.gz"
                sha256 "SHA_X86_64_APPLE_DARWIN"
              end
            end

            on_linux do
              if Hardware::CPU.arm?
                url "https://github.com/rvben/homeassistant-cli/releases/download/VERSION/homeassistant-cli-VERSION-aarch64-unknown-linux-gnu.tar.gz"
                sha256 "SHA_AARCH64_UNKNOWN_LINUX_GNU"
              else
                url "https://github.com/rvben/homeassistant-cli/releases/download/VERSION/homeassistant-cli-VERSION-x86_64-unknown-linux-gnu.tar.gz"
                sha256 "SHA_X86_64_UNKNOWN_LINUX_GNU"
              end
            end

            def install
              bin.install "ha"
            end

            test do
              system "#{bin}/ha", "--version"
            end
          end
          FORMULA

          sed -i "s/VERSION_NUM/${VERSION_NUM}/g" /tmp/homeassistant-cli.rb
          sed -i "s/VERSION/${VERSION}/g" /tmp/homeassistant-cli.rb
          sed -i "s/SHA_AARCH64_APPLE_DARWIN/${{ steps.hashes.outputs.aarch64_apple_darwin }}/g" /tmp/homeassistant-cli.rb
          sed -i "s/SHA_X86_64_APPLE_DARWIN/${{ steps.hashes.outputs.x86_64_apple_darwin }}/g" /tmp/homeassistant-cli.rb
          sed -i "s/SHA_AARCH64_UNKNOWN_LINUX_GNU/${{ steps.hashes.outputs.aarch64_unknown_linux_gnu }}/g" /tmp/homeassistant-cli.rb
          sed -i "s/SHA_X86_64_UNKNOWN_LINUX_GNU/${{ steps.hashes.outputs.x86_64_unknown_linux_gnu }}/g" /tmp/homeassistant-cli.rb

          # Clone tap repo, update formula, push
          git clone https://x-access-token:${GH_TOKEN}@github.com/rvben/homebrew-tap.git /tmp/tap
          mkdir -p /tmp/tap/Formula
          cp /tmp/homeassistant-cli.rb /tmp/tap/Formula/homeassistant-cli.rb
          cd /tmp/tap
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add Formula/homeassistant-cli.rb
          git diff --cached --quiet || git commit -m "Update homeassistant-cli to ${VERSION}"
          git push