From 376e94e47aeffe69771121d1f2b9ab5224b31af0 Mon Sep 17 00:00:00 2001 From: Jack Amadeo Date: Fri, 27 Mar 2026 07:36:19 -0400 Subject: [PATCH] chore(aaif): Switch macOS code signing (#8076) --- .github/actions/apple-codesign/action.yml | 53 ++++++++++++ .github/workflows/bundle-desktop-intel.yml | 93 +++++----------------- .github/workflows/bundle-desktop.yml | 93 +++++----------------- .github/workflows/release.yml | 11 ++- ui/desktop/forge.config.ts | 31 ++++++-- 5 files changed, 122 insertions(+), 159 deletions(-) create mode 100644 .github/actions/apple-codesign/action.yml diff --git a/.github/actions/apple-codesign/action.yml b/.github/actions/apple-codesign/action.yml new file mode 100644 index 00000000..cba6858c --- /dev/null +++ b/.github/actions/apple-codesign/action.yml @@ -0,0 +1,53 @@ +name: 'Apple Certificate Keychain Setup' +description: 'Import a Developer ID certificate into a temporary keychain for Electron Forge signing' + +inputs: + certificate-base64: + description: 'Base64-encoded Developer ID Application .p12 certificate' + required: true + certificate-password: + description: 'Password for the .p12 certificate' + required: true + +outputs: + keychain-path: + description: 'Path to the temporary keychain' + value: ${{ steps.import-cert.outputs.keychain-path }} + +runs: + using: 'composite' + steps: + - name: Import Apple certificate + id: import-cert + shell: bash + env: + APPLE_CERTIFICATE_BASE64: ${{ inputs.certificate-base64 }} + APPLE_CERTIFICATE_PASSWORD: ${{ inputs.certificate-password }} + run: | + set -e + + CERTIFICATE_PATH="$RUNNER_TEMP/certificate.p12" + KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db" + KEYCHAIN_PASSWORD="$(openssl rand -hex 16)" + + echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > "$CERTIFICATE_PATH" + + security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH" + security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + security import "$CERTIFICATE_PATH" \ + -k "$KEYCHAIN_PATH" \ + -P "$APPLE_CERTIFICATE_PASSWORD" \ + -T /usr/bin/codesign + + security set-key-partition-list \ + -S apple-tool:,apple: \ + -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" + + security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain-db + + echo "keychain-path=$KEYCHAIN_PATH" >> "$GITHUB_OUTPUT" + echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV" + + rm -f "$CERTIFICATE_PATH" diff --git a/.github/workflows/bundle-desktop-intel.yml b/.github/workflows/bundle-desktop-intel.yml index 8309efd7..674e543c 100644 --- a/.github/workflows/bundle-desktop-intel.yml +++ b/.github/workflows/bundle-desktop-intel.yml @@ -25,9 +25,11 @@ on: type: string required: false default: '' - secrets: - OSX_CODESIGN_ROLE: + environment: + description: 'GitHub Environment containing signing secrets (e.g. "production"). Leave empty to skip.' required: false + type: string + default: '' name: Reusable workflow to bundle desktop app for Intel Mac @@ -35,6 +37,7 @@ jobs: bundle-desktop-intel: runs-on: macos-latest name: Bundle Desktop App on Intel macOS + environment: ${{ inputs.environment || '' }} env: MACOSX_DEPLOYMENT_TARGET: "12.0" permissions: @@ -117,11 +120,22 @@ jobs: jq '.build.mac.target[0].arch = "x64"' package.json > package.json.tmp && mv package.json.tmp package.json working-directory: ui/desktop + - name: Import Apple signing certificate + if: ${{ inputs.signing }} + uses: ./.github/actions/apple-codesign + with: + certificate-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + # Check disk space before bundling - name: Check disk space before bundling run: df -h - name: Build App + env: + APPLE_ID: ${{ inputs.signing && secrets.APPLE_ID || '' }} + APPLE_ID_PASSWORD: ${{ inputs.signing && secrets.APPLE_ID_PASSWORD || '' }} + APPLE_TEAM_ID: ${{ inputs.signing && secrets.APPLE_TEAM_ID || '' }} run: | source ../../bin/activate-hermit attempt=0 @@ -138,80 +152,13 @@ jobs: fi working-directory: ui/desktop - - name: Configure AWS credentials - if: ${{ inputs.signing }} - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 - with: - role-to-assume: "${{ secrets.OSX_CODESIGN_ROLE }}" - aws-region: us-west-2 - - - name: Codesigning and Notarization - if: ${{ inputs.signing }} + - name: Clean up signing keychain + if: always() run: | - set -e - - echo "⬆️ uploading unsigned app" - source_job_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - unsigned_url="s3://block-goose-artifacts-bucket-production/unsigned/goose-${GITHUB_SHA}-${{ github.run_id }}-intel.zip" - - zip -q -u -r out/Goose-darwin-x64/Goose_intel_mac.zip entitlements.plist - - # upload unsigned goose to transfer bucket so it can be passed to lambda - aws s3 cp --quiet out/Goose-darwin-x64/Goose_intel_mac.zip "${unsigned_url}" - - # begin signing - echo "🚀 launching signing process" - aws lambda invoke \ - --function-name codesign_helper \ - --cli-binary-format raw-in-base64-out \ - --payload "{\"source_s3_url\": \"${unsigned_url}\", \"source_job_url\": \"${source_job_url}\"}" \ - response.json > /dev/null - - if [ "$(jq -r .statusCode response.json)" != "200" ]; then - echo "⚠️ lambda function did not return expected status code" - exit 1 + if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then + security delete-keychain "$KEYCHAIN_PATH" || true fi - build_number="$(jq -r .body.build_number response.json)" - - start_time=$(date +%s) - - while sleep 30; do - aws lambda invoke \ - --function-name codesign_helper \ - --cli-binary-format raw-in-base64-out \ - --payload "{\"source_s3_url\": \"${unsigned_url}\", \"build_number\": \"${build_number}\"}" \ - response.json > /dev/null - - if [ "$(jq -r .statusCode response.json)" != "200" ]; then - echo "⚠️ signing request returned unexpected response code $(jq -r .statusCode response.json):" - jq . response.json - exit 1 - fi - - state="$(jq -r .body.state response.json)" - - if [ "${state}" == "completed" ]; then - echo "✅ signing complete ($(($(date +%s) - start_time))s)" - break - fi - - if [ $(($(date +%s) - start_time)) -ge 3600 ]; then - echo "⚠️ timed out ($(($(date +%s) - start_time))s)" - exit 1 - fi - - echo "⏲️ waiting for signing to complete (${state}: $(($(date +%s) - start_time))s)" - done - - # parse lambda response - signed_url=$(jq -r .body.destination_url response.json) - - # download the signed app from S3 - echo "⬇️ downloading signed app" - aws s3 cp --quiet "${signed_url}" out/Goose-darwin-x64/Goose_intel_mac.zip - working-directory: ui/desktop - - name: Final cleanup before artifact upload run: | echo "Performing final cleanup..." diff --git a/.github/workflows/bundle-desktop.yml b/.github/workflows/bundle-desktop.yml index 1bde2fee..eeeaf649 100644 --- a/.github/workflows/bundle-desktop.yml +++ b/.github/workflows/bundle-desktop.yml @@ -27,9 +27,11 @@ on: required: false type: string default: '' - secrets: - OSX_CODESIGN_ROLE: + environment: + description: 'GitHub Environment containing signing secrets (e.g. "signing"). Leave empty to skip.' required: false + type: string + default: '' name: Reusable workflow to bundle desktop app @@ -37,6 +39,7 @@ jobs: bundle-desktop: runs-on: macos-latest name: Bundle Desktop App on macOS + environment: ${{ inputs.environment || '' }} env: MACOSX_DEPLOYMENT_TARGET: "12.0" permissions: @@ -149,11 +152,22 @@ jobs: run: source ../../bin/activate-hermit && pnpm install --frozen-lockfile working-directory: ui/desktop + - name: Import Apple signing certificate + if: ${{ inputs.signing }} + uses: ./.github/actions/apple-codesign + with: + certificate-base64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} + certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + # Check disk space before bundling - name: Check disk space before bundling run: df -h - name: Build App + env: + APPLE_ID: ${{ inputs.signing && secrets.APPLE_ID || '' }} + APPLE_ID_PASSWORD: ${{ inputs.signing && secrets.APPLE_ID_PASSWORD || '' }} + APPLE_TEAM_ID: ${{ inputs.signing && secrets.APPLE_TEAM_ID || '' }} run: | source ../../bin/activate-hermit attempt=0 @@ -170,80 +184,13 @@ jobs: fi working-directory: ui/desktop - - name: Configure AWS credentials - if: ${{ inputs.signing }} - uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 - with: - role-to-assume: "${{ secrets.OSX_CODESIGN_ROLE }}" - aws-region: us-west-2 - - - name: Codesigning and Notarization - if: ${{ inputs.signing }} + - name: Clean up signing keychain + if: always() run: | - set -e - - echo "⬆️ uploading unsigned app" - source_job_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" - unsigned_url="s3://block-goose-artifacts-bucket-production/unsigned/goose-${GITHUB_SHA}-${{ github.run_id }}-arm64.zip" - - zip -q -u -r out/Goose-darwin-arm64/Goose.zip entitlements.plist - - # upload unsigned goose to transfer bucket so it can be passed to lambda - aws s3 cp --quiet out/Goose-darwin-arm64/Goose.zip "${unsigned_url}" - - # begin signing - echo "🚀 launching signing process" - aws lambda invoke \ - --function-name codesign_helper \ - --cli-binary-format raw-in-base64-out \ - --payload "{\"source_s3_url\": \"${unsigned_url}\", \"source_job_url\": \"${source_job_url}\"}" \ - response.json > /dev/null - - if [ "$(jq -r .statusCode response.json)" != "200" ]; then - echo "⚠️ lambda function did not return expected status code" - exit 1 + if [ -n "$KEYCHAIN_PATH" ] && [ -f "$KEYCHAIN_PATH" ]; then + security delete-keychain "$KEYCHAIN_PATH" || true fi - build_number="$(jq -r .body.build_number response.json)" - - start_time=$(date +%s) - - while sleep 30; do - aws lambda invoke \ - --function-name codesign_helper \ - --cli-binary-format raw-in-base64-out \ - --payload "{\"source_s3_url\": \"${unsigned_url}\", \"build_number\": \"${build_number}\"}" \ - response.json > /dev/null - - if [ "$(jq -r .statusCode response.json)" != "200" ]; then - echo "⚠️ signing request returned unexpected response code $(jq -r .statusCode response.json):" - jq . response.json - exit 1 - fi - - state="$(jq -r .body.state response.json)" - - if [ "${state}" == "completed" ]; then - echo "✅ signing complete ($(($(date +%s) - start_time))s)" - break - fi - - if [ $(($(date +%s) - start_time)) -ge 3600 ]; then - echo "⚠️ timed out ($(($(date +%s) - start_time))s)" - exit 1 - fi - - echo "⏲️ waiting for signing to complete (${state}: $(($(date +%s) - start_time))s)" - done - - # parse lambda response - signed_url=$(jq -r .body.destination_url response.json) - - # download the signed app from S3 - echo "⬇️ downloading signed app" - aws s3 cp --quiet "${signed_url}" out/Goose-darwin-arm64/Goose.zip - working-directory: ui/desktop - - name: Final cleanup before artifact upload run: | echo "Performing final cleanup..." diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b901eb83..fc4a40f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,9 +8,8 @@ on: name: Release -# Permissions needed for AWS OIDC authentication in called workflows permissions: - id-token: write # Required for AWS OIDC authentication in called workflow + id-token: write # Required for Sigstore OIDC signing and AWS OIDC (Windows signing) contents: write # Required for creating releases and by actions/checkout actions: read # May be needed for some workflows attestations: write # Required for SLSA build provenance attestations @@ -50,8 +49,8 @@ jobs: contents: read with: signing: true - secrets: - OSX_CODESIGN_ROLE: ${{ secrets.OSX_CODESIGN_ROLE }} + environment: signing + secrets: inherit # ------------------------------------------------------------ # 4) Bundle Desktop App (macOS) @@ -63,8 +62,8 @@ jobs: contents: read with: signing: true - secrets: - OSX_CODESIGN_ROLE: ${{ secrets.OSX_CODESIGN_ROLE }} + environment: signing + secrets: inherit # ------------------------------------------------------------ # 5) Bundle Desktop App (Linux) diff --git a/ui/desktop/forge.config.ts b/ui/desktop/forge.config.ts index 879dc0d0..dcd327c2 100644 --- a/ui/desktop/forge.config.ts +++ b/ui/desktop/forge.config.ts @@ -33,11 +33,28 @@ let cfg = { }, ], // Usage descriptions for macOS TCC (Transparency, Consent, and Control) - NSCalendarsUsageDescription: 'Goose needs access to your calendars to help manage and query calendar events.', - NSRemindersUsageDescription: 'Goose needs access to your reminders to help manage and query reminders.', + NSCalendarsUsageDescription: + 'Goose needs access to your calendars to help manage and query calendar events.', + NSRemindersUsageDescription: + 'Goose needs access to your reminders to help manage and query reminders.', }, }; +// macOS code signing and notarization via Electron Forge +// Activated when APPLE_TEAM_ID is set (CI signing builds) +if (process.env.APPLE_TEAM_ID) { + cfg.osxSign = { + keychain: process.env.KEYCHAIN_PATH || undefined, + entitlements: 'entitlements.plist', + 'entitlements-inherit': 'entitlements.plist', + }; + cfg.osxNotarize = { + appleId: process.env.APPLE_ID, + appleIdPassword: process.env.APPLE_ID_PASSWORD, + teamId: process.env.APPLE_TEAM_ID, + }; +} + module.exports = { packagerConfig: cfg, rebuildConfig: {}, @@ -103,7 +120,7 @@ module.exports = { id: 'io.github.block.Goose', categories: ['Development'], icon: { - 'scalable': 'src/images/icon.svg', + scalable: 'src/images/icon.svg', '512x512': 'src/images/icon-512.png', }, homepage: 'https://block.github.io/goose/', @@ -119,9 +136,9 @@ module.exports = { 'mkdir -p /app/lib', // Point to the actual library in the 25.08 runtime // We use a wildcard to handle multi-arch paths (x86_64-linux-gnu, etc) - 'ln -s $(find /usr/lib -name "libbz2.so.1" | head -n 1) /app/lib/libbz2.so.1.0' - ] - } + 'ln -s $(find /usr/lib -name "libbz2.so.1" | head -n 1) /app/lib/libbz2.so.1.0', + ], + }, ], finishArgs: [ '--share=ipc', @@ -134,7 +151,7 @@ module.exports = { '--socket=session-bus', '--socket=system-bus', // This ensures the app looks in our shim folder first - '--env=LD_LIBRARY_PATH=/app/lib' + '--env=LD_LIBRARY_PATH=/app/lib', ], }, },