Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 43 additions & 108 deletions .github/workflows/release-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,113 +13,7 @@ env:
CI: true

jobs:
build-macos-binaries:
runs-on: ${{ matrix.runner }}
environment: production-signing
permissions:
contents: read
env:
AGENTDESKTOP_VERSION: ${{ inputs.version }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
target: x86_64-apple-darwin
runner: macos-15-intel
- arch: arm64
target: aarch64-apple-darwin
runner: macos-26
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install Node
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: frontend/.nvmrc

- name: Install Rust target
uses: dtolnay/rust-toolchain@683b588c487a69d01f6c6ddf40f036ff682cc2c8 # master
with:
toolchain: "1.98"
targets: ${{ matrix.target }}

- name: Build frontend
shell: bash
working-directory: frontend
run: |
npm install --global pnpm@11.25.0
pnpm install --frozen-lockfile
pnpm --filter @agentdesktop/desktop-web build

- name: Build binary
shell: bash
run: cargo build --locked --release --package agentdesktop --target "${{ matrix.target }}"

- name: Import macOS signing identity
shell: bash
env:
APPLE_APPLICATION_CERTIFICATE: ${{ secrets.APPLE_APPLICATION_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_INSTALLER_CERTIFICATE: ${{ secrets.APPLE_INSTALLER_CERTIFICATE }}
APPLE_INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERTIFICATE_PASSWORD }}
run: bash .github/scripts/import-macos-signing.sh

- name: Sign macOS binary
shell: bash
env:
TARGET: ${{ matrix.target }}
run: |
binary="target/${TARGET}/release/agentdesktop"
codesign --force --sign "$APPLE_SIGNING_IDENTITY" \
--identifier dev.agentdesktop.tray --options runtime --timestamp "$binary"
codesign --verify --strict --verbose=2 \
-R="anchor apple generic and identifier \"dev.agentdesktop.tray\" and certificate leaf = H\"${APPLE_SIGNING_IDENTITY}\"" "$binary"

- name: Notarize macOS binary
shell: bash
env:
TARGET: ${{ matrix.target }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ vars.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ vars.APPLE_API_ISSUER }}
run: |
# Bare binaries cannot be stapled; Gatekeeper looks up their ticket online.
archive="${RUNNER_TEMP}/agentdesktop-notarize.zip"
ditto -c -k "target/${TARGET}/release/agentdesktop" "$archive"
bash .github/scripts/notarize-macos.sh "$archive"

- name: Stage binary and checksum
shell: bash
env:
OUTPUT_NAME: agentdesktop-darwin-${{ matrix.arch }}
run: |
mkdir -p release
cp "target/${{ matrix.target }}/release/agentdesktop" "release/${OUTPUT_NAME}"
(cd release && shasum -a 256 "${OUTPUT_NAME}" > "${OUTPUT_NAME}.sha256")

- name: Remove macOS signing identity
if: always()
timeout-minutes: 2
shell: bash
run: |
# Delete exported files first, even if keychain deletion later fails.
rm -f "${RUNNER_TEMP}/agentdesktop-signing.p12" "${RUNNER_TEMP}/agentdesktop-notary.p8"
if [ -f "${RUNNER_TEMP}/agentdesktop-signing.keychain-db" ]; then
security delete-keychain "${RUNNER_TEMP}/agentdesktop-signing.keychain-db"
fi

- name: Upload binary
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-binary-darwin-${{ matrix.arch }}
path: release/agentdesktop-*
if-no-files-found: error

build-macos-installers:
build-macos:
runs-on: ${{ matrix.runner }}
environment: production-signing
permissions:
Expand Down Expand Up @@ -186,6 +80,29 @@ jobs:
-R="anchor apple generic and identifier \"dev.agentdesktop.tray\" and certificate leaf = H\"${APPLE_SIGNING_IDENTITY}\"" \
"$app/Contents/MacOS/agentdesktop"

- name: Sign and notarize standalone binary
shell: bash
env:
TARGET: ${{ matrix.target }}
OUTPUT_NAME: agentdesktop-darwin-${{ matrix.arch }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
APPLE_API_KEY_ID: ${{ vars.APPLE_API_KEY_ID }}
APPLE_API_ISSUER: ${{ vars.APPLE_API_ISSUER }}
run: |
mkdir -p release/binary
binary="release/binary/${OUTPUT_NAME}"
cp "target/${TARGET}/release/bundle/macos/agentdesktop.app/Contents/MacOS/agentdesktop" "$binary"
# The bundle signature seals Info.plist and resources, so the extracted copy needs its own.
codesign --force --sign "$APPLE_SIGNING_IDENTITY" \
--identifier dev.agentdesktop.tray --options runtime --timestamp "$binary"
codesign --verify --strict --verbose=2 \
-R="anchor apple generic and identifier \"dev.agentdesktop.tray\" and certificate leaf = H\"${APPLE_SIGNING_IDENTITY}\"" "$binary"
# Bare binaries cannot be stapled; Gatekeeper looks up their ticket online.
archive="${RUNNER_TEMP}/agentdesktop-notarize.zip"
ditto -c -k "$binary" "$archive"
bash .github/scripts/notarize-macos.sh "$archive"
(cd release/binary && shasum -a 256 "${OUTPUT_NAME}" > "${OUTPUT_NAME}.sha256")

- name: Stage PKG
shell: bash
env:
Expand All @@ -207,7 +124,18 @@ jobs:
APPLE_API_ISSUER: ${{ vars.APPLE_API_ISSUER }}
run: |
bash .github/scripts/notarize-macos.sh "release/${OUTPUT_NAME}"
xcrun stapler staple "release/${OUTPUT_NAME}"
# Ticket lookup via CloudKit fails transiently; retry stapling without resubmitting.
for attempt in 1 2 3 4 5; do
if xcrun stapler staple "release/${OUTPUT_NAME}"; then
break
fi
if (( attempt == 5 )); then
echo "Stapling failed after ${attempt} attempts" >&2
exit 1
fi
sleep $((attempt * 15))
done
xcrun stapler validate "release/${OUTPUT_NAME}"

- name: Checksum PKG
shell: bash
Expand Down Expand Up @@ -281,6 +209,13 @@ jobs:
security delete-keychain "${RUNNER_TEMP}/agentdesktop-signing.keychain-db"
fi

- name: Upload binary
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-binary-darwin-${{ matrix.arch }}
path: release/binary/agentdesktop-darwin-*
if-no-files-found: error

- name: Upload PKG
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
Expand Down
4 changes: 2 additions & 2 deletions frontend/desktop/scripts/package-macos.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,8 @@ const signingCleanupSteps = [
return { body, script: run[1].replace(/^ {10}/gm, "") };
});

test("both release signing cleanups always run with a short timeout", () => {
assert.equal(signingCleanupSteps.length, 2);
test("release signing cleanup always runs with a short timeout", () => {
assert.equal(signingCleanupSteps.length, 1);
for (const { body } of signingCleanupSteps) {
assert.match(body, /^ {8}if: always\(\)/m);
const timeout = body.match(/^ {8}timeout-minutes: (\d+)$/m);
Expand Down