From b2a0a42ca4dbbe7f2e9edcbea78c5c0332e51a74 Mon Sep 17 00:00:00 2001 From: Adrian Bonpin Date: Fri, 4 Sep 2026 09:22:04 +0800 Subject: [PATCH] feat: Developer-ID sign + notarize macOS builds - tauri.conf.json: enable hardenedRuntime + entitlements (keep ad-hoc signingIdentity as the dev default; release script overrides it) - add desktop/src-tauri/entitlements.plist (JIT / unsigned-exec-mem / dyld-env-vars for the hardened runtime) - scripts/release-mac.sh: auto-detect Developer ID identity from the keychain, load notarization creds from ~/.config/gridline/notarize.env (App Store Connect API key), re-sign bundled pg tools after install_name_tool (invalidated signatures would fail notarization), and verify codesign + stapled notarization before upload - README/AGENTS.md: replace the unsigned/xattr instructions with the macOS signing & notarization setup guide --- .github/workflows/release.yml | 15 ++---- AGENTS.md | 2 +- README.md | 31 ++++++------ desktop/src-tauri/entitlements.plist | 16 ++++++ desktop/src-tauri/tauri.conf.json | 3 +- scripts/release-mac.sh | 75 ++++++++++++++++++++++++++++ 6 files changed, 116 insertions(+), 26 deletions(-) create mode 100644 desktop/src-tauri/entitlements.plist diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f9f33e..bd1288f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,16 +7,11 @@ name: Release # git checkout prod && git pull # git tag v0.5.0 && git push origin v0.5.0 # -# SIGNING STATUS: macOS bundles are AD-HOC signed via tauri.conf.json -# (bundle.macOS.signingIdentity: "-", hardenedRuntime: false). That replaces -# the linker-only signature that made macOS report the app as "damaged" and -# refuse Finder launches — users now get the standard "Open Anyway" prompt -# (or run `sudo xattr -dr com.apple.quarantine` for the damaged-error case, see -# README). Still NOT Developer-ID signed / notarized (no certs yet). -# tauri-action automatically signs + notarizes when the signing secrets are -# present, so the moment we add APPLE_CERTIFICATE / APPLE_API_KEY / -# WINDOWS_CERTIFICATE (or Azure Trusted Signing) to repo secrets, future -# builds are properly signed — no changes to this file required. +# SIGNING STATUS: macOS is Developer-ID signed + notarized via +# scripts/release-mac.sh (built manually on Adrian's machine). NOTE: this +# GitHub workflow is NOT the active release path — Gitea Actions uses +# .gitea/workflows/release.yml (Linux/Windows only). Linux/Windows installers +# remain unsigned. on: push: diff --git a/AGENTS.md b/AGENTS.md index f6e7ba8..e7b05e7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -184,7 +184,7 @@ Cut a release by tagging the **`prod`** branch once the PR is merged — `git ta - `desktop/src/lib/version.test.ts` and `desktop/src/lib/docs-coverage.test.ts` if they assert the version - **README download links are static (versioned)** — both download tables (top **Download** section + **Which file should I download?**) link directly to the release-tag assets (`releases/download/v0.7.9/`). tauri-action uses default versioned asset names (`Gridline__aarch64.dmg`, `Gridline--1.x86_64.rpm`, etc.) — update BOTH tables to the new names on every release (see the MAINTENANCE comment in README.md). - **Bundled pg tools:** `tauri.conf.json` `bundle.resources` lists `resources/pg_tools/*`; the `release.yml` matrix builds/downloads + checksum-verifies the static binaries before the Tauri build step. -- **Release notes must include the macOS first-launch instructions** — until a Developer ID signing certificate is secured, releases are ad-hoc signed only (not notarized), so every macOS user hits a Gatekeeper prompt. The draft release body must include: right-click → **Open** → **Open** (or System Settings → Privacy & Security → **Open Anyway**) for "developer cannot be verified", and `sudo xattr -dr com.apple.quarantine /Applications/Gridline.app` (with the re-run-after-every-upgrade note) for "damaged and can't be opened". +- **macOS is Developer-ID signed + notarized** via `scripts/release-mac.sh` (built manually on Adrian's machine; not in CI). The script auto-detects the Developer ID identity from the login keychain, reads notarization credentials from `~/.config/gridline/notarize.env` (App Store Connect API key), and verifies codesign + stapled notarization before uploading. If the cert or credentials are missing it fails with setup instructions (see the README's "macOS signing & notarization" section). ### Adding a Tauri Command diff --git a/README.md b/README.md index 5d0f279..9a4dc26 100644 --- a/README.md +++ b/README.md @@ -49,9 +49,9 @@ brew install --cask AdrianBonpin/gridline/gridline brew install --cask gridline ``` -The cask strips the macOS quarantine flag automatically during install, so **no manual `xattr` step is needed** on the Homebrew path — the app just launches. (Direct-DMG downloads still need the [first-launch instructions](#installers-are-unsigned-for-now) below.) +The cask installs the app directly. The DMGs are Developer-ID signed and notarized, so macOS trusts them on first launch — no `xattr` or "Open Anyway" step needed. -> **macOS first launch:** macOS may say *"Gridline is damaged and can't be opened"* or *"the developer cannot be verified"* — this is expected; the app isn't Developer-ID signed/notarized yet. See the [macOS first-launch instructions](#installers-are-unsigned-for-now) (right-click → Open, or the one-time `xattr` fix). Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below. +> **macOS first launch:** Gridline is Developer-ID signed and notarized, so macOS opens it without a Gatekeeper prompt. Not sure if your Mac is Intel or Apple Silicon? See [Which file should I download?](#which-file-should-i-download) below. + com.apple.security.cs.allow-jit + + + com.apple.security.cs.allow-unsigned-executable-memory + + + com.apple.security.cs.allow-dyld-environment-variables + + + diff --git a/desktop/src-tauri/tauri.conf.json b/desktop/src-tauri/tauri.conf.json index 12b76ee..3ff9e10 100644 --- a/desktop/src-tauri/tauri.conf.json +++ b/desktop/src-tauri/tauri.conf.json @@ -29,7 +29,8 @@ "resources": ["resources/pg_tools/*", "resources/mysql_tools/*"], "macOS": { "signingIdentity": "-", - "hardenedRuntime": false + "hardenedRuntime": true, + "entitlements": "entitlements.plist" }, "icon": [ "icons/32x32.png", diff --git a/scripts/release-mac.sh b/scripts/release-mac.sh index a3da6d2..7904c46 100755 --- a/scripts/release-mac.sh +++ b/scripts/release-mac.sh @@ -75,6 +75,11 @@ for b in pg_dump pg_restore psql; do libpq=$(otool -L "$OUT_PG/$b" | awk '/libpq/ {print $1; exit}') install_name_tool -change "$libpq" "@loader_path/libpq.5.dylib" "$OUT_PG/$b" done +# install_name_tool invalidates the linker's ad-hoc signature; re-sign so the +# bundle passes notarization (every code object inside must be signed). +for f in "$OUT_PG"/pg_dump "$OUT_PG"/pg_restore "$OUT_PG"/psql "$OUT_PG"/libpq.5.dylib; do + codesign --force --sign - "$f" 2>/dev/null || true +done echo ">> Building MariaDB client tools..." mkdir -p "$OUT_MY" @@ -116,6 +121,58 @@ for pass in 1 2 3 4 5 6; do done done +# ---- macOS signing + notarization ----------------------------------------- +# Requires a "Developer ID Application" certificate in the login keychain and +# an App Store Connect API key for notarization. See the README's "macOS +# signing" section for how to create both. +# +# Notarization credentials are read from ~/.config/gridline/notarize.env +# (never committed). Create it with: +# APPLE_API_KEY= +# APPLE_API_ISSUER= +# APPLE_API_KEY_PATH=/absolute/path/to/AuthKey_.p8 +# (or drop the .p8 at ~/.appstoreconnect/private_keys/AuthKey_.p8 and +# omit APPLE_API_KEY_PATH — tauri auto-searches that location.) + +# 1. Resolve the Developer ID Application signing identity from the keychain. +SIGNING_IDENTITY="$(security find-identity -v -p codesigning 2>/dev/null \ + | grep -i 'Developer ID Application' \ + | sed -E 's/.*"([^"]+)".*/\1/' \ + | head -1 || true)" +if [ -z "$SIGNING_IDENTITY" ]; then + echo "ERROR: No 'Developer ID Application' certificate found in the login keychain." >&2 + echo " Create one at https://developer.apple.com/account/resources/certificates/list" >&2 + echo " (Certificates, IDs & Profiles -> + -> Developer ID Application), download the" >&2 + echo " .cer, and double-click it to install into your login keychain." >&2 + echo " Verify with: security find-identity -v -p codesigning" >&2 + exit 1 +fi +export APPLE_SIGNING_IDENTITY="$SIGNING_IDENTITY" +echo ">> Signing identity: $SIGNING_IDENTITY" + +# 2. Load notarization credentials. +NOTARIZE_ENV="${HOME}/.config/gridline/notarize.env" +if [ -f "$NOTARIZE_ENV" ]; then + set -a + # shellcheck disable=SC1090 + source "$NOTARIZE_ENV" + set +a +fi +if [ -z "${APPLE_API_KEY:-}" ] || [ -z "${APPLE_API_ISSUER:-}" ]; then + if [ "${GRIDLINE_SKIP_NOTARIZE:-0}" = "1" ]; then + echo ">> WARNING: notarization credentials missing; GRIDLINE_SKIP_NOTARIZE=1 set." >&2 + echo " Building signed but NOT notarized (test-only — users will still see a prompt)." >&2 + else + echo "ERROR: Notarization credentials not configured." >&2 + echo " Create ${NOTARIZE_ENV} with APPLE_API_KEY and APPLE_API_ISSUER" >&2 + echo " (and APPLE_API_KEY_PATH, or place the .p8 at ~/.appstoreconnect/private_keys/)." >&2 + echo " See the README's 'macOS signing' section for the App Store Connect API key setup." >&2 + exit 1 + fi +else + echo ">> Notarization: API key ${APPLE_API_KEY} (issuer ${APPLE_API_ISSUER})" +fi + # ---- build the app (both arches) ------------------------------------------ cd "${WORKSPACE}/desktop" echo ">> Installing frontend dependencies..." @@ -130,6 +187,24 @@ VERSION="${TAG#v}" BUNDLE_ARM="${WORKSPACE}/desktop/src-tauri/target/aarch64-apple-darwin/release/bundle" BUNDLE_INTEL="${WORKSPACE}/desktop/src-tauri/target/x86_64-apple-darwin/release/bundle" +# ---- verify signing + notarization ----------------------------------------- +# Confirm both .app bundles are properly signed and carry a stapled +# notarization ticket before we upload them. spctl can lag right after +# notarization (assessment cache), so stapler validate is the authoritative +# check; a pending spctl is only a warning. +for APP in \ + "${BUNDLE_ARM}/macos/Gridline.app" \ + "${BUNDLE_INTEL}/macos/Gridline.app"; do + [ -d "$APP" ] || { echo "missing $APP" >&2; exit 1; } + echo ">> Verifying $APP" + codesign --verify --deep --strict --verbose=2 "$APP" \ + || { echo "codesign verify FAILED" >&2; exit 1; } + xcrun stapler validate "$APP" \ + || { echo "notarization ticket not stapled" >&2; exit 1; } + spctl --assess --type execute --verbose=4 "$APP" \ + || echo " (spctl assessment pending — normal right after notarization; stapler validate passed)" +done + # ---- create/ensure release + upload DMGs ----------------------------------- API="${GITEA_SERVER}/api/v1/repos/${REPO_OWNER}/${REPO_NAME}" AUTH="Authorization: token ${GITEA_TOKEN}"