agent-desktop/.github/workflows/release.yml
Lahfir 1291a9cdbf
refactor!: unify command execution contracts
Unify CLI and batch dispatch around the typed command path, centralize command policy and ref resolution, harden macOS action verification, split command tests from implementation, and add package/release guardrails.

BREAKING CHANGE: CLI and batch execution now share the typed command path and current command argument contracts.

BREAKING CHANGE: Ref-consuming commands use snapshot-scoped refs; deterministic consumers should pass snapshot_id and handle SNAPSHOT_NOT_FOUND.

BREAKING CHANGE: permissions and status now return PermissionReport fields for accessibility, screen_recording, and automation instead of a single boolean status.

BREAKING CHANGE: PermissionState gains NotRequired; macOS automation now reports not_required instead of unknown.

BREAKING CHANGE: right-click now separates action success from menu verification; consumers should inspect menu or menu_probe instead of assuming every right-click returns an inline menu.

BREAKING CHANGE: focus-window now confirms OS focus and returns ACTION_FAILED when focus does not settle; data.focused.is_focused is true on success.

BREAKING CHANGE: PlatformAdapter::execute_action now takes ActionRequest, and permission probing uses permission_report/request_permissions.

BREAKING CHANGE: FFI ad_execute_action now defaults to headless policy. Consumers that need focus fallback or cursor-moving behavior must call ad_execute_action_with_policy with AD_POLICY_KIND_FOCUS_FALLBACK or AD_POLICY_KIND_PHYSICAL.

BREAKING CHANGE: FFI ad_check_permissions no longer treats unknown accessibility permission as success; stub-style unknown probes return ERR_PLATFORM_NOT_SUPPORTED and macOS ambiguous unknown returns ERR_INTERNAL with last-error detail.

BREAKING CHANGE: JSON response envelopes now report version 2.0; parsers pinned to 1.0 must branch or update.

BREAKING CHANGE: focus now uses accessibility focus without cursor movement; callers that need physical focus must use explicit mouse or physical-policy paths.

BREAKING CHANGE: chain execution deadlines now return TIMEOUT instead of ACTION_FAILED when the target app does not respond before the chain deadline.
2026-05-19 18:27:08 -07:00

340 lines
12 KiB
YAML

name: Release
on:
push:
branches: [main]
# One release run per commit — never cancel, just queue.
concurrency:
group: release-${{ github.sha }}
# Deny all permissions at workflow level; each job declares only what it needs.
permissions: {}
env:
CARGO_TERM_COLOR: always
jobs:
release-please:
name: Release Please
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.major }}.${{ steps.release.outputs.minor }}.${{ steps.release.outputs.patch }}
steps:
- uses: googleapis/release-please-action@5c625bfb5d1ff62eadeeb3772007f7f66fdcf071 # v4.4.1
id: release
with:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
build:
name: Build (${{ matrix.target }})
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
runs-on: macos-latest
timeout-minutes: 30
permissions:
contents: read
strategy:
fail-fast: false
matrix:
target:
- aarch64-apple-darwin
- x86_64-apple-darwin
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.release-please.outputs.tag_name }}
- name: Install Rust toolchain
run: |
rustup show
rustup target add ${{ matrix.target }}
- 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 }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.toml', 'Cargo.lock') }}
restore-keys: ${{ runner.os }}-${{ matrix.target }}-cargo-
- name: Build release binary
run: cargo build --locked --release --target ${{ matrix.target }}
- name: Check binary size
run: |
SIZE=$(stat -f%z target/${{ matrix.target }}/release/agent-desktop)
LIMIT=$((15 * 1024 * 1024))
echo "Binary size: $(du -sh target/${{ matrix.target }}/release/agent-desktop | cut -f1)"
if [ "$SIZE" -gt "$LIMIT" ]; then
echo "FAIL: binary exceeds 15MB limit (${SIZE} bytes)"
exit 1
fi
- name: Create tarball
run: |
VERSION=${{ needs.release-please.outputs.version }}
TARBALL="agent-desktop-v${VERSION}-${{ matrix.target }}.tar.gz"
tar -czf "${TARBALL}" -C target/${{ matrix.target }}/release agent-desktop
shasum -a 256 "${TARBALL}" > "${TARBALL}.sha256"
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: binary-${{ matrix.target }}
path: |
agent-desktop-v*.tar.gz
agent-desktop-v*.tar.gz.sha256
retention-days: 1
# FFI cdylib for language-binding consumers (Python/Swift/Go/Ruby/Node/C).
# Ships alongside CLI tarballs on the same release tag; npm stays CLI-only.
build-ffi:
name: Build FFI (${{ matrix.target }})
needs: release-please
if: needs.release-please.outputs.release_created == 'true'
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
- target: aarch64-apple-darwin
runner: macos-latest
archive: tar.gz
lib_name: libagent_desktop_ffi.dylib
- target: x86_64-apple-darwin
runner: macos-latest
archive: tar.gz
lib_name: libagent_desktop_ffi.dylib
- target: x86_64-unknown-linux-gnu
runner: ubuntu-22.04
archive: tar.gz
lib_name: libagent_desktop_ffi.so
- target: aarch64-unknown-linux-gnu
runner: ubuntu-22.04-arm
archive: tar.gz
lib_name: libagent_desktop_ffi.so
- target: x86_64-pc-windows-msvc
runner: windows-latest
archive: zip
lib_name: agent_desktop_ffi.dll
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.release-please.outputs.tag_name }}
- name: Install Rust toolchain
run: |
rustup show
rustup target add ${{ matrix.target }}
- 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 }}-${{ matrix.target }}-ffi-cargo-${{ hashFiles('**/Cargo.toml', 'Cargo.lock') }}
restore-keys: ${{ runner.os }}-${{ matrix.target }}-ffi-cargo-
- name: Build FFI cdylib (release-ffi profile)
run: cargo build --locked --profile release-ffi -p agent-desktop-ffi --target ${{ matrix.target }}
# build.rs bakes install_name=@rpath/... ; a regression here silently
# breaks Swift/SPM consumers, so verify before shipping.
- name: Verify macOS install_name
if: runner.os == 'macOS'
shell: bash
run: |
ACTUAL=$(otool -D target/${{ matrix.target }}/release-ffi/${{ matrix.lib_name }} | tail -1)
EXPECTED="@rpath/libagent_desktop_ffi.dylib"
if [ "$ACTUAL" != "$EXPECTED" ]; then
echo "FAIL: install_name is '$ACTUAL', expected '$EXPECTED'"
exit 1
fi
echo "OK: install_name = $ACTUAL"
- name: Stage tarball contents
shell: bash
run: |
VERSION=${{ needs.release-please.outputs.version }}
STAGE="agent-desktop-ffi-v${VERSION}-${{ matrix.target }}"
mkdir -p "$STAGE/lib" "$STAGE/include"
cp "target/${{ matrix.target }}/release-ffi/${{ matrix.lib_name }}" "$STAGE/lib/"
cp crates/ffi/include/agent_desktop.h "$STAGE/include/"
cp LICENSE "$STAGE/"
cat > "$STAGE/README.md" <<EOF
# agent-desktop FFI cdylib — v${VERSION} (${{ matrix.target }})
- \`lib/${{ matrix.lib_name }}\` — shared library
- \`include/agent_desktop.h\` — committed C ABI header
- \`LICENSE\` — Apache-2.0
Docs: https://github.com/lahfir/agent-desktop/tree/main/skills/agent-desktop-ffi
macOS \`install_name\` is \`@rpath/libagent_desktop_ffi.dylib\` — linking
consumers need \`-rpath\` pointing at the dylib's directory; \`dlopen\` /
ctypes callers resolve by path and need nothing.
EOF
echo "STAGE=$STAGE" >> "$GITHUB_ENV"
- name: Create archive (Unix)
if: runner.os != 'Windows'
shell: bash
run: |
tar -czf "${STAGE}.tar.gz" "${STAGE}"
shasum -a 256 "${STAGE}.tar.gz" > "${STAGE}.tar.gz.sha256"
- name: Create archive (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
Compress-Archive -Path "${env:STAGE}" -DestinationPath "${env:STAGE}.zip"
$hash = (Get-FileHash "${env:STAGE}.zip" -Algorithm SHA256).Hash.ToLower()
"$hash ${env:STAGE}.zip" | Out-File -Encoding ascii "${env:STAGE}.zip.sha256"
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ffi-${{ matrix.target }}
path: |
agent-desktop-ffi-v*.tar.gz
agent-desktop-ffi-v*.tar.gz.sha256
agent-desktop-ffi-v*.zip
agent-desktop-ffi-v*.zip.sha256
retention-days: 1
publish-github:
name: Publish to GitHub Release
needs: [release-please, build, build-ffi]
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write
id-token: write # Sigstore OIDC exchange
attestations: write # write provenance bundle back to GitHub
steps:
- name: Download all artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: "{binary,ffi}-*"
merge-multiple: true
- name: Create checksums file
run: |
cat ./*.sha256 > checksums.txt
cat checksums.txt
- name: Upload assets to GitHub Release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
gh release upload ${{ needs.release-please.outputs.tag_name }} \
./*.tar.gz ./*.zip checksums.txt \
--clobber
# Keyless Sigstore provenance. Verify with:
# gh attestation verify <file> --repo lahfir/agent-desktop
- name: Attest build provenance
uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0
with:
subject-path: |
*.tar.gz
*.zip
checksums.txt
publish-npm:
name: Publish to npm
needs: [release-please, publish-github]
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.release-please.outputs.tag_name }}
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '24'
registry-url: https://registry.npmjs.org
- name: Verify GitHub Release assets
env:
GH_TOKEN: ${{ github.token }}
run: |
# Expected: 2 CLI tarballs + 4 FFI tarballs + 1 FFI zip + checksums = 8.
ASSETS=$(gh release view ${{ needs.release-please.outputs.tag_name }} --json assets --jq '.assets | length')
if [ "$ASSETS" -lt 8 ]; then
echo "FAIL: expected at least 8 assets, found ${ASSETS}"
exit 1
fi
echo "OK: ${ASSETS} assets found on release"
- name: Prepare npm package
run: |
VERSION=${{ needs.release-please.outputs.version }}
cd npm
node -e "
const pkg = require('./package.json');
pkg.version = '${VERSION}';
require('fs').writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n');
"
echo "npm package version set to ${VERSION}"
cp ../README.md README.md
cp ../CHANGELOG.md CHANGELOG.md 2>/dev/null || true
- name: Publish to npm
run: npm publish --provenance --access public
working-directory: npm
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
publish-skills:
name: Publish Skills to ClawHub
needs: [release-please]
if: needs.release-please.outputs.release_created == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.release-please.outputs.tag_name }}
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
with:
node-version: '24'
- name: Install ClawHub CLI
run: npm i -g clawhub
- name: Authenticate with ClawHub
run: clawhub login --token "$CLAWHUB_TOKEN" --no-browser
env:
CLAWHUB_TOKEN: ${{ secrets.CLAWHUB_TOKEN }}
- name: Publish all skills to ClawHub
run: |
clawhub sync \
--root skills/ \
--all \
--bump patch \
--changelog "Release ${{ needs.release-please.outputs.tag_name }}"