agent-desktop/.github/workflows/ci.yml
Lahfir fb75685e05 ci: harden package and supply-chain checks
Add locked Rust verification, binary command tests, npm package surface checks, dependency policy scanning, workflow security auditing, and Dependabot updates so PR and release gates cover the package surfaces that are shipped.
2026-05-19 13:25:06 -07:00

156 lines
5.6 KiB
YAML

name: CI
on:
push:
branches: [main, master]
pull_request:
workflow_dispatch:
# Cancel in-progress runs for the same ref so stale PR builds don't burn minutes.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
# Deny all permissions at workflow level; each job declares only what it needs.
permissions: {}
env:
CARGO_TERM_COLOR: always
RUST_BACKTRACE: 1
jobs:
fmt:
name: Format
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- run: rustup component add rustfmt
- run: cargo fmt --all -- --check
test:
name: Test
runs-on: macos-latest
timeout-minutes: 30
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Rust toolchain
run: rustup show
- name: Cache cargo registry
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.toml', 'Cargo.lock') }}
restore-keys: ${{ runner.os }}-cargo-
- name: Cache build artifacts
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: target/
key: ${{ runner.os }}-build-ci-${{ hashFiles('**/Cargo.toml', 'Cargo.lock') }}-${{ hashFiles('**/*.rs') }}
restore-keys: |
${{ runner.os }}-build-ci-${{ hashFiles('**/Cargo.toml', 'Cargo.lock') }}-
${{ runner.os }}-build-ci-
- name: Check dependency isolation
run: |
if cargo tree --locked -p agent-desktop-core 2>/dev/null | grep -E 'agent-desktop-(macos|windows|linux)'; then
echo "FAIL: core crate depends on platform crates"
exit 1
fi
echo "OK: core crate has no platform dependencies"
- name: Clippy
run: cargo clippy --locked --all-targets -- -D warnings
- name: Unit tests
run: cargo test --locked --lib --workspace
- name: Binary command tests
run: cargo test --locked -p agent-desktop
# The FFI crate ships integration harnesses under crates/ffi/tests/ that
# exercise the C-ABI from outside the crate (raw extern "C" decls, enum
# fuzzing, out-param zeroing, last-error lifetimes). The workspace --lib
# run skips them, so wire them in explicitly so regressions fail PR CI.
- name: FFI integration tests
run: cargo test --locked -p agent-desktop-ffi --tests
# Use the ci profile (no LTO, opt-level 1) — fast compile, still checks the binary builds.
- name: Build binary (ci profile)
run: cargo build --locked --profile ci
- name: Check binary size
run: |
SIZE=$(stat -f%z target/ci/agent-desktop)
LIMIT=$((15 * 1024 * 1024))
echo "Binary size: $(du -sh target/ci/agent-desktop | cut -f1)"
if [ "$SIZE" -gt "$LIMIT" ]; then
echo "FAIL: binary exceeds 15MB limit (${SIZE} bytes)"
exit 1
fi
echo "OK: binary within 15MB limit"
- name: FFI cdylib build (release-ffi profile)
run: cargo build --locked --profile release-ffi -p agent-desktop-ffi
- name: FFI header drift check
run: |
# build.rs stamps target/ffi-header-path.txt with the absolute path
# to the header produced by the most recent build, so the drift
# check always compares against the just-generated artifact rather
# than an arbitrary match from `find | head -1` over a warm cache.
STAMP=target/ffi-header-path.txt
if [ ! -f "$STAMP" ]; then
echo "FAIL: $STAMP missing; build.rs did not stamp header path"
exit 1
fi
GENERATED=$(cat "$STAMP")
if [ ! -f "$GENERATED" ]; then
echo "FAIL: stamped header path does not exist: $GENERATED"
exit 1
fi
if ! diff -u crates/ffi/include/agent_desktop.h "$GENERATED"; then
echo "FAIL: crates/ffi/include/agent_desktop.h is out of date"
echo "Run 'scripts/update-ffi-header.sh' locally and commit the regenerated header."
exit 1
fi
echo "OK: FFI header is in sync with source"
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '24'
- name: NPM package contents
run: node scripts/check-npm-package.js
- name: NPM wrapper smoke
run: |
case "$(uname -s)-$(uname -m)" in
Darwin-arm64) NAME=agent-desktop-darwin-arm64 ;;
Darwin-x86_64) NAME=agent-desktop-darwin-x64 ;;
*)
echo "Unsupported smoke-test platform: $(uname -s)-$(uname -m)"
exit 1
;;
esac
trap 'rm -f "npm/bin/${NAME}"' EXIT
cp target/ci/agent-desktop "npm/bin/${NAME}"
chmod +x "npm/bin/${NAME}"
node npm/bin/agent-desktop.js version --json > /tmp/agent-desktop-version.json
node -e "
const out = require('fs').readFileSync('/tmp/agent-desktop-version.json', 'utf8');
const json = JSON.parse(out);
if (json.ok !== true || !json.data || typeof json.data.version !== 'string') {
throw new Error('agent-desktop npm wrapper did not return version JSON');
}
"