byokey 1.2.0

Bring Your Own Keys — AI subscription-to-API proxy gateway
name: Build

on:
  release:
    types: [created]

permissions:
  contents: write

jobs:
  build:
    name: ${{ matrix.target }}
    # Only build for the root byokey tag (v*). Skip per-crate tags like
    # byokey-proto-v* or byokey-daemon-v* that release-plz may emit.
    if: startsWith(github.event.release.tag_name, 'v')
    runs-on: ${{ matrix.os }}
    env:
      # Lift secrets to job-level env so conditional steps can gate on them.
      # When a fork builds without secrets configured, these become empty
      # and the Sentry upload steps skip gracefully.
      SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
      SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
      SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_DAEMON }}
      BYOKEY_SENTRY_DSN: ${{ secrets.BYOKEY_DAEMON_SENTRY_DSN }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-22.04
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-22.04-arm
          - target: x86_64-apple-darwin
            os: macos-latest
          - target: aarch64-apple-darwin
            os: macos-latest
          - target: x86_64-pc-windows-msvc
            os: windows-latest

    steps:
      - uses: actions/checkout@v4

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

      - uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}

      - name: Install protoc
        uses: arduino/setup-protoc@v3
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

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

      - name: Install sentry-cli (Unix)
        if: env.SENTRY_AUTH_TOKEN != '' && runner.os != 'Windows'
        shell: bash
        run: curl -sL https://sentry.io/get-cli/ | INSTALL_DIR=/usr/local/bin bash

      - name: Upload Rust debug symbols to Sentry
        # Skipped on Windows: sentry-cli's Windows support is spotty and
        # the install path above is Unix-only. Runtime Sentry still works
        # on Windows; only stack-trace symbolication is missing there.
        if: env.SENTRY_AUTH_TOKEN != '' && runner.os != 'Windows'
        shell: bash
        run: |
          sentry-cli debug-files upload \
            --include-sources \
            "target/${{ matrix.target }}/release/"

      - name: Package (Unix)
        if: github.event_name == 'release' && runner.os != 'Windows'
        shell: bash
        run: |
          BIN="target/${{ matrix.target }}/release/byokey"
          ARCHIVE="byokey-${{ github.event.release.tag_name }}-${{ matrix.target }}.tar.gz"
          tar czf "$ARCHIVE" -C "$(dirname "$BIN")" "$(basename "$BIN")"
          echo "ASSET=$ARCHIVE" >> "$GITHUB_ENV"

      - name: Package (Windows)
        if: github.event_name == 'release' && runner.os == 'Windows'
        shell: pwsh
        run: |
          $archive = "byokey-${{ github.event.release.tag_name }}-${{ matrix.target }}.zip"
          Compress-Archive -Path "target\${{ matrix.target }}\release\byokey.exe" -DestinationPath $archive
          "ASSET=$archive" | Out-File -FilePath $env:GITHUB_ENV -Append

      - name: Upload to Release
        if: github.event_name == 'release'
        uses: softprops/action-gh-release@v2
        with:
          files: ${{ env.ASSET }}

  desktop:
    name: Desktop ${{ matrix.arch }}
    if: startsWith(github.event.release.tag_name, 'v')
    runs-on: macos-latest
    env:
      SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
      SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
      SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT_DESKTOP }}
      BYOKEY_SENTRY_DSN: ${{ secrets.BYOKEY_DESKTOP_SENTRY_DSN }}
    strategy:
      fail-fast: false
      matrix:
        include:
          # build_suffix makes CFBundleVersion unique per arch so Sparkle's
          # generate_appcast does not reject the DMGs as duplicates
          # ("SUSparkleErrorDomain Code=1002"). Sparkle 2 filters by embedded
          # binary architecture before comparing versions, so arm64 users
          # never see the x86_64 variant offered as an update.
          - arch: arm64
            rust_target: aarch64-apple-darwin
            build_suffix: "1"
          - arch: x86_64
            rust_target: x86_64-apple-darwin
            build_suffix: "2"

    steps:
      - uses: actions/checkout@v4

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

      - uses: Swatinem/rust-cache@v2
        with:
          key: desktop-${{ matrix.arch }}

      - name: Install protoc
        uses: arduino/setup-protoc@v3
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

      - name: Import certificate
        uses: apple-actions/import-codesign-certs@v3
        with:
          p12-file-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
          p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}

      - name: Verify certificate
        run: security find-identity -v -p codesigning

      - name: Install App Store Connect API key
        env:
          APPLE_API_KEY_BASE64: ${{ secrets.APPLE_API_KEY_BASE64 }}
          APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
        run: |
          mkdir -p ~/private_keys
          echo "$APPLE_API_KEY_BASE64" | base64 --decode \
            > ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8

      - name: Parse version from tag
        run: |
          TAG="${{ github.event.release.tag_name }}"
          echo "VERSION=${TAG#v}" >> "$GITHUB_ENV"

      - name: Archive
        run: |
          xcodebuild archive \
            -project desktop/Byokey.xcodeproj \
            -scheme Byokey \
            -configuration Release \
            -archivePath "$RUNNER_TEMP/Byokey.xcarchive" \
            ARCHS="${{ matrix.arch }}" \
            ONLY_ACTIVE_ARCH=NO \
            MARKETING_VERSION="$VERSION" \
            CURRENT_PROJECT_VERSION="${{ github.run_number }}.${{ matrix.build_suffix }}" \
            CODE_SIGN_STYLE=Manual \
            "CODE_SIGN_IDENTITY=Developer ID Application" \
            DEVELOPMENT_TEAM=8U3ZJ258K9 \
            PRODUCT_BUNDLE_IDENTIFIER=io.byokey.desktop \
            "OTHER_CODE_SIGN_FLAGS=--timestamp --options=runtime" \
            "BYOKEY_SENTRY_DSN=$BYOKEY_SENTRY_DSN"

      - name: Install sentry-cli
        if: env.SENTRY_AUTH_TOKEN != ''
        run: curl -sL https://sentry.io/get-cli/ | INSTALL_DIR=/usr/local/bin bash

      - name: Upload dSYM to Sentry
        if: env.SENTRY_AUTH_TOKEN != ''
        run: |
          sentry-cli debug-files upload \
            --include-sources \
            "$RUNNER_TEMP/Byokey.xcarchive/dSYMs/"

      - name: Export archive
        run: |
          xcodebuild -exportArchive \
            -archivePath "$RUNNER_TEMP/Byokey.xcarchive" \
            -exportOptionsPlist desktop/ExportOptions.plist \
            -exportPath "$RUNNER_TEMP/export"

      - name: Create DMG
        run: |
          brew install create-dmg
          DMG_NAME="Byokey-${{ github.event.release.tag_name }}-${{ matrix.arch }}.dmg"
          DMG_PATH="$RUNNER_TEMP/$DMG_NAME"

          create-dmg \
            --volname "Byokey" \
            --window-pos 200 120 \
            --window-size 600 400 \
            --icon-size 100 \
            --icon "Byokey.app" 175 190 \
            --app-drop-link 425 190 \
            "$DMG_PATH" \
            "$RUNNER_TEMP/export/Byokey.app"

          echo "DMG_NAME=$DMG_NAME" >> "$GITHUB_ENV"
          echo "DMG_PATH=$DMG_PATH" >> "$GITHUB_ENV"

      - name: Notarize
        env:
          APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
          APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
        run: |
          xcrun notarytool submit "$DMG_PATH" \
            --key ~/private_keys/AuthKey_${APPLE_API_KEY_ID}.p8 \
            --key-id "$APPLE_API_KEY_ID" \
            --issuer "$APPLE_API_ISSUER_ID" \
            --wait

      - name: Staple
        run: xcrun stapler staple "$DMG_PATH"

      - name: Upload DMG to Release
        uses: softprops/action-gh-release@v2
        with:
          files: ${{ env.DMG_PATH }}

  update-appcast:
    name: Update Appcast
    if: startsWith(github.event.release.tag_name, 'v')
    needs: desktop
    runs-on: macos-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Download Sparkle
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          mkdir -p /tmp/sparkle
          gh release download --repo sparkle-project/Sparkle \
            --pattern 'Sparkle-*.tar.xz' -D /tmp/sparkle
          tar xf /tmp/sparkle/Sparkle-*.tar.xz -C /tmp/sparkle

      - name: Prepare release directory
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          mkdir -p /tmp/release
          # Preserve existing appcast entries (remove if empty to avoid XML parse error)
          git show origin/gh-pages:appcast.xml > /tmp/release/appcast.xml 2>/dev/null || rm -f /tmp/release/appcast.xml
          # Download both arm64 and x86_64 DMGs; generate_appcast creates
          # separate <item> entries with matching hardwareRequirements
          gh release download "${{ github.event.release.tag_name }}" \
            --pattern 'Byokey-*.dmg' -D /tmp/release

      - name: Generate appcast
        env:
          SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
        run: |
          TAG="${{ github.event.release.tag_name }}"
          KEY_FILE=$(mktemp)
          printf '%s' "$SPARKLE_ED_PRIVATE_KEY" > "$KEY_FILE"
          trap "rm -f $KEY_FILE" EXIT

          /tmp/sparkle/bin/generate_appcast /tmp/release \
            --ed-key-file "$KEY_FILE" \
            --download-url-prefix \
            "https://github.com/AprilNEA/BYOKEY/releases/download/${TAG}/"

      - name: Deploy to GitHub Pages
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: /tmp/release
          publish_branch: gh-pages
          keep_files: true
          exclude_assets: '**.dmg'

  update-homebrew:
    name: Update Homebrew
    needs: build
    if: always() && github.event_name == 'release' && !cancelled() && startsWith(github.event.release.tag_name, 'v')
    runs-on: ubuntu-latest
    steps:
      - name: Generate token
        id: app-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.RELEASE_APP_ID }}
          private-key: ${{ secrets.RELEASE_APP_KEY }}
          repositories: homebrew-tap
      - name: Trigger homebrew-tap update
        uses: peter-evans/repository-dispatch@v3
        with:
          token: ${{ steps.app-token.outputs.token }}
          repository: AprilNEA/homebrew-tap
          event-type: update-formula
          client-payload: '{"version": "${{ github.event.release.tag_name }}"}'