From 7c9ff7ab0d8d08bcfe525d11218bb5faf099b956 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 26 Oct 2026 15:52:21 +0000 Subject: [PATCH] Make release signing and notarization easier to set up The workflow now reads the Developer ID identity from the imported certificate, so MACOS_SIGN_IDENTITY is optional, and it fails early when a certificate is set without notarization credentials. make-dmg.sh checks the notarization status and prints Apple's log when a submission is rejected. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01Pqz2pCwcBrWAn7nD7KkoKi --- .github/workflows/release.yml | 20 ++++++++++++++++++-- README.md | 6 +++--- scripts/make-dmg.sh | 9 ++++++++- 3 files changed, 29 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 418bec3..82ebc6d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -30,7 +30,15 @@ jobs: env: MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }} MACOS_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }} + APPLE_ID: ${{ secrets.APPLE_ID }} + APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} + APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} run: | + # A signed but unnotarized DMG is still blocked by Gatekeeper, so don't ship one by accident. + if [[ -z "$APPLE_ID" || -z "$APPLE_TEAM_ID" || -z "$APPLE_APP_PASSWORD" ]]; then + echo "::error::MACOS_CERTIFICATE is set, but APPLE_ID, APPLE_TEAM_ID and APPLE_APP_PASSWORD are needed to notarize." + exit 1 + fi keychain="$RUNNER_TEMP/signing.keychain-db" keychain_password="$(uuidgen)" echo "$MACOS_CERTIFICATE" | base64 --decode > "$RUNNER_TEMP/certificate.p12" @@ -41,6 +49,12 @@ jobs: security set-key-partition-list -S apple-tool:,apple: -k "$keychain_password" "$keychain" security list-keychains -d user -s "$keychain" $(security list-keychains -d user | tr -d '"') rm "$RUNNER_TEMP/certificate.p12" + identity="$(security find-identity -v -p codesigning "$keychain" | sed -n 's/.*"\(Developer ID Application: .*\)"/\1/p' | head -1)" + if [[ -z "$identity" ]]; then + echo "::error::No Developer ID Application identity in MACOS_CERTIFICATE. Export the certificate together with its private key." + exit 1 + fi + echo "DETECTED_SIGN_IDENTITY=$identity" >> "$GITHUB_ENV" - name: Run tests run: swift test @@ -48,14 +62,16 @@ jobs: - name: Build the DMG env: CONSTA_VERSION: ${{ startsWith(github.ref, 'refs/tags/v') && github.ref_name || '' }} - CONSTA_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} + MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }} APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} APPLE_APP_PASSWORD: ${{ secrets.APPLE_APP_PASSWORD }} run: | export CONSTA_VERSION="${CONSTA_VERSION#v}" [[ -n "$CONSTA_VERSION" ]] || unset CONSTA_VERSION - [[ -n "$CONSTA_SIGN_IDENTITY" ]] || unset CONSTA_SIGN_IDENTITY + # MACOS_SIGN_IDENTITY is optional; by default use the identity found in the imported certificate. + identity="${MACOS_SIGN_IDENTITY:-${DETECTED_SIGN_IDENTITY:-}}" + [[ -z "$identity" ]] || export CONSTA_SIGN_IDENTITY="$identity" scripts/make-dmg.sh - uses: actions/upload-artifact@v4 diff --git a/README.md b/README.md index 28ee443..ac7740b 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ Pick what you're working on, press Start, and watch the time add up. Daily goals Download the latest `Consta-.dmg` from [Releases](https://github.com/banstack/consta/releases/latest), open it, and drag Consta to Applications. Consta needs macOS 27 or later. -The DMG isn't notarized yet, so macOS blocks the first launch. Open **System Settings › Privacy & Security** and click **Open Anyway** once. +Releases are signed and notarized by Apple, so Consta opens like any other app. Releases up to v0.2.0 weren't notarized: if macOS blocks one of those on first launch, open **System Settings › Privacy & Security** and click **Open Anyway** once. ## Build from source @@ -62,13 +62,13 @@ scripts/make-dmg.sh #### Releases -Pushing a tag like `v0.1.0` runs `.github/workflows/release.yml`, which tests, builds the DMG and attaches it to a GitHub release. It signs and notarizes when these repository secrets are set and builds an unsigned DMG otherwise: +Pushing a tag like `v0.1.0` runs `.github/workflows/release.yml`, which tests, builds the DMG and attaches it to a GitHub release. It signs and notarizes when these repository secrets are set and builds an unsigned DMG otherwise. With a certificate but no notarization credentials the build fails, since a signed but unnotarized DMG is still blocked by Gatekeeper. | Secret | What it is | | --- | --- | | `MACOS_CERTIFICATE` | Your Developer ID Application certificate exported as .p12, base64-encoded (`base64 -i cert.p12 \| pbcopy`) | | `MACOS_CERTIFICATE_PASSWORD` | The .p12 export password | -| `MACOS_SIGN_IDENTITY` | The identity name, e.g. `Developer ID Application: Your Name (TEAMID)` | +| `MACOS_SIGN_IDENTITY` | Optional. The identity name, e.g. `Developer ID Application: Your Name (TEAMID)`. By default the workflow uses the Developer ID identity in `MACOS_CERTIFICATE`. | | `APPLE_ID`, `APPLE_TEAM_ID`, `APPLE_APP_PASSWORD` | The Apple ID, team ID and app-specific password used for notarization | ### Scripted UI runs diff --git a/scripts/make-dmg.sh b/scripts/make-dmg.sh index 7ca46e4..ce4c001 100755 --- a/scripts/make-dmg.sh +++ b/scripts/make-dmg.sh @@ -41,7 +41,14 @@ else fi echo "==> Notarizing (this usually takes a few minutes)" -xcrun notarytool submit "$dmg" "${notary_auth[@]}" --wait +result="$(xcrun notarytool submit "$dmg" "${notary_auth[@]}" --wait --output-format plist)" +submission="$(plutil -extract id raw - <<<"$result")" +status="$(plutil -extract status raw - <<<"$result")" +if [[ "$status" != "Accepted" ]]; then + echo "==> Notarization finished with status $status. Apple's log:" >&2 + xcrun notarytool log "$submission" "${notary_auth[@]}" >&2 || true + exit 1 +fi xcrun stapler staple "$dmg" spctl --assess --type open --context context:primary-signature --verbose=2 "$dmg" echo "==> Done: $dmg (signed and notarized)"