@@ -13,6 +13,10 @@ name: Release
1313# APPLE_CERTIFICATE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_TEAM_ID and for notarization either an App Store Connect
1414# API key (APPLE_API_ISSUER, APPLE_API_KEY = key id, APPLE_API_KEY_P8 = contents of the .p8) or APPLE_ID + APPLE_PASSWORD.
1515#
16+ # Official releases fail instead of shipping anything unsigned: the DMGs, the apps inside them, the update bundles and
17+ # the standalone macOS binaries are all checked with scripts/macos-verify.sh, and verify-downloads.yml re-checks what
18+ # usegodmode.com serves every morning (it opens an issue when Gatekeeper would refuse a download).
19+ #
1620# Required secrets for auto-update: TAURI_SIGNING_PRIVATE_KEY + TAURI_SIGNING_PRIVATE_KEY_PASSWORD (the key matching
1721# `plugins.updater.pubkey` in tauri.conf.json). They sign the update bundles and add latest.json to the release;
1822# installed apps pick the update up once the release is published.
9195 exit 1
9296 fi
9397
98+ - name : Check the Apple signing setup
99+ if : github.repository == 'codextde/godmode-bot'
100+ env :
101+ APPLE_CERTIFICATE : ${{ secrets.APPLE_CERTIFICATE }}
102+ APPLE_CERTIFICATE_PASSWORD : ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
103+ APPLE_SIGNING_IDENTITY : ${{ secrets.APPLE_SIGNING_IDENTITY }}
104+ APPLE_TEAM_ID : ${{ secrets.APPLE_TEAM_ID }}
105+ APPLE_API_ISSUER : ${{ secrets.APPLE_API_ISSUER }}
106+ APPLE_API_KEY : ${{ secrets.APPLE_API_KEY }}
107+ APPLE_API_KEY_P8 : ${{ secrets.APPLE_API_KEY_P8 }}
108+ run : |
109+ # An official release is never shipped unsigned: macOS would only offer to move it to the Trash.
110+ missing=0
111+ for name in APPLE_CERTIFICATE APPLE_CERTIFICATE_PASSWORD APPLE_SIGNING_IDENTITY APPLE_TEAM_ID APPLE_API_ISSUER APPLE_API_KEY APPLE_API_KEY_P8; do
112+ if [ -z "${!name}" ]; then echo "::error::the $name secret is missing (see the top of release.yml)"; missing=1; fi
113+ done
114+ [ "$missing" = 0 ] || exit 1
115+ [[ "$APPLE_SIGNING_IDENTITY" == "Developer ID Application: "*"($APPLE_TEAM_ID)" || "$APPLE_SIGNING_IDENTITY" =~ ^[0-9A-Fa-f]{40}$ ]] ||
116+ { echo "::error::APPLE_SIGNING_IDENTITY must be a Developer ID Application identity of team $APPLE_TEAM_ID"; exit 1; }
117+ printf '%s' "$APPLE_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/cert.p12"
118+ export PW="$APPLE_CERTIFICATE_PASSWORD"
119+ openssl pkcs12 -in "$RUNNER_TEMP/cert.p12" -passin env:PW -nokeys -clcerts -out "$RUNNER_TEMP/cert.pem" 2>/dev/null ||
120+ openssl pkcs12 -legacy -in "$RUNNER_TEMP/cert.p12" -passin env:PW -nokeys -clcerts -out "$RUNNER_TEMP/cert.pem" ||
121+ { echo "::error::APPLE_CERTIFICATE can't be opened with APPLE_CERTIFICATE_PASSWORD"; exit 1; }
122+ subject=$(openssl x509 -in "$RUNNER_TEMP/cert.pem" -noout -subject)
123+ until=$(openssl x509 -in "$RUNNER_TEMP/cert.pem" -noout -enddate | cut -d= -f2)
124+ rm -f "$RUNNER_TEMP/cert.p12"
125+ [[ "$subject" == *"$APPLE_TEAM_ID"* ]] || { echo "::error::APPLE_CERTIFICATE belongs to another team: $subject"; exit 1; }
126+ openssl x509 -in "$RUNNER_TEMP/cert.pem" -noout -checkend 0 >/dev/null ||
127+ { echo "::error::the Developer ID certificate expired on $until: create a new one on developer.apple.com and update APPLE_CERTIFICATE"; exit 1; }
128+ openssl x509 -in "$RUNNER_TEMP/cert.pem" -noout -checkend $((45 * 86400)) >/dev/null ||
129+ echo "::warning::the Developer ID certificate expires on $until: renew it before then"
130+ rm -f "$RUNNER_TEMP/cert.pem"
131+ echo "Signing as $APPLE_SIGNING_IDENTITY, certificate valid until $until"
132+
94133 - name : Create (or reuse) the draft release
95134 id : release
96135 env :
@@ -201,7 +240,11 @@ jobs:
201240 printf '%s\n' "$APPLE_API_KEY_P8" > "$key_path"
202241 echo "APPLE_API_KEY_PATH=$key_path" >> "$GITHUB_ENV"
203242 fi
204- if [ -z "$APPLE_CERTIFICATE" ]; then
243+ if [ -z "$APPLE_CERTIFICATE" ] || [ -z "$APPLE_API_KEY_P8" ]; then
244+ if [ "$GITHUB_REPOSITORY" = "codextde/godmode-bot" ]; then
245+ echo "::error::the Apple signing secrets are missing: an unsigned app can only be moved to the Trash on a Mac"
246+ exit 1
247+ fi
205248 echo "::warning::macOS build is not signed with a Developer ID (APPLE_CERTIFICATE missing): Gatekeeper will block it"
206249 fi
207250
@@ -236,11 +279,9 @@ jobs:
236279 - name : Check the macOS signature and notarization
237280 if : runner.os == 'macOS' && env.APPLE_CERTIFICATE != ''
238281 run : |
239- app=$(ls -d apps/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/macos/*.app | head -n1)
240- codesign --verify --deep --strict --verbose=2 "$app"
241- codesign -dvv "$app" 2>&1 | grep -E "Authority=Developer ID Application|TeamIdentifier"
242- xcrun stapler validate "$app"
243- spctl --assess --type execute --verbose=2 "$app"
282+ bundle=apps/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle
283+ apps=("$bundle"/macos/*.app) updates=("$bundle"/macos/*.app.tar.gz)
284+ scripts/macos-verify.sh "${apps[0]}" "${updates[0]}"
244285
245286 # The bundler notarizes the .app only; a downloaded DMG that is signed but not notarized can still warn on open.
246287 - name : Notarize the DMG and replace it in the release
@@ -252,7 +293,8 @@ jobs:
252293 dmg=$(ls apps/desktop/src-tauri/target/${{ matrix.rust_target }}/release/bundle/dmg/*.dmg | head -n1)
253294 xcrun notarytool submit "$dmg" --key "$APPLE_API_KEY_PATH" --key-id "$APPLE_API_KEY" --issuer "$APPLE_API_ISSUER" --wait --timeout 30m
254295 xcrun stapler staple "$dmg"
255- spctl --assess --type open --context context:primary-signature --verbose=2 "$dmg"
296+ # What a user downloads: the DMG and the app inside it, as Gatekeeper sees them.
297+ scripts/macos-verify.sh "$dmg"
256298 # GitHub stores spaces in asset names as dots.
257299 name=$(basename "$dmg" | tr ' ' '.')
258300 old=$(gh api "repos/$GITHUB_REPOSITORY/releases/$RELEASE_ID/assets?per_page=100" --jq ".[] | select(.name == \"$name\") | .id")
@@ -314,7 +356,7 @@ jobs:
314356 fail-fast : false
315357 matrix :
316358 include :
317- # Built on Linux: Linux + Windows. macOS binaries are built (and signed ad hoc) on macOS.
359+ # Built on Linux: Linux + Windows. macOS binaries are built, signed and notarized on macOS.
318360 - os : ubuntu-latest
319361 targets : bun-linux-x64 bun-linux-x64-baseline bun-linux-arm64 bun-windows-x64
320362 - os : macos-latest
@@ -340,6 +382,29 @@ jobs:
340382 bun run scripts/build.ts --target="$target" --smoke
341383 done
342384
385+ - name : Sign and notarize the macOS binaries
386+ if : runner.os == 'macOS'
387+ env :
388+ APPLE_CERTIFICATE : ${{ secrets.APPLE_CERTIFICATE }}
389+ APPLE_CERTIFICATE_PASSWORD : ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
390+ APPLE_SIGNING_IDENTITY : ${{ secrets.APPLE_SIGNING_IDENTITY }}
391+ APPLE_TEAM_ID : ${{ secrets.APPLE_TEAM_ID }}
392+ APPLE_API_ISSUER : ${{ secrets.APPLE_API_ISSUER }}
393+ APPLE_API_KEY : ${{ secrets.APPLE_API_KEY }}
394+ APPLE_API_KEY_P8 : ${{ secrets.APPLE_API_KEY_P8 }}
395+ run : |
396+ # Ad-hoc signed binaries downloaded in a browser can only be moved to the Trash.
397+ if [ -z "$APPLE_CERTIFICATE" ] || [ -z "$APPLE_API_KEY_P8" ]; then
398+ if [ "$GITHUB_REPOSITORY" = "codextde/godmode-bot" ]; then echo "::error::the Apple signing secrets are missing"; exit 1; fi
399+ echo "::warning::macOS binaries stay ad-hoc signed (no Apple signing secrets)"
400+ exit 0
401+ fi
402+ export APPLE_API_KEY_PATH="$RUNNER_TEMP/AuthKey_$APPLE_API_KEY.p8"
403+ printf '%s\n' "$APPLE_API_KEY_P8" > "$APPLE_API_KEY_PATH"
404+ scripts/macos-sign-binaries.sh packages/core/bin/godmode-darwin-*
405+ packages/core/bin/godmode-darwin-arm64 version
406+ scripts/macos-verify.sh packages/core/bin/godmode-darwin-*
407+
343408 - name : Upload to the draft release
344409 working-directory : packages/core/bin
345410 env :
0 commit comments