From 8a8f58e7add598002fd81ee5fa4f7b92e061e38d Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Wed, 29 Jul 2026 09:59:00 -0400 Subject: [PATCH] fix(release): actually notarize the macOS DMG, and verify it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS leg ran only `packageReleaseDmg`, which SIGNS the DMG but does not notarize it — notarization is a separate Compose task. The `notarization {}` block in desktopApp/build.gradle.kts only supplies credentials; nothing invoked it. So every release up to v1.13.1 shipped a signed but unnotarized DMG: `xcrun stapler validate` reports no ticket on either the .app or the .dmg, and Gatekeeper blocks it on first launch. Append `:desktopApp:notarizeReleaseDmg` on the macOS leg. Compose 1.11.1's AbstractNotarizationTask runs `notarytool submit --wait` and then `stapler staple` in place, so the asset-collection step still finds the same file. Raise that leg's inner timeout to 45m since the submit blocks on Apple. Gate it on the cert AND all three notary secrets, so forks and credential-less runs keep producing a plain unsigned DMG instead of failing. Add a verify step that asserts the stapled ticket. The bug survived many releases precisely because nothing ever checked the outcome. Also update the reference cask to 1.13.1 and make it pass `brew audit --new --cask` + `brew style` cleanly: add the missing conflicts_with (the tiling window manager's cask installs the same Amethyst.app), add depends_on :macos, fix stanza order, drop the unnecessary `verified:` (url and homepage share a domain), and widen the zap to the state dirs the source actually uses. Correct BUILDING.md's macOS state table, which listed a com.vitorpamplona.amethyst.desktop.plist that does not exist and omitted ~/.amethyst. Note that Java's Preferences API writes to a SHARED com.apple.java.util.prefs.plist, which is why the cask must not zap it. --- .github/workflows/create-release.yml | 40 ++++++++++++++++++- BUILDING.md | 9 ++++- .../packaging/homebrew/amethyst-nostr.rb | 27 ++++++++++--- 3 files changed, 67 insertions(+), 9 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 3a10ca3e3c..19ca1146e8 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -155,8 +155,44 @@ jobs: AMETHYST_NOTARY_TEAM_ID: ${{ secrets.MAC_NOTARY_TEAM_ID }} with: max_attempts: 2 - timeout_minutes: 15 - command: ./gradlew --no-daemon :desktopApp:${{ matrix.tasks }} + # macOS needs far longer: notarizeReleaseDmg blocks on `notarytool + # submit --wait`, which is minutes-to-tens-of-minutes on Apple's side. + timeout_minutes: ${{ matrix.family == 'macos' && 45 || 15 }} + # Append notarization on the macOS leg. `packageReleaseDmg` only SIGNS + # the DMG — notarization is a separate Compose task, and because it was + # never invoked every release up to v1.13.1 shipped a signed but + # UNNOTARIZED DMG that Gatekeeper blocks on first launch. The task runs + # `notarytool submit --wait` and then `stapler staple`, in place, so the + # asset-collection step below still finds the same file. + # + # Gated on the cert AND all three notary secrets being present, so forks + # and credential-less runs keep producing a plain unsigned DMG exactly as + # before instead of failing. + command: ./gradlew --no-daemon :desktopApp:${{ matrix.tasks }}${{ (matrix.family == 'macos' && steps.mac_keychain.outputs.signing == 'true' && secrets.MAC_NOTARY_APPLE_ID != '' && secrets.MAC_NOTARY_PASSWORD != '' && secrets.MAC_NOTARY_TEAM_ID != '') && ' :desktopApp:notarizeReleaseDmg' || '' }} + + # Regression guard. The missing-notarization bug was invisible for many + # releases precisely because nothing ever asserted the outcome; assert it + # now so a silently-dropped notarization step can never ship again. + - name: Verify the DMG is notarized and stapled (macOS leg) + if: matrix.family == 'macos' + env: + EXPECT_NOTARIZED: ${{ (steps.mac_keychain.outputs.signing == 'true' && secrets.MAC_NOTARY_APPLE_ID != '' && secrets.MAC_NOTARY_PASSWORD != '' && secrets.MAC_NOTARY_TEAM_ID != '') && 'true' || 'false' }} + run: | + set -euo pipefail + DMG=$(find desktopApp/build/compose/binaries -name "*.dmg" -print -quit) + [[ -n "$DMG" ]] || { echo "::error::no DMG produced"; exit 1; } + echo "Checking $DMG" + + if [[ "$EXPECT_NOTARIZED" != "true" ]]; then + echo "::warning::Apple signing/notary credentials are not configured; this DMG is unsigned and unnotarized. Gatekeeper will block it, and it is not eligible for the Homebrew cask." + exit 0 + fi + + if ! xcrun stapler validate "$DMG"; then + echo "::error::$DMG has no stapled notarization ticket -- notarizeReleaseDmg did not run or failed" + exit 1 + fi + echo "notarization ticket stapled OK" # jpackage pins libicu to the build host's version (libicu74 on # ubuntu-24.04). Rewrite the .deb so it installs across Debian/Ubuntu. diff --git a/BUILDING.md b/BUILDING.md index 1fd7b61bf5..120a23172a 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -662,12 +662,19 @@ by any other channel. | OS | App location | State directories | |---|---|---| -| macOS | `/Applications/Amethyst.app` | `~/Library/Application Support/Amethyst`
`~/Library/Preferences/com.vitorpamplona.amethyst.desktop.plist`
`~/Library/Caches/Amethyst` | +| macOS | `/Applications/Amethyst.app` | `~/.amethyst` (accounts + keys)
`~/Library/Application Support/Amethyst` (Tor)
`~/Library/Caches/AmethystDesktop` (image cache)
`~/Library/Preferences/com.apple.java.util.prefs.plist` (**shared** — see below) | | Windows | `%LOCALAPPDATA%\Amethyst` or `C:\Program Files\Amethyst` | `%APPDATA%\Amethyst`
`%LOCALAPPDATA%\Amethyst` | | Linux (deb/rpm) | `/opt/amethyst` | `~/.config/amethyst`
`~/.local/share/amethyst`
`~/.cache/amethyst` | | Linux (AppImage/tar.gz) | user-chosen | Same as above | | Linux (Flatpak) | `/var/lib/flatpak` or `~/.local/share/flatpak` | `~/.var/app/com.vitorpamplona.amethyst.Desktop/` | +**macOS preferences are in a SHARED file.** `DesktopPreferences` uses the Java +Preferences API, which on macOS writes into +`~/Library/Preferences/com.apple.java.util.prefs.plist` — one plist for *every* +Java application on the machine, not a per-app file. Never delete it to "reset +Amethyst": that wipes unrelated apps' settings. This is why the Homebrew cask's +`zap` stanza deliberately omits it. + Uninstall: - Homebrew: `brew uninstall --cask amethyst-nostr && brew zap amethyst-nostr` diff --git a/desktopApp/packaging/homebrew/amethyst-nostr.rb b/desktopApp/packaging/homebrew/amethyst-nostr.rb index c63eef6ec1..0e0ae12854 100644 --- a/desktopApp/packaging/homebrew/amethyst-nostr.rb +++ b/desktopApp/packaging/homebrew/amethyst-nostr.rb @@ -16,13 +16,12 @@ # https://github.com/vitorpamplona/amethyst/releases/download/vX.Y.Z/amethyst-desktop-X.Y.Z-macos-arm64.dmg # shasum -a 256 amethyst.dmg cask "amethyst-nostr" do - version "1.12.6" - sha256 "69882e83ebcec6723e1ad5655ec2c9d1fa151b9d1a8ae51b869a9d62feabf093" + version "1.13.1" + sha256 "ead8f4f6263417d661b0d24be3853ec3284b330abc14748ebd77a2c2afcd2789" - url "https://github.com/vitorpamplona/amethyst/releases/download/v#{version}/amethyst-desktop-#{version}-macos-arm64.dmg", - verified: "github.com/vitorpamplona/amethyst/" + url "https://github.com/vitorpamplona/amethyst/releases/download/v#{version}/amethyst-desktop-#{version}-macos-arm64.dmg" name "Amethyst" - desc "Nostr client for desktop" + desc "Nostr client" homepage "https://github.com/vitorpamplona/amethyst" livecheck do @@ -30,9 +29,25 @@ cask "amethyst-nostr" do strategy :github_latest end + # The unrelated tiling window manager (cask `amethyst`, ianyh/Amethyst) also + # installs `Amethyst.app`, so the two cannot coexist in /Applications. + conflicts_with cask: "amethyst" depends_on arch: :arm64 + depends_on :macos app "Amethyst.app" - zap trash: "~/.amethyst" + # Verified against the source, not the docs: + # ~/.amethyst DesktopAccountStorage (accounts + keys) + # ~/Library/Application Support/Amethyst DesktopTorManager (tor/) + # ~/Library/Caches/AmethystDesktop Coil image cache + # + # Deliberately NOT zapped: ~/Library/Preferences/com.apple.java.util.prefs.plist. + # The app uses the Java Preferences API, which writes to that single SHARED + # plist — deleting it would wipe every other Java app's preferences too. + zap trash: [ + "~/.amethyst", + "~/Library/Application Support/Amethyst", + "~/Library/Caches/AmethystDesktop", + ] end