feat: sign and notarize macOS DMG releases - #18
Conversation
Wire Developer ID + App Store Connect API notarization into the release workflow, ship arch-specific DMGs, and prefer them in the release installer.
📝 WalkthroughWalkthroughmacOS release automation now optionally signs and notarizes UI DMGs, while Tauri bundle settings provide hardened runtime entitlements. Release documentation and the installer now support DMG-first distribution with tarball fallback. ChangesmacOS release and installation
Estimated code review effort: 4 (Complex) | ~45 minutes Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/release.yml:
- Around line 159-160: Update the release validation commands in the workflow to
fail the job when signing or notarization verification fails: replace the
diagnostic-only spctl and codesign invocations and remove their `|| true`
suppression, using strict deep code-signature verification and stapler
validation for the generated DMG or app artifacts.
In `@INSTALL.md`:
- Around line 86-88: Update the documented macOS DMG filename in INSTALL.md to
use the complete <target> suffix, such as aarch64-apple-darwin, instead of only
<arch>; preserve the existing tag placeholder and filename structure.
In `@scripts/install-from-release.sh`:
- Line 106: Update the release marker assignment in the install script so MARKER
points to a separate state directory outside Microbridge.app, rather than
placing .microbridge-release under DEST. Ensure the directory exists before the
marker is written, while preserving the existing marker read/write behavior.
- Line 117: Remove the xattr command that clears com.apple.quarantine from
install_app_bundle(), including the DMG and tarball installation paths, so the
installed app bundle retains quarantine and macOS can enforce Gatekeeper on
first launch.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: a0b1ef2d-f005-412c-b740-00357562a050
📒 Files selected for processing (6)
.github/workflows/release.ymlINSTALL.mdapps/microbridge-ui/src-tauri/Entitlements.plistapps/microbridge-ui/src-tauri/tauri.conf.jsondocs/macos-signing.mdscripts/install-from-release.sh
| spctl --assess --type open --context context:primary-signature "$DMG_OUT" || true | ||
| codesign -dv --verbose=2 "$APP" || true |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
sed -n '130,190p' .github/workflows/release.ymlRepository: DevVig/microbridge
Length of output: 2591
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
p = Path('.github/workflows/release.yml')
text = p.read_text()
for i, line in enumerate(text.splitlines(), 1):
if 145 <= i <= 170:
print(f"{i:4}: {line}")
PYRepository: DevVig/microbridge
Length of output: 1483
🏁 Script executed:
rg -n "spctl|stapler|codesign" .github/workflows/release.yml .github/workflows -g '*.yml' -g '*.yaml'Repository: DevVig/microbridge
Length of output: 1534
🏁 Script executed:
sed -n '1,220p' .github/workflows/release.ymlRepository: DevVig/microbridge
Length of output: 8789
🌐 Web query:
Apple codesign -dv does not verify integrity spctl assess notarization stapler validate documentation
💡 Result:
The tools codesign, spctl, and stapler serve distinct roles in verifying the integrity and notarization status of macOS software [1][2][3]. It is inaccurate to state that codesign does not verify integrity; rather, it verifies code signature integrity, while other tools verify notarization status [4][3]. 1. codesign: This tool verifies the integrity of the code signature itself [4][3]. It confirms that the binary is signed, the signature is cryptographically sound, and that all sealed components (the executable code and resources) remain unaltered [4]. Running codesign -vvv --deep --strict /path/to/app verifies that the app's signature is structurally and cryptographically valid [1]. 2. spctl: This tool assesses whether the software is authorized by system policies, including Gatekeeper [2][5]. To check if an app is notarized, you use spctl --assess --verbose /path/to/app [1][2]. A successful result indicates that the app is notarized (e.g., source=Notarized Developer ID) [6]. It essentially asks the system policy daemon (syspolicyd) to evaluate the app against current security requirements, which includes checking for a notarization ticket [2]. 3. stapler: This tool is used to attach (staple) a notarization ticket to an app bundle, allowing the app to be verified offline [7][8]. The command stapler validate /path/to/app confirms whether a notarization ticket is successfully stapled to the bundle [9][10]. If an app is notarized but the ticket is not stapled, stapler validate will report that no ticket is found, even if the app is notarized online [3][10]. In summary, codesign confirms the app's internal integrity (that it hasn't been modified since it was signed), while spctl and stapler are used to verify the app's notarization status with Apple [1][2][3]. If you need to confirm both, you must use both categories of tools [2][10].
Citations:
- 1: https://developer.apple.com/documentation/security/resolving-common-notarization-issues
- 2: https://www.macinternals.app/en/blog/gatekeeper
- 3: https://eclecticlight.co/2020/05/27/a-users-guide-to-code-signatures-and-notarization/
- 4: https://keith.github.io/xcode-man-pages/codesign.1.html
- 5: https://developer.apple.com/library/archive/documentation/Security/Conceptual/CodeSigningGuide/AboutCS/AboutCS.html
- 6: https://help.apple.com/xcode/mac/current/en.lproj/dev1cc22a95c.html
- 7: https://apple-docs.everest.mt/docs/security/notarizing-macos-software-before-distribution/
- 8: https://developer.apple.com/forums/thread/720093
- 9: https://developer.apple.com/forums/thread/710738
- 10: https://eclecticlight.co/2019/05/31/can-you-tell-whether-code-has-been-notarized/
🏁 Script executed:
python3 - <<'PY'
from pathlib import Path
text = Path('.github/workflows/release.yml').read_text().splitlines()
for i, line in enumerate(text, 1):
if 'spctl' in line or 'stapler' in line or 'codesign' in line:
start = max(1, i-3)
end = min(len(text), i+3)
print(f'--- around line {i} ---')
for j in range(start, end+1):
print(f'{j:4}: {text[j-1]}')
PYRepository: DevVig/microbridge
Length of output: 3119
🏁 Script executed:
python3 - <<'PY'
import subprocess, textwrap, json, os, sys
# Probe local tool availability only; do not modify files.
for cmd in [
["bash","-lc","command -v gh || true"],
["bash","-lc","command -v jq || true"],
]:
r = subprocess.run(cmd, capture_output=True, text=True)
print("CMD:", " ".join(cmd))
print(r.stdout.strip() or "<no stdout>")
PYRepository: DevVig/microbridge
Length of output: 252
Make release validation fail closed. .github/workflows/release.yml:159-160
spctl --assess and codesign -dv here are only diagnostics, and || true lets the release continue even when the DMG is untrusted or unstapled. Replace this with a real verification step that fails the job, e.g. codesign --verify --deep --strict and stapler validate.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/release.yml around lines 159 - 160, Update the release
validation commands in the workflow to fail the job when signing or notarization
verification fails: replace the diagnostic-only spctl and codesign invocations
and remove their `|| true` suppression, using strict deep code-signature
verification and stapler validation for the generated DMG or app artifacts.
| arch-specific menu bar app). On macOS, releases also include a | ||
| **Developer ID–signed and notarized** DMG | ||
| (`microbridge-ui-<tag>-<arch>.dmg`). |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Use <target> in the documented DMG filename.
The published suffix is the complete target triple, such as aarch64-apple-darwin, rather than only the architecture.
-(`microbridge-ui-<tag>-<arch>.dmg`).
+(`microbridge-ui-<tag>-<target>.dmg`, for example
+`microbridge-ui-<tag>-aarch64-apple-darwin.dmg`).📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| arch-specific menu bar app). On macOS, releases also include a | |
| **Developer ID–signed and notarized** DMG | |
| (`microbridge-ui-<tag>-<arch>.dmg`). | |
| arch-specific menu bar app). On macOS, releases also include a | |
| **Developer ID–signed and notarized** DMG | |
| (`microbridge-ui-<tag>-<target>.dmg`, for example | |
| `microbridge-ui-<tag>-aarch64-apple-darwin.dmg`). |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@INSTALL.md` around lines 86 - 88, Update the documented macOS DMG filename in
INSTALL.md to use the complete <target> suffix, such as aarch64-apple-darwin,
instead of only <arch>; preserve the existing tag placeholder and filename
structure.
| UI_PLIST="$HOME/Library/LaunchAgents/${UI_LABEL}.plist" | ||
| cat >"$UI_PLIST" <<EOF | ||
| DEST="$HOME/Applications/Microbridge.app" | ||
| MARKER="$DEST/.microbridge-release" |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the relevant installer script section with line numbers.
sed -n '80,140p' scripts/install-from-release.sh | cat -n
# Show the full file length so I can judge whether a wider slice is needed.
wc -l scripts/install-from-release.shRepository: DevVig/microbridge
Length of output: 2587
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find other macOS installer/signing-related logic.
rg -n --hidden --glob '!**/.git/**' 'codesign|spctl|quarantine|\.microbridge-release|release-managed|MARKER=' .
# Inspect the full installer script around the marker and install flow.
sed -n '1,220p' scripts/install-from-release.sh | cat -nRepository: DevVig/microbridge
Length of output: 8623
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the release workflow sections that sign the app and build the DMG.
sed -n '120,180p' .github/workflows/release.yml | cat -nRepository: DevVig/microbridge
Length of output: 3138
Move the release marker out of Microbridge.app. The release workflow signs the app bundle, and writing .microbridge-release after copying mutates that signed bundle and can break Gatekeeper/code-signature validation. Store the marker in a separate state directory instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install-from-release.sh` at line 106, Update the release marker
assignment in the install script so MARKER points to a separate state directory
outside Microbridge.app, rather than placing .microbridge-release under DEST.
Ensure the directory exists before the marker is written, while preserving the
existing marker read/write behavior.
| rm -rf "$DEST" | ||
| mkdir -p "$HOME/Applications" | ||
| cp -R "$APP_SRC" "$DEST" | ||
| xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the installer script around the referenced lines.
git ls-files scripts/install-from-release.sh
wc -l scripts/install-from-release.sh
cat -n scripts/install-from-release.sh | sed -n '90,180p'Repository: DevVig/microbridge
Length of output: 4260
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect surrounding logic and any other quarantine / assessment handling in the repo.
rg -n "xattr -dr com.apple.quarantine|spctl --assess|hdiutil attach|curl -fsSL -o" scripts/install-from-release.shRepository: DevVig/microbridge
Length of output: 591
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check whether release packaging signs/notarizes the macOS artifacts.
rg -n "codesign|notarytool|altool|spctl|hdiutil create|create-dmg|productbuild|pkgbuild|xcrun" .github scripts src .Repository: DevVig/microbridge
Length of output: 889
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect any release or packaging workflow that may explain whether the DMG is signed/notarized.
fd -a -H -t f .github scriptsRepository: DevVig/microbridge
Length of output: 156
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Read the release workflow around DMG creation/assessment.
cat -n .github/workflows/release.yml | sed -n '70,170p'Repository: DevVig/microbridge
Length of output: 5746
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the installer path that strips quarantine and whether any later step re-adds it.
cat -n scripts/install-from-release.sh | sed -n '145,185p'Repository: DevVig/microbridge
Length of output: 2075
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check if the installer itself documents expected Gatekeeper behavior.
rg -n "quarantine|Gatekeeper|notar|signed DMG|spctl|security" scripts/install-from-release.sh README* .github/workflows/release.ymlRepository: DevVig/microbridge
Length of output: 1877
Keep quarantine on the installed app bundle
install_app_bundle() clears com.apple.quarantine for both the DMG and tarball paths, which suppresses Gatekeeper for the downloaded app. Leave the xattr intact and let macOS enforce trust on first launch.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@scripts/install-from-release.sh` at line 117, Remove the xattr command that
clears com.apple.quarantine from install_app_bundle(), including the DMG and
tarball installation paths, so the installed app bundle retains quarantine and
macOS can enforce Gatekeeper on first launch.
There was a problem hiding this comment.
Pull request overview
This PR updates the release pipeline to produce Developer ID–signed and notarized macOS DMG assets for the Tauri UI app, and updates installer/docs so macOS users prefer the DMG path when installing from GitHub Releases.
Changes:
- Add macOS signing + notarization steps in the release workflow and attach
.dmgassets alongside existing.tar.gzarchives. - Update
install-from-release.shto prefer downloading/installing from the DMG on macOS, with archive fallback. - Document the DMG install path and required CI secrets (new
docs/macos-signing.md), and enable hardened runtime + entitlements in Tauri config.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/install-from-release.sh | Prefer installing UI from DMG on macOS; refactors UI install into a helper function. |
| INSTALL.md | Documents notarized DMG assets and install flow on macOS. |
| docs/macos-signing.md | New documentation for CI signing/notarization secrets and process. |
| apps/microbridge-ui/src-tauri/tauri.conf.json | Enables hardened runtime and points to entitlements for macOS bundles. |
| apps/microbridge-ui/src-tauri/Entitlements.plist | Adds macOS entitlements used during signing/hardened runtime. |
| .github/workflows/release.yml | Imports Developer ID cert, configures ASC API key, builds DMG, and publishes DMG assets. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| DEST="$HOME/Applications/Microbridge.app" | ||
| MARKER="$DEST/.microbridge-release" | ||
| install_app_bundle() { | ||
| local APP_SRC="$1" | ||
| if [[ -d "$DEST" && ! -f "$MARKER" && "${MICROBRIDGE_FORCE_APP:-}" != "1" ]]; then | ||
| echo " warning: $DEST exists and is not release-managed — leave it" | ||
| echo " set MICROBRIDGE_FORCE_APP=1 to replace" | ||
| return 0 | ||
| fi | ||
| rm -rf "$DEST" | ||
| mkdir -p "$HOME/Applications" | ||
| cp -R "$APP_SRC" "$DEST" | ||
| xattr -dr com.apple.quarantine "$DEST" 2>/dev/null || true | ||
| echo "owned-by-release" >"$MARKER" |
| test -n "${APPLE_API_KEY:-}" | ||
| test -n "${APPLE_API_ISSUER:-}" | ||
| test -n "${APPLE_API_KEY_P8:-}" | ||
| KEY_PATH="$RUNNER_TEMP/AuthKey_${APPLE_API_KEY}.p8" | ||
| printf '%s\n' "$APPLE_API_KEY_P8" > "$KEY_PATH" | ||
| chmod 600 "$KEY_PATH" | ||
| { | ||
| echo "APPLE_API_KEY=$APPLE_API_KEY" | ||
| echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" | ||
| echo "APPLE_API_KEY_PATH=$KEY_PATH" | ||
| echo "APPLE_TEAM_ID=${APPLE_TEAM_ID:-3NQG568C4Q}" | ||
| } >> "$GITHUB_ENV" |
| echo "Packed signed DMG: $DMG_OUT" | ||
| spctl --assess --type open --context context:primary-signature "$DMG_OUT" || true | ||
| codesign -dv --verbose=2 "$APP" || true |
| <key>com.apple.security.cs.allow-jit</key> | ||
| <true/> | ||
| <key>com.apple.security.cs.allow-unsigned-executable-memory</key> | ||
| <true/> | ||
| <key>com.apple.security.cs.disable-library-validation</key> |
Summary
app+dmgwith Tauri, and notarizes via App Store Connect API keymicrobridge-ui-<tag>-<target>.dmg) attach to GitHub Releases alongside.apptarballsinstall-from-release.shprefers the notarized DMG on macOS; docs cover required secretsSecrets (already set on DevVig/microbridge)
APPLE_CERTIFICATE,APPLE_CERTIFICATE_PASSWORD,APPLE_SIGNING_IDENTITY,KEYCHAIN_PASSWORD,APPLE_API_KEY,APPLE_API_ISSUER,APPLE_API_KEY_P8,APPLE_TEAM_IDTest plan
v0.0.x) or dry-run on this branch via a temporary tagspctl --assesssucceeds./scripts/install-from-release.sh <tag>installs from DMG pathMade with Cursor
Summary by CodeRabbit
New Features
Documentation