From a951494489dd60fa09c360d1efc50731cd9e6ee1 Mon Sep 17 00:00:00 2001 From: zhayujie Date: Mon, 6 Jul 2026 10:19:54 +0800 Subject: [PATCH] fix(desktop): notarize the .app zip --- .github/workflows/release.yml | 60 ++++------------------ .gitignore | 1 - desktop/build/notarize-dmg.sh | 94 ----------------------------------- desktop/electron-builder.js | 14 +++--- 4 files changed, 18 insertions(+), 151 deletions(-) delete mode 100755 desktop/build/notarize-dmg.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e606f802..b1e40be1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -108,6 +108,10 @@ jobs: # is the correct state for unsigned builds. MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }} MAC_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }} + # electron-builder's built-in notarization reads these. + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} WIN_CSC_LINK: ${{ secrets.WIN_CSC_LINK }} WIN_CSC_KEY_PASSWORD: ${{ secrets.WIN_CSC_KEY_PASSWORD }} run: | @@ -128,57 +132,15 @@ jobs: # Never let electron-builder publish: our publish target is a generic # (read-only) feed served from R2/D1, which it can't upload to. We mirror # installers to R2 and register them in D1 ourselves (publish-r2 job). - # `--publish never` still emits the latest*.yml files we parse for sha512. - # Use the dynamic config (electron-builder.js) so mac.binaries is - # populated with the embedded backend's Mach-O files for signing. - # electron-builder signs but does NOT notarize (notarize:false); the - # dedicated step below notarizes + staples with retry. + # `--publish never` still emits the latest*.yml files. + # Use the dynamic config (electron-builder.js): it populates + # mac.binaries (backend Mach-O files to sign) and enables built-in + # notarization, which zips the .app and submits that (the path Apple + # actually accepts for this large bundle) then staples + packs the dmg. npx electron-builder ${{ matrix.eb_flags }} --config electron-builder.js --publish never - # Whether Mac signing secrets are present. `secrets` can't be used in a - # step `if:` directly, and step-level `env` isn't visible to that step's - # own `if:`, so surface it as an output for the notarize step to gate on. - - name: Check mac signing secrets - id: macsign - if: matrix.platform == 'mac' - shell: bash - env: - MAC_CSC_LINK: ${{ secrets.MAC_CSC_LINK }} - run: | - if [ -n "$MAC_CSC_LINK" ]; then - echo "enabled=true" >> "$GITHUB_OUTPUT" - else - echo "enabled=false" >> "$GITHUB_OUTPUT" - echo "::notice::MAC_CSC_LINK not set — skipping notarization (unsigned build)." - fi - - # Notarize + staple the dmg: submit ONCE, then poll that submission id - # until Accepted/Invalid (see notarize-dmg.sh — never resubmits). - # Skipped for Windows and for unsigned builds (no cert). - - name: Notarize macOS dmg - if: matrix.platform == 'mac' && steps.macsign.outputs.enabled == 'true' - working-directory: desktop - shell: bash - env: - APPLE_ID: ${{ secrets.APPLE_ID }} - APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - run: | - shopt -s nullglob - dmgs=(release/*.dmg) - if [ ${#dmgs[@]} -eq 0 ]; then - echo "::error::no dmg found to notarize" - exit 1 - fi - for dmg in "${dmgs[@]}"; do - bash build/notarize-dmg.sh "$dmg" - done - - # Always upload artifacts, even when notarization timed out. The - # notarization ticket is keyed to the dmg's hash on Apple's servers: if - # the submission is eventually Accepted, THIS exact dmg becomes valid - # as-is (download it, `xcrun stapler staple`, publish manually) — no - # rebuild needed. Rebuilding would change the hash and waste the ticket. + # Upload artifacts regardless of outcome, so a failed run still surfaces + # the built installers (and, on success, the notarized+stapled dmg). - name: Upload artifacts if: always() uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index b22aa395..287a69ca 100644 --- a/.gitignore +++ b/.gitignore @@ -55,7 +55,6 @@ desktop/build/* !desktop/build/requirements-desktop.txt !desktop/build/build-backend.sh !desktop/build/entitlements.mac.plist -!desktop/build/notarize-dmg.sh # Icon authoring scratch dir: intermediate assets used to produce the final # icons. Only the finished icons under desktop/resources/ should be committed. diff --git a/desktop/build/notarize-dmg.sh b/desktop/build/notarize-dmg.sh deleted file mode 100755 index caf2dd85..00000000 --- a/desktop/build/notarize-dmg.sh +++ /dev/null @@ -1,94 +0,0 @@ -#!/usr/bin/env bash -# -# Notarize + staple a macOS dmg. -# -# Submits ONCE and then polls that same submission id until it reaches a -# terminal state. Never resubmits: a previous version resubmitted on every -# wait timeout, which piled up duplicate "In Progress" submissions in Apple's -# queue and made everything slower. -# -# Notarization is fully automated on Apple's side (malware scan, no humans); -# most submissions finish in minutes, but large bundles or Apple-side backlog -# can take much longer, so we poll patiently instead of failing fast. -# -# Usage: notarize-dmg.sh -# Requires env: APPLE_ID, APPLE_APP_SPECIFIC_PASSWORD, APPLE_TEAM_ID -# Tunables: NOTARIZE_MAX_WAIT_MINUTES (default 180), NOTARIZE_POLL_SECONDS (default 60) -set -euo pipefail - -DMG="${1:?usage: notarize-dmg.sh }" - -: "${APPLE_ID:?APPLE_ID not set}" -: "${APPLE_APP_SPECIFIC_PASSWORD:?APPLE_APP_SPECIFIC_PASSWORD not set}" -: "${APPLE_TEAM_ID:?APPLE_TEAM_ID not set}" - -MAX_WAIT_MINUTES="${NOTARIZE_MAX_WAIT_MINUTES:-180}" -POLL_SECONDS="${NOTARIZE_POLL_SECONDS:-60}" - -auth=( - --apple-id "$APPLE_ID" - --password "$APPLE_APP_SPECIFIC_PASSWORD" - --team-id "$APPLE_TEAM_ID" -) - -json_field() { - # json_field — read a top-level string field from JSON on stdin. - python3 -c "import json,sys; print(json.load(sys.stdin).get('$1',''))" 2>/dev/null -} - -echo "==> Submitting for notarization: $DMG" -submit_out="$(xcrun notarytool submit "$DMG" "${auth[@]}" --no-wait --output-format json)" -submission_id="$(echo "$submit_out" | json_field id)" - -if [ -z "$submission_id" ]; then - echo "::error::could not parse submission id from notarytool output" - echo "$submit_out" - exit 1 -fi -echo "==> Submission id: $submission_id (uploaded OK, polling status...)" - -deadline=$(( $(date +%s) + MAX_WAIT_MINUTES * 60 )) -while :; do - # info can fail transiently (network); treat as unknown and keep polling. - status="$(xcrun notarytool info "$submission_id" "${auth[@]}" --output-format json 2>/dev/null | json_field status || true)" - echo "==> [$(date '+%H:%M:%S')] status: ${status:-}" - - case "$status" in - Accepted) - echo "==> Notarization Accepted" - break - ;; - Invalid|Rejected) - echo "::error::Notarization ${status}; fetching log for diagnostics" - xcrun notarytool log "$submission_id" "${auth[@]}" || true - exit 1 - ;; - esac - - if [ "$(date +%s)" -ge "$deadline" ]; then - echo "::error::Notarization still not finished after ${MAX_WAIT_MINUTES} minutes (id: $submission_id)." - echo "::error::NOT resubmitting. Check later with: xcrun notarytool info $submission_id" - exit 1 - fi - sleep "$POLL_SECONDS" -done - -echo "==> Stapling ticket into dmg" -staple_attempt=1 -while :; do - if xcrun stapler staple "$DMG"; then - echo "==> Staple OK" - break - fi - if [ "$staple_attempt" -ge 3 ]; then - echo "::error::Stapling failed after 3 attempts" - exit 1 - fi - echo "==> Staple failed, retrying in 15s..." - sleep 15 - staple_attempt=$(( staple_attempt + 1 )) -done - -echo "==> Verifying staple" -xcrun stapler validate "$DMG" -echo "==> Notarization + staple complete: $DMG" diff --git a/desktop/electron-builder.js b/desktop/electron-builder.js index d6f9bce1..ccbea773 100644 --- a/desktop/electron-builder.js +++ b/desktop/electron-builder.js @@ -71,13 +71,13 @@ function collectBackendBinaries() { if (process.platform === 'darwin') { const binaries = collectBackendBinaries() console.log(`[electron-builder.js] injecting ${binaries.length} backend binaries into mac.binaries`) - // Sign the app (+ backend binaries) here, but do NOT notarize inline. - // electron-builder runs `notarytool submit --wait`, which aborts the whole - // build on a single status-polling timeout (NSURLErrorDomain -1001) — a - // frequent transient failure on the runner -> Apple link. Notarization still - // happens: a dedicated CI step submits + staples the dmg with a retry loop, - // so a flaky poll retries instead of throwing away the signed build. - config.mac = { ...config.mac, binaries, notarize: false } + // Notarize via electron-builder's built-in flow, which zips the .app and + // submits THAT (then staples the .app and packs the dmg around it). + // Submitting the .app-zip is the path that actually gets Accepted by Apple's + // notary service; submitting the dmg directly got stuck "In Progress" + // indefinitely for this large PyInstaller bundle. notarize needs APPLE_ID / + // APPLE_APP_SPECIFIC_PASSWORD / APPLE_TEAM_ID in the environment. + config.mac = { ...config.mac, binaries, notarize: true } } module.exports = config