agent-desktop/.github/workflows/ci.yml
Lahfir 3cffbd67f6
feat(ffi): ship C-ABI cdylib with review hardening and release pipeline (#26)
Lands the full agent-desktop FFI layer with every PR #22 review finding resolved, modular refactor applied, todo-resolve batches (011 + 006) closed, and a production release pipeline that bundles the prebuilt cdylib for 5 target triples alongside the CLI on every GitHub Release.

**FFI surface**
- Panic-unwind cdylib boundary via `trap_panic` / `trap_panic_ptr` / `trap_panic_const_ptr` / `trap_panic_void`
- Runtime main-thread enforcement on every macOS-sensitive entrypoint; TLS errno-style last-error lifetime
- Every `#[repr(i32)]` field validated at the C boundary via `try_from_c_enum!` — arbitrary bit patterns return `ErrInvalidArgs` without UB
- BFS flat-tree layout with `child_start` / `child_count`; iterative traversal
- Opaque list handles (`AdAppList`, `AdWindowList`, `AdSurfaceList`, `AdNotificationList`); opaque `AdImageBuffer` with `_data` / `_size` / `_width` / `_height` / `_format` accessors
- `AdNativeHandle` single-owner single-thread contract; zero-on-free makes double-call deterministic
- Fail-closed UTF-8 for optional filter pointers (`try_c_to_string` tri-state)
- `AdTreeOptions` fully honored in `ad_get_tree` (include_bounds / interactive_only / compact)
- Verified notification action identity (`NotificationIdentity` fingerprint) — refuses to press if NC reordered between list and act

**Release pipeline**
- New `build-ffi` matrix in `.github/workflows/release.yml` producing `libagent_desktop_ffi.{dylib,so,dll}` tarballs for aarch64/x86_64-apple-darwin, x86_64/aarch64-unknown-linux-gnu, x86_64-pc-windows-msvc
- macOS `install_name = @rpath/libagent_desktop_ffi.dylib` baked in by `build.rs` and CI-verified via `otool -D`
- `actions/attest-build-provenance@v4.1.0` — keyless Sigstore provenance over every release artifact; `gh attestation verify`
- `checksums.txt` covers both CLI and FFI assets; asset-count assertion bumped 3 → 8
- npm package stays CLI-only; Python/Swift/Go/Ruby/Node/C hosts pull dylib tarball directly from the Release
- README gains a "Language bindings (FFI)" section with platform→artifact table

**Docs**
- `skills/agent-desktop-ffi/` — SKILL.md + build-and-link.md + ownership.md + threading.md + error-handling.md
- `docs/solutions/best-practices/` — two new solution docs (deterministic build-artifact marker, identity fingerprint against OS reorder)

**Verification**
- `cargo clippy --all-targets -- -D warnings` — clean
- `cargo test --lib --workspace` — 139 passed (5 suites)
- `cargo test -p agent-desktop-ffi --tests` — 87 passed (4 suites, including new `c_header_compile` C-ABI harness)
- macOS PR CI green on multiple runs; latest: 24561160455
- Python `ctypes.CDLL` round-trip from a freshly extracted FFI tarball confirmed; adapter lifecycle + `ad_list_apps` enumeration clean; `install_name` survives the tarball

Closes #22 (superseded).
2026-04-17 04:00:43 -07:00

124 lines
4.3 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') }}
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') }}-${{ hashFiles('**/*.rs') }}
restore-keys: |
${{ runner.os }}-build-ci-${{ hashFiles('**/Cargo.toml') }}-
${{ runner.os }}-build-ci-
- name: Check dependency isolation
run: |
if cargo tree -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 --all-targets -- -D warnings
- name: Unit tests
run: cargo test --lib --workspace
# 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 -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 --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 --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"