browser-control 0.2.1

CLI that manages browsers and exposes them over CDP/BiDi for agent-driven development. Includes an optional MCP server.
Documentation
# Release workflow.
#
# Triggered by pushing a `v*` tag. The tag IS the gate — no workflow_dispatch,
# no production environment, no manual approval. Whoever pushes the tag is
# the gatekeeper. Use `cargo release patch --execute` (driven by `release.toml`
# at the repo root) to bump + commit + tag + push from a workstation; CI does
# the rest.
#
# Pipeline:
#   1. verify           — tag must match Cargo.toml version.
#   2. create-release   — draft GitHub Release.
#   3. publish-crate    — idempotent `cargo publish`; append crates.io link.
#   4. build-binaries   — matrix per target; upload tarball/zip to draft release.
#   5. homebrew-bump    — render Formula/browser-control.rb from template,
#                         commit to main (Pattern A, in-repo tap).
#   6. finalize-release — un-draft the release once everything succeeded.
#
# Idempotency:
#   * Re-running on the same tag is safe. cargo publish treats "already
#     uploaded" as success. Archive uploads use --clobber. Homebrew commit
#     is a no-op if SHAs haven't changed. The release stays a draft until
#     finalize succeeds, so partial failures don't ship a half-baked release.

name: Release

on:
  push:
    tags:
      - "v*"

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always

jobs:
  verify:
    name: Verify tag matches Cargo.toml version
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Check version
        run: |
          set -euo pipefail
          TAG="${GITHUB_REF_NAME#v}"
          CARGO_VERSION=$(sed -n '/^\[package\]/,/^\[/p' Cargo.toml \
            | grep -m1 '^version' \
            | sed -E 's/version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/')
          echo "Tag:        $TAG"
          echo "Cargo.toml: $CARGO_VERSION"
          if [ -z "$CARGO_VERSION" ] || [ "$TAG" != "$CARGO_VERSION" ]; then
            echo "::error::Tag ($TAG) does not match Cargo.toml version ($CARGO_VERSION)"
            exit 1
          fi

  create-release:
    name: Create draft release
    needs: verify
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: softprops/action-gh-release@v2
        with:
          tag_name: ${{ github.ref_name }}
          name: ${{ github.ref_name }}
          draft: true
          generate_release_notes: true

  publish-crate:
    name: Publish to crates.io
    needs: create-release
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
        with:
          key: publish-crate
      - name: Publish browser-control
        env:
          CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
        # crates.io rejects duplicate versions; treat that as success.
        # Useful for partial-failure reruns.
        shell: bash
        run: |
          set +e
          output=$(cargo publish --locked 2>&1)
          rc=$?
          set -e
          printf '%s\n' "$output"
          if [ "$rc" -eq 0 ]; then
            exit 0
          fi
          if printf '%s' "$output" | grep -qE "already (exists|uploaded)"; then
            exit 0
          fi
          exit "$rc"
      - name: Add crates.io link to release notes
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          VERSION="${GITHUB_REF_NAME#v}"
          gh release edit "${GITHUB_REF_NAME}" \
            --notes "$(gh release view "${GITHUB_REF_NAME}" --json body -q .body)

          crates.io: https://crates.io/crates/browser-control/${VERSION}"

  build-binaries:
    name: Build CLI (${{ matrix.target }})
    needs: create-release
    runs-on: ${{ matrix.os }}
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - os: ubuntu-latest
            target: x86_64-unknown-linux-gnu
          - os: ubuntu-24.04-arm
            target: aarch64-unknown-linux-gnu
          - os: macos-latest
            target: aarch64-apple-darwin
          - os: macos-latest
            target: x86_64-apple-darwin
          - os: windows-latest
            target: x86_64-pc-windows-msvc
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@v2
        with:
          key: ${{ matrix.target }}
          cache-targets: false
      - name: Build binary
        shell: bash
        run: cargo build --release --target ${{ matrix.target }} --locked
      - name: Strip binary (Unix)
        if: runner.os != 'Windows'
        shell: bash
        run: |
          BIN="target/${{ matrix.target }}/release/browser-control"
          strip "$BIN" || true
      - name: Package archive
        shell: bash
        run: |
          set -euo pipefail
          NAME="browser-control-${{ matrix.target }}"
          STAGE="staging/$NAME"
          mkdir -p "$STAGE" dist
          if [ "${{ runner.os }}" = "Windows" ]; then
            cp "target/${{ matrix.target }}/release/browser-control.exe" "$STAGE/"
          else
            cp "target/${{ matrix.target }}/release/browser-control" "$STAGE/"
          fi
          cp README.md LICENSE "$STAGE/"
          cd staging
          if [ "${{ runner.os }}" = "Windows" ]; then
            7z a -tzip "../dist/${NAME}.zip" "$NAME" >/dev/null
          else
            tar -czf "../dist/${NAME}.tar.gz" "$NAME"
          fi
      - name: Upload archive to release
        shell: bash
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          shopt -s nullglob
          assets=(dist/*.tar.gz dist/*.zip)
          if [ "${#assets[@]}" -eq 0 ]; then
            echo "::error::No release archives found under dist/"
            exit 1
          fi
          gh release upload "${GITHUB_REF_NAME}" "${assets[@]}" --clobber

  homebrew-bump:
    name: Bump in-repo Homebrew formula
    # Needs build-binaries so the tarballs exist on the draft release; we
    # download them and compute SHA-256s. Pattern A: in-repo tap.
    needs: build-binaries
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - uses: actions/checkout@v4
        with:
          ref: main
      - name: Download release artifacts
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          mkdir -p tarballs
          gh release download "${GITHUB_REF_NAME}" -p "*.tar.gz" -D tarballs
      - name: Compute SHAs
        id: shas
        shell: bash
        run: |
          set -euo pipefail
          read_sha() {
            shasum -a 256 "tarballs/browser-control-$1.tar.gz" | awk '{print $1}'
          }
          {
            echo "darwin_arm=$(read_sha aarch64-apple-darwin)"
            echo "darwin_x86=$(read_sha x86_64-apple-darwin)"
            echo "linux_x86=$(read_sha x86_64-unknown-linux-gnu)"
            echo "linux_arm=$(read_sha aarch64-unknown-linux-gnu)"
          } >> "$GITHUB_OUTPUT"
      - name: Render formula
        shell: bash
        run: |
          set -euo pipefail
          VERSION="${GITHUB_REF_NAME#v}"
          BASE_URL="https://github.com/${{ github.repository }}/releases/download/v${VERSION}"
          mkdir -p Formula
          sed \
            -e "s|@@VERSION@@|${VERSION}|g" \
            -e "s|@@URL_DARWIN_ARM@@|${BASE_URL}/browser-control-aarch64-apple-darwin.tar.gz|g" \
            -e "s|@@SHA_DARWIN_ARM@@|${{ steps.shas.outputs.darwin_arm }}|g" \
            -e "s|@@URL_DARWIN_X86@@|${BASE_URL}/browser-control-x86_64-apple-darwin.tar.gz|g" \
            -e "s|@@SHA_DARWIN_X86@@|${{ steps.shas.outputs.darwin_x86 }}|g" \
            -e "s|@@URL_LINUX_X86@@|${BASE_URL}/browser-control-x86_64-unknown-linux-gnu.tar.gz|g" \
            -e "s|@@SHA_LINUX_X86@@|${{ steps.shas.outputs.linux_x86 }}|g" \
            -e "s|@@URL_LINUX_ARM@@|${BASE_URL}/browser-control-aarch64-unknown-linux-gnu.tar.gz|g" \
            -e "s|@@SHA_LINUX_ARM@@|${{ steps.shas.outputs.linux_arm }}|g" \
            .github/templates/browser-control.rb.tmpl \
            > Formula/browser-control.rb
          echo "Rendered formula:"
          cat Formula/browser-control.rb
      - name: Commit and push to main
        shell: bash
        run: |
          set -euo pipefail
          VERSION="${GITHUB_REF_NAME#v}"
          git config --global user.email "actions@github.com"
          git config --global user.name  "github-actions[bot]"
          git add Formula/browser-control.rb
          if git diff --staged --quiet; then
            echo "::notice::Formula already up to date — no commit needed"
            exit 0
          fi
          git commit -m "release: bump Homebrew formula to ${VERSION} [skip ci]"
          git push origin main

  finalize-release:
    name: Publish GitHub release
    needs: [publish-crate, build-binaries, homebrew-bump]
    runs-on: ubuntu-latest
    permissions:
      contents: write
    steps:
      - name: Un-draft release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh release edit "${GITHUB_REF_NAME}" \
            --repo "${{ github.repository }}" \
            --draft=false