albatross-cli 2.2.0

Open source, terminal-first AI coding agent with fully transparent multi-model routing. Local (Ollama, LM Studio, MLX, llama.cpp) or cloud, your keys, one TUI.
name: Release

on:
  push:
    tags:
      - 'v*'
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to release (e.g. v0.3.0). For testing — does not move the git tag.'
        required: true

permissions:
  contents: write

jobs:
  build:
    name: Build (${{ matrix.target }})
    runs-on: macos-14
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: aarch64-apple-darwin
            arch: arm64
          - target: x86_64-apple-darwin
            arch: x86_64
    steps:
      - uses: actions/checkout@v6

      - name: Resolve tag
        id: tag
        shell: bash
        run: |
          if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
            echo "tag=${{ inputs.tag }}" >> "$GITHUB_OUTPUT"
          else
            echo "tag=${GITHUB_REF_NAME}" >> "$GITHUB_OUTPUT"
          fi

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

      - name: Cache cargo registry + target
        uses: actions/cache@v5
        with:
          path: |
            ~/.cargo/registry
            ~/.cargo/git
            target
          key: ${{ runner.os }}-cargo-${{ matrix.target }}-${{ hashFiles('Cargo.lock') }}

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

      # Codesign + notarize only when the Apple secrets are present.
      # Without them the artifact is still produced — users see a Gatekeeper
      # warning on first launch and must right-click → Open once.
      - name: Codesign + notarize
        if: env.APPLE_ID != ''
        env:
          APPLE_ID: ${{ secrets.APPLE_ID }}
          APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
          APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
          APPLE_DEVELOPER_ID: ${{ secrets.APPLE_DEVELOPER_ID }}
          APPLE_CERTIFICATE_P12_BASE64: ${{ secrets.APPLE_CERTIFICATE_P12_BASE64 }}
          APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
        shell: bash
        run: |
          set -euo pipefail
          BIN=target/${{ matrix.target }}/release/albatross

          # Import the certificate into a temporary keychain.
          KEYCHAIN=build.keychain
          KEYCHAIN_PASSWORD=$(openssl rand -hex 16)
          security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
          security default-keychain -s "$KEYCHAIN"
          security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
          security set-keychain-settings -lut 21600 "$KEYCHAIN"

          CERT_PATH=$RUNNER_TEMP/dev-id.p12
          echo "$APPLE_CERTIFICATE_P12_BASE64" | base64 --decode > "$CERT_PATH"
          security import "$CERT_PATH" -k "$KEYCHAIN" \
            -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
          security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN"

          codesign --force --options runtime --timestamp \
            --sign "$APPLE_DEVELOPER_ID" "$BIN"

          # Notarize via ditto-zipped binary, then staple is N/A for a bare
          # binary (no app bundle). The signature + notarization ticket lives
          # on Apple's servers; Gatekeeper queries online on first launch.
          ZIP=$RUNNER_TEMP/albatross-${{ matrix.target }}.zip
          ditto -c -k --keepParent "$BIN" "$ZIP"
          xcrun notarytool submit "$ZIP" \
            --apple-id "$APPLE_ID" \
            --team-id "$APPLE_TEAM_ID" \
            --password "$APPLE_APP_SPECIFIC_PASSWORD" \
            --wait

      - name: Package tarball + checksum
        shell: bash
        run: |
          set -euo pipefail
          OUT_DIR="$RUNNER_TEMP/dist"
          mkdir -p "$OUT_DIR"
          NAME="albatross-${{ steps.tag.outputs.tag }}-${{ matrix.target }}"
          ARTIFACT="$OUT_DIR/$NAME.tar.gz"
          tar -czf "$ARTIFACT" \
            -C target/${{ matrix.target }}/release albatross \
            -C "$GITHUB_WORKSPACE" LICENSE README.md Quickstart.md
          (cd "$OUT_DIR" && shasum -a 256 "$NAME.tar.gz" > "$NAME.sha256")
          echo "ARTIFACT_DIR=$OUT_DIR" >> "$GITHUB_ENV"
          echo "ARTIFACT_NAME=$NAME" >> "$GITHUB_ENV"

      - name: Upload artifact
        uses: actions/upload-artifact@v7
        with:
          name: ${{ env.ARTIFACT_NAME }}
          path: ${{ env.ARTIFACT_DIR }}

  publish:
    name: Publish release
    needs: build
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    outputs:
      version: ${{ steps.meta.outputs.version }}
      arm_sha: ${{ steps.meta.outputs.arm_sha }}
      x86_sha: ${{ steps.meta.outputs.x86_sha }}
    steps:
      - uses: actions/checkout@v6

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

      - name: Flatten artifacts
        shell: bash
        run: |
          set -euo pipefail
          mkdir -p release
          find dist -type f \( -name '*.tar.gz' -o -name '*.sha256' \) -exec cp {} release/ \;
          (cd release && sha256sum *.tar.gz > SHA256SUMS)
          ls -la release/

      - name: Extract release metadata
        id: meta
        shell: bash
        run: |
          set -euo pipefail
          version="${GITHUB_REF_NAME#v}"
          arm_sha=$(awk '/aarch64-apple-darwin\.tar\.gz/{print $1}' release/SHA256SUMS)
          x86_sha=$(awk '/x86_64-apple-darwin\.tar\.gz/{print $1}' release/SHA256SUMS)
          {
            echo "version=$version"
            echo "arm_sha=$arm_sha"
            echo "x86_sha=$x86_sha"
          } >> "$GITHUB_OUTPUT"

      - name: Create GitHub release
        uses: softprops/action-gh-release@v3
        with:
          files: release/*
          fail_on_unmatched_files: true
          generate_release_notes: true

  update-tap:
    name: Update Homebrew tap
    needs: publish
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    env:
      TAP_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}
    steps:
      - name: Check for tap token
        id: gate
        shell: bash
        run: |
          if [[ -z "${TAP_TOKEN}" ]]; then
            echo "::notice::TAP_GITHUB_TOKEN not set — skipping Homebrew tap update."
            echo "enabled=false" >> "$GITHUB_OUTPUT"
          else
            echo "enabled=true" >> "$GITHUB_OUTPUT"
          fi

      - name: Checkout tap repo
        if: steps.gate.outputs.enabled == 'true'
        uses: actions/checkout@v6
        with:
          repository: morganlinton/homebrew-tap
          token: ${{ secrets.TAP_GITHUB_TOKEN }}
          path: tap

      - name: Regenerate formula
        if: steps.gate.outputs.enabled == 'true'
        shell: bash
        env:
          VERSION: ${{ needs.publish.outputs.version }}
          ARM_SHA: ${{ needs.publish.outputs.arm_sha }}
          X86_SHA: ${{ needs.publish.outputs.x86_sha }}
        run: |
          set -euo pipefail
          [[ -n "$VERSION" && -n "$ARM_SHA" && -n "$X86_SHA" ]] || {
            echo "::error::missing version/sha outputs from publish job"; exit 1; }
          mkdir -p tap/Formula
          cat > tap/Formula/albatross.rb <<EOF
          # Generated by Albatross release workflow — do not edit by hand.
          class Albatross < Formula
            desc "Terminal-based agent harness for running small LLMs on your Mac"
            homepage "https://github.com/morganlinton/Albatross"
            version "${VERSION}"
            license "MIT"

            on_macos do
              on_arm do
                url "https://github.com/morganlinton/Albatross/releases/download/v${VERSION}/albatross-v${VERSION}-aarch64-apple-darwin.tar.gz"
                sha256 "${ARM_SHA}"
              end
              on_intel do
                url "https://github.com/morganlinton/Albatross/releases/download/v${VERSION}/albatross-v${VERSION}-x86_64-apple-darwin.tar.gz"
                sha256 "${X86_SHA}"
              end
            end

            def install
              bin.install "albatross"
              doc.install Dir["README.md", "Quickstart.md", "LICENSE"]
              generate_completions_from_executable(bin/"albatross", "completions")
            end

            test do
              assert_match "albatross", shell_output("#{bin}/albatross completions bash")
            end
          end
          EOF

      - name: Commit + push formula
        if: steps.gate.outputs.enabled == 'true'
        shell: bash
        working-directory: tap
        run: |
          set -euo pipefail
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          # `git status --porcelain` rather than `git diff`: diff reports no
          # change for an untracked file, so a newly added formula (as after a
          # rename) would look unchanged and silently skip the commit.
          if [[ -z "$(git status --porcelain -- Formula/albatross.rb)" ]]; then
            echo "Formula already at ${{ needs.publish.outputs.version }} — nothing to do."
            exit 0
          fi
          git add Formula/albatross.rb
          git commit -m "albatross ${{ needs.publish.outputs.version }}"
          git push