-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathrelease-sign.sh
More file actions
executable file
·180 lines (155 loc) · 6.29 KB
/
Copy pathrelease-sign.sh
File metadata and controls
executable file
·180 lines (155 loc) · 6.29 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
#!/usr/bin/env bash
echo "Apple distribution and package generation are suspended." >&2
exit 1
# release-sign.sh <tag>
#
# One-shot post-tag step for macOS distribution.
#
# Prerequisite: you've already pushed the tag (e.g. `git push origin v0.4.0`),
# and the GitHub Actions release workflow is running or finished.
#
# This script:
# 1. waits for the Release's unsigned MD-Preview-macOS-universal.dmg to land
# 2. downloads it
# 3. signs + notarizes + staples (both inner .app AND the dmg) via the
# local-first remote-mac-sign dispatcher
# 4. uploads the signed dmg back to the Release, overwriting the unsigned one
# 5. generates and uploads Sparkle appcast.xml for macOS in-app self-update
# 6. drops the stapled .app into target/ and /Applications if that copy exists
#
# Expected end state: the Release's macOS dmg and the .app inside it both
# pass `stapler validate`, `codesign --verify`, and `spctl --assess` —
# Gatekeeper accepts them offline, no warnings for end users.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
TAG="${1:-}"
if [ -z "$TAG" ]; then
echo "usage: $0 <tag> e.g. $0 v0.4.0" >&2
exit 1
fi
WORK=""
cleanup() {
local rc=$?
if [ -n "$WORK" ] && [ -d "$WORK" ]; then
rm -rf "$WORK" 2>/dev/null || true
fi
if [ "$rc" -ne 0 ] && command -v osascript >/dev/null 2>&1; then
osascript -e "display notification \"${TAG:-?} signing FAILED (rc=$rc). See target/release-sign-${TAG:-unknown}.log\" with title \"md-preview signing FAILED\"" >/dev/null 2>&1 || true
fi
}
trap cleanup EXIT
REPO="vorojar/md-preview"
ASSET="MD-Preview-macOS-universal.dmg"
SIGN_SCRIPT="${MD_PREVIEW_SIGN_SCRIPT:-$HOME/.claude/skills/remote-mac-sign/sign.sh}"
SIGN_ATTEMPTS="${MD_PREVIEW_SIGN_ATTEMPTS:-2}"
if [ ! -x "$SIGN_SCRIPT" ]; then
echo "error: signing script not found or not executable at $SIGN_SCRIPT" >&2
exit 2
fi
if [[ ! "$SIGN_ATTEMPTS" =~ ^[1-9][0-9]*$ ]]; then
echo "error: MD_PREVIEW_SIGN_ATTEMPTS must be a positive integer" >&2
exit 2
fi
WORK=$(mktemp -d)
# Remove any stale sentinel from a previous run of the same tag, so a
# waiter doesn't see an old DONE and think this one finished instantly.
rm -f "$REPO_ROOT/target/.release-sign.done.$TAG" 2>/dev/null || true
echo "[1/6] waiting for $TAG Release to expose $ASSET (poll 15s, up to 15min)..."
for i in $(seq 1 60); do
if gh release view "$TAG" -R "$REPO" --json assets -q \
".assets[] | select(.name==\"$ASSET\") | .name" 2>/dev/null \
| grep -q "^$ASSET$"; then
echo " found."
break
fi
if [ "$i" -eq 60 ]; then
echo " timed out waiting for $ASSET; check GH Actions for failures." >&2
exit 3
fi
sleep 15
done
echo "[2/6] downloading unsigned dmg..."
cd "$WORK"
gh release download "$TAG" -R "$REPO" -p "$ASSET" --clobber
echo "[3/6] signing + notarizing + stapling (this takes ~5min, Apple notary x2)..."
for attempt in $(seq 1 "$SIGN_ATTEMPTS"); do
rm -rf "$WORK/signed-output"
echo " signing attempt $attempt/$SIGN_ATTEMPTS via $SIGN_SCRIPT"
if "$SIGN_SCRIPT" "$WORK/$ASSET"; then
break
fi
rc=$?
if [ "$attempt" -eq "$SIGN_ATTEMPTS" ]; then
echo " signing failed after $SIGN_ATTEMPTS attempt(s), last rc=$rc" >&2
exit "$rc"
fi
echo " signing failed with rc=$rc; retrying in 20s..."
sleep 20
done
SIGNED="$WORK/signed-output/signed_$ASSET"
if [ ! -f "$SIGNED" ]; then
echo " signing returned no output at $SIGNED" >&2
exit 4
fi
# Quick sanity check before uploading.
xcrun stapler validate "$SIGNED" >/dev/null || { echo " stapler validate failed" >&2; exit 5; }
spctl -a -t open --context context:primary-signature "$SIGNED" >/dev/null 2>&1 \
|| { echo " spctl assess failed" >&2; exit 6; }
VERIFY_MOUNT="$WORK/verify-mount"
mkdir -p "$VERIFY_MOUNT"
hdiutil attach "$SIGNED" -nobrowse -mountpoint "$VERIFY_MOUNT" >/dev/null
VERIFY_APP="$VERIFY_MOUNT/MD Preview.app"
VERIFY_EXTENSION="$VERIFY_APP/Contents/PlugIns/MDPreviewFinderExtension.appex"
if ! codesign --verify --deep --strict --verbose=2 "$VERIFY_APP" >/dev/null 2>&1 \
|| ! codesign -d --entitlements :- "$VERIFY_EXTENSION" 2>&1 \
| grep -q 'com.apple.security.app-sandbox'; then
hdiutil detach "$VERIFY_MOUNT" >/dev/null 2>&1 || true
echo " Finder extension signature or sandbox entitlement missing" >&2
exit 7
fi
hdiutil detach "$VERIFY_MOUNT" >/dev/null
echo "[4/6] uploading signed dmg to $TAG (replacing unsigned)..."
cp "$SIGNED" "$WORK/$ASSET"
gh release upload "$TAG" "$WORK/$ASSET" -R "$REPO" --clobber
echo "[5/6] generating Sparkle appcast..."
APPCAST="$WORK/appcast.xml"
"$REPO_ROOT/scripts/generate-appcast.sh" "$TAG" "$WORK/$ASSET" "$APPCAST" >/dev/null
gh release upload "$TAG" "$APPCAST" -R "$REPO" --clobber
echo "[6/6] deploying stapled dmg + .app locally..."
mkdir -p "$REPO_ROOT/target"
# Keep a copy of the signed dmg in target/ so it's visible in the repo checkout.
LOCAL_DMG="$REPO_ROOT/target/$ASSET"
cp "$SIGNED" "$LOCAL_DMG"
echo " saved $LOCAL_DMG"
MOUNT=$(mktemp -d)/mnt
mkdir -p "$MOUNT"
hdiutil attach "$SIGNED" -nobrowse -mountpoint "$MOUNT" >/dev/null
TARGET_APP="$REPO_ROOT/target/MD Preview.app"
if [ -d "$MOUNT/MD Preview.app" ]; then
rm -rf "$TARGET_APP"
ditto "$MOUNT/MD Preview.app" "$TARGET_APP"
echo " replaced $TARGET_APP"
fi
APPL="/Applications/MD Preview.app"
if [ -d "$APPL" ] && [ -w "/Applications" ]; then
rm -rf "$APPL"
ditto "$MOUNT/MD Preview.app" "$APPL"
echo " replaced $APPL"
elif [ -d "$APPL" ]; then
echo " /Applications not writable; skipping system-wide replace"
fi
hdiutil detach "$MOUNT" >/dev/null
echo ""
echo "DONE. $TAG: dmg and inner .app both signed + notarized + stapled."
echo "Release: https://github.com/$REPO/releases/tag/$TAG"
# Write a sentinel file with the tag in its name. Anyone waiting on this
# pipeline (or me in an interactive session) polls for file existence — no
# regex against log timestamps, no mv of a live log. Removed at the top of
# the next run so stale sentinels don't trigger false positives.
SENTINEL="$REPO_ROOT/target/.release-sign.done.$TAG"
touch "$SENTINEL"
echo " sentinel: $SENTINEL"
# macOS notification (for background runs triggered by the pre-push hook).
if command -v osascript >/dev/null 2>&1; then
osascript -e "display notification \"$TAG dmg + .app signed, notarized, stapled.\" with title \"md-preview release signed\"" >/dev/null 2>&1 || true
fi