computer-use-linux 0.2.7

Linux desktop control over MCP — AT-SPI accessibility tree, multi-compositor window targeting (GNOME, KWin, Hyprland, i3, COSMIC), screencast portal screenshots, and ydotool input synthesis. Wayland-first, X11 best-effort.
name: ci

on:
  push:
    branches: [main]
    tags: ['v*']
  pull_request:
    branches: [main]

permissions:
  contents: read

env:
  CARGO_TERM_COLOR: always
  FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
  RUSTFLAGS: -D warnings

jobs:
  fmt:
    name: cargo fmt
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          components: rustfmt
      - run: cargo fmt --all -- --check

  check:
    name: cargo check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
      - run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - run: cargo check --locked --all-targets

  clippy:
    name: cargo clippy
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          components: clippy
      - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
      - run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - run: cargo clippy --locked --all-targets -- -D warnings

  test:
    name: cargo test (${{ matrix.target }})
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        target:
          - x86_64-unknown-linux-gnu
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
      - run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - run: cargo test --locked --target ${{ matrix.target }} --no-fail-fast

  rustdoc-and-package:
    name: rustdoc and cargo package
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
      - run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - run: RUSTDOCFLAGS="-D warnings" cargo doc --locked --no-deps --document-private-items
      - run: cargo publish --dry-run --locked

  supply-chain:
    name: cargo audit
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
      - run: cargo install cargo-audit --locked
      - run: cargo audit --deny warnings

  mcp-safety:
    name: MCP safety contract
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
      - run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - run: cargo build --locked
      - run: scripts/mcp_safety_check.py --binary target/debug/computer-use-linux

  agent-config:
    name: agnix
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
      - run: npm install -g agnix
      - run: agnix .

  npm-wrapper:
    name: npm wrapper
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - uses: actions/setup-node@v4
        with:
          node-version: 24
      - run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - name: Check Cargo and npm versions match
        run: |
          cargo_version="$(grep -m1 '^version = ' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')"
          npm_version="$(node -p "require('./package.json').version")"
          test "$cargo_version" = "$npm_version"
      - run: node --check npm/install.js
      - run: node --check npm/bin/computer-use-linux.js
      - run: cargo build --locked
      - name: Test npm wrapper against local binary
        run: |
          prefix="$RUNNER_TEMP/npm-prefix"
          COMPUTER_USE_LINUX_LOCAL_BINARY="$PWD/target/debug/computer-use-linux" \
          COMPUTER_USE_LINUX_LOCAL_COSMIC_HELPER="$PWD/target/debug/computer-use-linux-cosmic" \
            npm install --prefix "$prefix" -g .
          "$prefix/bin/computer-use-linux" --help
      - run: npm pack --dry-run

  zod-schema:
    name: zod MCP schema
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
      - uses: actions/setup-node@v4
        with:
          node-version: 24
      - run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - run: cargo build --locked
      - name: Install MCP SDK for the zod check
        run: npm ci --prefix scripts/zod-check --ignore-scripts --no-audit --no-fund
      - name: Validate tools/list against the MCP SDK zod schema
        run: node scripts/zod-check/check.mjs --command target/debug/computer-use-linux

  release-binary:
    name: release artifact (${{ matrix.target }})
    needs:
      - fmt
      - check
      - clippy
      - test
      - rustdoc-and-package
      - supply-chain
      - mcp-safety
      - agent-config
      - npm-wrapper
      - zod-schema
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      contents: write
    strategy:
      fail-fast: false
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            asset: computer-use-linux-x86_64-unknown-linux-gnu
          - target: aarch64-unknown-linux-gnu
            asset: computer-use-linux-aarch64-unknown-linux-gnu
            cross: true
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
        with:
          targets: ${{ matrix.target }}
      - uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2
      - name: Install host deps
        run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - name: Install cross-compile deps
        if: matrix.cross
        run: sudo apt-get install -y gcc-aarch64-linux-gnu
      - name: Configure cross linker
        if: matrix.cross
        run: |
          mkdir -p .cargo
          cat >> .cargo/config.toml <<'EOF'
          [target.aarch64-unknown-linux-gnu]
          linker = "aarch64-linux-gnu-gcc"
          EOF
      - name: Build release binary
        run: cargo build --locked --release --target ${{ matrix.target }}
      - name: Stage artifact
        run: |
          mkdir -p dist
          cp target/${{ matrix.target }}/release/computer-use-linux dist/${{ matrix.asset }}
          cp target/${{ matrix.target }}/release/computer-use-linux-cosmic dist/computer-use-linux-cosmic-${{ matrix.target }}
          (
            cd dist
            sha256sum ${{ matrix.asset }} > ${{ matrix.asset }}.sha256
            sha256sum computer-use-linux-cosmic-${{ matrix.target }} > computer-use-linux-cosmic-${{ matrix.target }}.sha256
          )
      - uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
        with:
          files: |
            dist/${{ matrix.asset }}
            dist/${{ matrix.asset }}.sha256
            dist/computer-use-linux-cosmic-${{ matrix.target }}
            dist/computer-use-linux-cosmic-${{ matrix.target }}.sha256
          generate_release_notes: true

  publish-crate:
    name: publish crate
    needs: [release-binary]
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    env:
      CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
      - run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - name: Publish to crates.io
        run: |
          test -n "$CARGO_REGISTRY_TOKEN"
          crate="$(grep -m1 '^name = ' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')"
          version="$(grep -m1 '^version = ' Cargo.toml | sed 's/.*"\(.*\)".*/\1/')"
          test "v$version" = "$GITHUB_REF_NAME"
          if curl -fsS "https://crates.io/api/v1/crates/${crate}/${version}" >/dev/null; then
            echo "${crate} ${version} is already on crates.io; skipping"
          else
            cargo publish --locked
          fi

  publish-npm:
    name: publish npm
    needs: [release-binary]
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    env:
      NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
          registry-url: https://registry.npmjs.org
      - name: Publish to npm
        run: |
          test -n "$NODE_AUTH_TOKEN"
          name="$(node -p "require('./package.json').name")"
          version="$(node -p "require('./package.json').version")"
          test "v$version" = "$GITHUB_REF_NAME"
          base="https://github.com/agent-sh/computer-use-linux/releases/download/v${version}"
          for asset in \
            computer-use-linux-x86_64-unknown-linux-gnu \
            computer-use-linux-aarch64-unknown-linux-gnu \
            computer-use-linux-cosmic-x86_64-unknown-linux-gnu \
            computer-use-linux-cosmic-aarch64-unknown-linux-gnu
          do
            for suffix in "" ".sha256"; do
              for attempt in {1..30}; do
                if curl -fsSIL "${base}/${asset}${suffix}" >/dev/null; then
                  break
                fi
                if [[ "$attempt" -eq 30 ]]; then
                  echo "release asset not available: ${asset}${suffix}" >&2
                  exit 1
                fi
                sleep 5
              done
            done
          done
          if npm view "${name}@${version}" version >/dev/null 2>&1; then
            echo "${name} ${version} is already on npm; skipping"
          else
            npm publish --access public
          fi

  validate-published:
    name: zod schema (published npm)
    needs: [publish-npm]
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 24
      - run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
      - name: Install the published package
        run: |
          name="$(node -p "require('./package.json').name")"
          version="$(node -p "require('./package.json').version")"
          test "v$version" = "$GITHUB_REF_NAME"
          npm install --prefix "$RUNNER_TEMP/published" -g "${name}@${version}"
      - name: Install MCP SDK for the zod check
        run: npm ci --prefix scripts/zod-check --ignore-scripts --no-audit --no-fund
      - name: Validate the published tools/list against the MCP SDK zod schema
        run: node scripts/zod-check/check.mjs --command "$RUNNER_TEMP/published/bin/computer-use-linux"

  notify-codex-desktop:
    name: open codex-desktop update reminder
    needs: [publish-crate, publish-npm]
    if: startsWith(github.ref, 'refs/tags/v')
    runs-on: ubuntu-latest
    permissions:
      contents: read
      issues: write
    steps:
      - uses: actions/checkout@v4
      - name: Open reminder issue to sync codex-desktop-linux
        env:
          GH_TOKEN: ${{ github.token }}
          VERSION: ${{ github.ref_name }}
        run: |
          title="Sync codex-desktop-linux with ${VERSION} release content"
          existing="$(gh issue list --state all --search "in:title $title" --json title,number \
            --jq ".[] | select(.title == \"$title\") | .number" | head -n1)"
          if [ -n "$existing" ]; then
            echo "reminder issue #$existing already exists; skipping"
            exit 0
          fi
          notes="$(awk -v ver="${VERSION#v}" '
            $0 ~ "^## \\[" ver "\\]" { capture = 1; next }
            capture && /^## \[/ { exit }
            capture { print }
          ' CHANGELOG.md)"
          [ -z "$notes" ] && notes="(no CHANGELOG section found for ${VERSION})"
          body="$(printf '%s released. Update [codex-desktop-linux](https://github.com/avifenesh/codex-desktop-linux) to pin/ship this version and mirror the relevant changes.\n\n## %s changelog\n%s\n\n---\n_Opened automatically by the release workflow._' "${VERSION}" "${VERSION}" "${notes}")"
          gh issue create --title "$title" --body "$body"