Skip to content

Commit c5ccdf1

Browse files
feat(desktop): Developer ID codesign Munim Mac DMGs on publish
Post-sign the packaged .app/DMG/ZIP after electron-builder so releases ship with the keychain Developer ID identity; notarize when ASC API issuer env is set. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b671c08 commit c5ccdf1

1 file changed

Lines changed: 133 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
#!/usr/bin/env bash
2+
# Developer ID codesign the .app inside a Munim Mac DMG, then rebuild DMG + ZIP.
3+
# Notarization requires an App Store Connect API Issuer ID (UUID) — set
4+
# APPLE_API_ISSUER (+ APPLE_API_KEY / APPLE_API_KEY_ID) to also submit + staple.
5+
set -euo pipefail
6+
7+
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
8+
9+
DMG="${1:-}"
10+
if [[ -z "$DMG" || ! -f "$DMG" ]]; then
11+
echo "usage: $0 /path/to/MT-Code-*-arm64.dmg" >&2
12+
exit 1
13+
fi
14+
15+
IDENTITY="${T3_PERSONAL_SIGN_IDENTITY:-$(security find-identity -v -p codesigning 2>/dev/null | awk -F'"' '/Developer ID Application/ { print $2; exit }')}"
16+
if [[ -z "$IDENTITY" ]]; then
17+
echo "no Developer ID Application identity in keychain" >&2
18+
exit 1
19+
fi
20+
21+
WORK="$(mktemp -d /tmp/mt-codesign.XXXXXX)"
22+
cleanup() { rm -rf "$WORK"; }
23+
trap cleanup EXIT
24+
25+
echo "codesigning with: $IDENTITY"
26+
echo "dmg: $DMG"
27+
28+
ATTACH_OUT="$(hdiutil attach -nobrowse -readonly "$DMG")"
29+
MOUNT="$(printf '%s\n' "$ATTACH_OUT" | awk '/\/Volumes\//{print substr($0, index($0, "/Volumes/"))}' | tail -1)"
30+
if [[ -z "$MOUNT" || ! -d "$MOUNT" ]]; then
31+
echo "failed to mount DMG" >&2
32+
echo "$ATTACH_OUT" >&2
33+
exit 1
34+
fi
35+
36+
APP_SRC="$(find "$MOUNT" -maxdepth 2 -name '*.app' | head -1)"
37+
if [[ -z "$APP_SRC" ]]; then
38+
hdiutil detach "$MOUNT" -quiet || true
39+
echo "no .app found in DMG" >&2
40+
exit 1
41+
fi
42+
43+
ditto "$APP_SRC" "$WORK/MT Code.app"
44+
hdiutil detach "$MOUNT" -quiet || hdiutil detach "$MOUNT" -force
45+
46+
codesign --deep --force --options runtime --timestamp \
47+
--sign "$IDENTITY" \
48+
"$WORK/MT Code.app"
49+
codesign --verify --deep --strict "$WORK/MT Code.app"
50+
51+
STAGE="$WORK/stage"
52+
mkdir -p "$STAGE"
53+
ditto "$WORK/MT Code.app" "$STAGE/MT Code.app"
54+
ln -s /Applications "$STAGE/Applications"
55+
56+
VOL_NAME="$(basename "$DMG" .dmg | sed 's/MT-Code-/MT Code /; s/-arm64//; s/$/ Installer/')"
57+
SIGNED_DMG="$WORK/signed.dmg"
58+
hdiutil create -volname "$VOL_NAME" -srcfolder "$STAGE" -ov -format UDZO "$SIGNED_DMG"
59+
codesign --force --sign "$IDENTITY" --timestamp "$SIGNED_DMG"
60+
61+
SIGNED_ZIP="$WORK/signed.zip"
62+
ditto -c -k --sequesterRsrc --keepParent "$WORK/MT Code.app" "$SIGNED_ZIP"
63+
64+
# Optional notarization when ASC API issuer is available.
65+
if [[ -n "${APPLE_API_ISSUER:-}" && -n "${APPLE_API_KEY_ID:-}" && -n "${APPLE_API_KEY:-}" ]]; then
66+
KEY_FILE="$WORK/AuthKey.p8"
67+
if [[ -f "${APPLE_API_KEY}" ]]; then
68+
KEY_FILE="$APPLE_API_KEY"
69+
else
70+
printf '%s' "$APPLE_API_KEY" >"$KEY_FILE"
71+
fi
72+
echo "submitting for notarization..."
73+
xcrun notarytool submit "$SIGNED_DMG" \
74+
--key "$KEY_FILE" \
75+
--key-id "$APPLE_API_KEY_ID" \
76+
--issuer "$APPLE_API_ISSUER" \
77+
--wait
78+
xcrun stapler staple "$SIGNED_DMG"
79+
echo "notarized + stapled"
80+
else
81+
echo "skipping notarization (set APPLE_API_ISSUER, APPLE_API_KEY_ID, APPLE_API_KEY to enable)"
82+
fi
83+
84+
cp "$SIGNED_DMG" "$DMG"
85+
ZIP_OUT="${DMG%.dmg}.zip"
86+
cp "$SIGNED_ZIP" "$ZIP_OUT"
87+
88+
# Refresh latest-mac.yml next to the DMG when present.
89+
YML="$(dirname "$DMG")/latest-mac.yml"
90+
if [[ -f "$YML" ]]; then
91+
python3 - "$DMG" "$ZIP_OUT" "$YML" <<'PY'
92+
import base64, hashlib, sys
93+
from datetime import datetime, timezone
94+
from pathlib import Path
95+
96+
dmg, zip_path, yml = map(Path, sys.argv[1:])
97+
98+
def digest(path: Path) -> tuple[str, int]:
99+
h = hashlib.sha512()
100+
with path.open("rb") as f:
101+
for chunk in iter(lambda: f.read(1024 * 1024), b""):
102+
h.update(chunk)
103+
return base64.b64encode(h.digest()).decode(), path.stat().st_size
104+
105+
zip_sha, zip_size = digest(zip_path)
106+
dmg_sha, dmg_size = digest(dmg)
107+
now = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")
108+
# Keep version line if present
109+
version = "0.0.0"
110+
for line in yml.read_text().splitlines():
111+
if line.startswith("version:"):
112+
version = line.split(":", 1)[1].strip()
113+
break
114+
yml.write_text(
115+
f"""version: {version}
116+
files:
117+
- url: {zip_path.name}
118+
sha512: {zip_sha}
119+
size: {zip_size}
120+
- url: {dmg.name}
121+
sha512: {dmg_sha}
122+
size: {dmg_size}
123+
path: {zip_path.name}
124+
sha512: {zip_sha}
125+
releaseDate: '{now}'
126+
"""
127+
)
128+
print(f"updated {yml}")
129+
PY
130+
fi
131+
132+
echo "SIGNED_DMG=$DMG"
133+
echo "SIGNED_ZIP=$ZIP_OUT"

0 commit comments

Comments
 (0)