diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 83547b56e1..4955866d9b 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -101,8 +101,52 @@ jobs: fi chmod +x desktopApp/packaging/appimage/appimagetool-x86_64.AppImage + # macOS only: import the Developer ID Application cert into a throwaway + # keychain so jpackage's codesign pass can find it. Soft-gated — if the + # MAC_CERTIFICATE_P12 secret isn't set (forks, or before Apple creds are + # provisioned) the DMG is built UNSIGNED, exactly as before. notarytool + # runs as part of the gradle task when the identity env is exported below. + - name: Import Apple Developer ID certificate (macOS leg, if configured) + if: matrix.family == 'macos' + id: mac_keychain + env: + CERT_P12: ${{ secrets.MAC_CERTIFICATE_P12 }} + CERT_PASSWORD: ${{ secrets.MAC_CERTIFICATE_PASSWORD }} + run: | + set -euo pipefail + if [[ -z "${CERT_P12:-}" ]]; then + echo "::notice::MAC_CERTIFICATE_P12 not set — building UNSIGNED DMG." + echo "signing=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + KEYCHAIN="$RUNNER_TEMP/amethyst-signing.keychain-db" + KEYCHAIN_PWD="$(openssl rand -base64 24)" + CERT_PATH="$RUNNER_TEMP/developer_id.p12" + security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" + security set-keychain-settings -lut 21600 "$KEYCHAIN" + security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" + echo "$CERT_P12" | base64 --decode > "$CERT_PATH" + security import "$CERT_PATH" -P "$CERT_PASSWORD" \ + -k "$KEYCHAIN" -T /usr/bin/codesign -T /usr/bin/productsign + # Let codesign use the private key without an interactive UI prompt. + security set-key-partition-list -S apple-tool:,apple:,codesign: \ + -s -k "$KEYCHAIN_PWD" "$KEYCHAIN" >/dev/null + # Prepend our keychain to the user search list so codesign sees it. + security list-keychains -d user -s "$KEYCHAIN" \ + $(security list-keychains -d user | sed -e 's/[\"[:space:]]//g') + rm -f "$CERT_PATH" + echo "signing=true" >> "$GITHUB_OUTPUT" + - name: Build desktop artifacts uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0 + env: + # Empty on non-macOS legs and on the macOS leg when no cert is + # configured — the gradle macOS{} block skips signing when the + # identity is blank. + AMETHYST_MAC_SIGN_IDENTITY: ${{ steps.mac_keychain.outputs.signing == 'true' && secrets.MAC_SIGN_IDENTITY || '' }} + AMETHYST_NOTARY_APPLE_ID: ${{ secrets.MAC_NOTARY_APPLE_ID }} + AMETHYST_NOTARY_PASSWORD: ${{ secrets.MAC_NOTARY_PASSWORD }} + AMETHYST_NOTARY_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }} with: max_attempts: 2 timeout_minutes: 15 diff --git a/BUILDING.md b/BUILDING.md index 941bf4b3f1..83cb4b655e 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -250,13 +250,26 @@ provided automatically; everything else you set yourself.) | `SONATYPE_PASSWORD` | Maven Central user token password | Same | | `SIGNING_PRIVATE_KEY` | **GPG/PGP** private key, ASCII-armored | Signs the Maven artifacts (Central requires it) | | `SIGNING_PASSWORD` | Passphrase for that GPG key | Same | +| `MAC_CERTIFICATE_P12` | Base64 of your **Apple Developer ID Application** cert (`.p12`, includes the private key) | Signs the macOS desktop **DMG** | +| `MAC_CERTIFICATE_PASSWORD` | Password set when exporting the `.p12` | Imports the cert into the CI keychain | +| `MAC_SIGN_IDENTITY` | Full identity string, e.g. `Developer ID Application: Your Name (TEAMID)` | The `codesign` identity to sign with | +| `MAC_NOTARY_APPLE_ID` | Apple ID email of the notarization account | Apple notarization (`notarytool`) | +| `MAC_NOTARY_PASSWORD` | **App-specific** password for that Apple ID (not the login password) | Same | +| `MAC_NOTARY_TEAM_ID` | 10-char Apple Developer **Team ID** | Same | | `HOMEBREW_TOKEN` | PAT for `Homebrew/homebrew-cask` | Desktop cask bump (stable tags) | | `WINGET_TOKEN` | PAT for `microsoft/winget-pkgs` | Desktop winget bump (stable tags) | | `CROWDIN_PERSONAL_TOKEN`, `CROWDIN_PROJECT_ID` | Crowdin API creds | Translation sync (separate workflow, not the release) | -Note the **two distinct signing identities** people often conflate: +Note the **three distinct signing identities** people often conflate: `SIGNING_KEY` + `KEY_*` is the **Android keystore**; `SIGNING_PRIVATE_KEY` + -`SIGNING_PASSWORD` is the **GPG key** for Maven Central. They are unrelated. +`SIGNING_PASSWORD` is the **GPG key** for Maven Central; `MAC_CERTIFICATE_*` + +`MAC_SIGN_IDENTITY` + `MAC_NOTARY_*` is the **Apple Developer ID** for the macOS +desktop DMG. They are unrelated — each comes from a different authority. + +The macOS desktop signing secrets are **optional**: if `MAC_CERTIFICATE_P12` is +unset the release workflow still builds the DMG, just **unsigned** (the previous +behavior). Provision all six to switch signing + notarization on. Obtaining them +requires Apple Developer Program membership ($99/yr). Generating the values: @@ -269,6 +282,14 @@ base64 -i upload.jks | tr -d '\n' # paste output into SIGNING_KEY # GPG key → armored private key for SIGNING_PRIVATE_KEY gpg --full-generate-key # create the key (once) gpg --armor --export-secret-keys # paste output into SIGNING_PRIVATE_KEY + +# Apple Developer ID Application cert → base64 for MAC_CERTIFICATE_P12. +# In Keychain Access, export the "Developer ID Application: ..." cert (with its +# private key) as a .p12, setting an export password (-> MAC_CERTIFICATE_PASSWORD). +base64 -i developer_id.p12 | tr -d '\n' # paste output into MAC_CERTIFICATE_P12 +security find-identity -v -p codesigning # shows the exact MAC_SIGN_IDENTITY string +# MAC_NOTARY_PASSWORD is an app-specific password from https://appleid.apple.com +# (Sign-In and Security -> App-Specific Passwords), NOT your Apple ID login. ``` `SONATYPE_USERNAME`/`SONATYPE_PASSWORD` are a **user token** from @@ -480,9 +501,17 @@ for the deprecation date. When it hits: Homebrew has committed to disabling unsigned casks in `Homebrew/homebrew-cask` on 2026-09-01. Before that date: -**Option A**: Commit budget to Apple Developer Program ($99/yr), add -`signing { sign.set(true) }` + `notarization {}` blocks to -`desktopApp/build.gradle.kts`, wire Developer ID + notary creds into CI. +**Option A (wiring done — needs Apple creds)**: The `signing { sign.set(true) }` ++ `notarization {}` blocks are already in `desktopApp/build.gradle.kts` (gated on +the `AMETHYST_MAC_SIGN_IDENTITY` env var), and the macOS leg of +`create-release.yml` imports a Developer ID cert into a throwaway keychain and +exports the signing/notary env. It all stays a **no-op until the six +`MAC_*`/notary secrets are provisioned** (see [§ Secrets the CI +needs](#secrets-the-ci-needs)) — until then the DMG builds unsigned. To turn it +on: join the Apple Developer Program ($99/yr), create a *Developer ID +Application* certificate, generate an app-specific password, and set the six +secrets. The first signed+notarized DMG is best validated with a +`workflow_dispatch` dry-run before a real tag. **Option B**: Pivot to a private Homebrew tap: diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index 730ab5c59f..d8cf817781 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -135,6 +135,36 @@ compose.desktop { macOS { bundleID = "com.vitorpamplona.amethyst.desktop" iconFile.set(project.file("src/jvmMain/resources/icon.icns")) + + // --- Developer ID code signing + notarization --- + // Required for Homebrew's main cask (unsigned casks rejected after + // 2026-09-01) and to clear macOS Gatekeeper without the right-click + // dance. Gated on the signing-identity env var so local dev builds + // and PR CI keep producing plain UNSIGNED DMGs exactly as before — + // signing only kicks in when the release workflow exports these + // (which it does only when the Apple secrets are present): + // + // AMETHYST_MAC_SIGN_IDENTITY "Developer ID Application: NAME (TEAMID)" + // AMETHYST_NOTARY_APPLE_ID Apple ID email of the notary account + // AMETHYST_NOTARY_PASSWORD app-specific password for that Apple ID + // AMETHYST_NOTARY_TEAM_ID 10-char Apple Developer Team ID + // + // The Developer ID Application certificate must already be in the + // build host's keychain (CI imports it from a base64 .p12 secret). + // Compose ships default hardened-runtime entitlements that permit + // the JVM's JIT, so no custom entitlements file is needed. + val macSignIdentity = System.getenv("AMETHYST_MAC_SIGN_IDENTITY") + if (!macSignIdentity.isNullOrBlank()) { + signing { + sign.set(true) + identity.set(macSignIdentity) + } + notarization { + appleID.set(System.getenv("AMETHYST_NOTARY_APPLE_ID")) + password.set(System.getenv("AMETHYST_NOTARY_PASSWORD")) + teamID.set(System.getenv("AMETHYST_NOTARY_TEAM_ID")) + } + } } windows {