Skip to content

Commit 9e2517e

Browse files
author
Evgeniy
committed
Add Apple Developer ID codesigning and notarization to CI/CD
- Conditional codesigning: uses Developer ID cert when MACOS_CERTIFICATE_P12 secret is set, falls back to ad-hoc signing for forks/PRs - Hardened runtime (--options runtime) for notarization compatibility - DMG signing with Developer ID when available - Notarization via xcrun notarytool + stapler when notarization secrets are set - Temporary keychain with automatic cleanup - README: add codesigning setup instructions with secret names and export steps
1 parent 4cfcae0 commit 9e2517e

2 files changed

Lines changed: 309 additions & 40 deletions

File tree

.github/workflows/build.yml

Lines changed: 275 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ on:
77
pull_request:
88
branches: [ main ]
99

10+
env:
11+
KEYCHAIN_NAME: build.keychain-db
12+
KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }}
13+
1014
jobs:
1115
test:
1216
name: Run Tests
@@ -39,24 +43,82 @@ jobs:
3943
- name: Select Xcode
4044
run: sudo xcode-select -s /Applications/Xcode_15.4.app/Contents/Developer || sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
4145

42-
- name: Build arm64
46+
- name: Setup codesigning
47+
if: ${{ secrets.MACOS_CERTIFICATE_P12 != '' }}
48+
env:
49+
CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }}
50+
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
4351
run: |
44-
xcodebuild -project LangSwitcher.xcodeproj \
45-
-scheme LangSwitcher \
46-
-configuration Release \
47-
-derivedDataPath build \
48-
-arch arm64 \
49-
ONLY_ACTIVE_ARCH=NO \
50-
CODE_SIGN_IDENTITY="-" \
51-
CODE_SIGNING_REQUIRED=NO \
52-
CODE_SIGNING_ALLOWED=NO \
53-
build
52+
# Decode certificate
53+
echo "$CERTIFICATE_P12" | base64 --decode > certificate.p12
54+
55+
# Create temporary keychain
56+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
57+
security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME"
58+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
59+
60+
# Import certificate into keychain
61+
security import certificate.p12 \
62+
-k "$KEYCHAIN_NAME" \
63+
-P "$CERTIFICATE_PASSWORD" \
64+
-T /usr/bin/codesign \
65+
-T /usr/bin/security
66+
67+
# Allow codesign to access keychain without prompting
68+
security set-key-partition-list -S apple-tool:,apple:,codesign: \
69+
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
70+
71+
# Add keychain to search list
72+
security list-keychains -d user -s "$KEYCHAIN_NAME" $(security list-keychains -d user | tr -d '"')
73+
74+
# Clean up certificate file
75+
rm certificate.p12
76+
77+
echo "CODESIGN_IDENTITY=${{ secrets.MACOS_SIGNING_IDENTITY }}" >> $GITHUB_ENV
78+
echo "CODESIGN_ENABLED=true" >> $GITHUB_ENV
5479
55-
- name: Ad-hoc sign app bundle
80+
- name: Set ad-hoc signing (fallback)
81+
if: ${{ secrets.MACOS_CERTIFICATE_P12 == '' }}
82+
run: |
83+
echo "CODESIGN_IDENTITY=-" >> $GITHUB_ENV
84+
echo "CODESIGN_ENABLED=false" >> $GITHUB_ENV
85+
86+
- name: Build arm64
87+
run: |
88+
if [ "$CODESIGN_ENABLED" = "true" ]; then
89+
xcodebuild -project LangSwitcher.xcodeproj \
90+
-scheme LangSwitcher \
91+
-configuration Release \
92+
-derivedDataPath build \
93+
-arch arm64 \
94+
ONLY_ACTIVE_ARCH=NO \
95+
CODE_SIGN_IDENTITY="$CODESIGN_IDENTITY" \
96+
CODE_SIGN_STYLE=Manual \
97+
build
98+
else
99+
xcodebuild -project LangSwitcher.xcodeproj \
100+
-scheme LangSwitcher \
101+
-configuration Release \
102+
-derivedDataPath build \
103+
-arch arm64 \
104+
ONLY_ACTIVE_ARCH=NO \
105+
CODE_SIGN_IDENTITY="-" \
106+
CODE_SIGNING_REQUIRED=NO \
107+
CODE_SIGNING_ALLOWED=NO \
108+
build
109+
fi
110+
111+
- name: Sign app bundle
56112
run: |
57113
APP_PATH="build/Build/Products/Release/LangSwitcher.app"
58-
# Strip linker-only signature and re-sign entire bundle (seals resources + Info.plist)
59-
codesign --force --deep -s - "$APP_PATH"
114+
if [ "$CODESIGN_ENABLED" = "true" ]; then
115+
echo "=== Signing with Developer ID ==="
116+
codesign --force --options runtime --deep \
117+
-s "$CODESIGN_IDENTITY" "$APP_PATH"
118+
else
119+
echo "=== Ad-hoc signing ==="
120+
codesign --force --deep -s - "$APP_PATH"
121+
fi
60122
echo "=== Codesign verify ==="
61123
codesign -dvvv "$APP_PATH" 2>&1
62124
echo "=== spctl assess ==="
@@ -89,6 +151,11 @@ jobs:
89151
-ov -format UDZO \
90152
"$DMG_NAME"
91153
154+
# Sign the DMG itself if Developer ID is available
155+
if [ "$CODESIGN_ENABLED" = "true" ]; then
156+
codesign --force -s "$CODESIGN_IDENTITY" "$DMG_NAME"
157+
fi
158+
92159
echo "DMG_NAME=$DMG_NAME" >> $GITHUB_ENV
93160
94161
- name: Create ZIP with ditto (preserves permissions)
@@ -98,6 +165,28 @@ jobs:
98165
VERSION="${VERSION#v}"
99166
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "LangSwitcher-${VERSION}-arm64.zip"
100167
168+
- name: Notarize DMG
169+
if: ${{ secrets.MACOS_CERTIFICATE_P12 != '' && secrets.MACOS_NOTARIZATION_APPLE_ID != '' }}
170+
env:
171+
APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
172+
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
173+
TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
174+
run: |
175+
echo "=== Submitting DMG for notarization ==="
176+
xcrun notarytool submit "$DMG_NAME" \
177+
--apple-id "$APPLE_ID" \
178+
--password "$APPLE_PASSWORD" \
179+
--team-id "$TEAM_ID" \
180+
--wait
181+
182+
echo "=== Stapling notarization ticket to DMG ==="
183+
xcrun stapler staple "$DMG_NAME"
184+
185+
- name: Cleanup keychain
186+
if: ${{ always() && secrets.MACOS_CERTIFICATE_P12 != '' }}
187+
run: |
188+
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
189+
101190
- name: Upload arm64 DMG
102191
uses: actions/upload-artifact@v4
103192
with:
@@ -123,23 +212,70 @@ jobs:
123212
- name: Select Xcode
124213
run: sudo xcode-select -s /Applications/Xcode_15.4.app/Contents/Developer || sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
125214

126-
- name: Build x86_64
215+
- name: Setup codesigning
216+
if: ${{ secrets.MACOS_CERTIFICATE_P12 != '' }}
217+
env:
218+
CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }}
219+
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
127220
run: |
128-
xcodebuild -project LangSwitcher.xcodeproj \
129-
-scheme LangSwitcher \
130-
-configuration Release \
131-
-derivedDataPath build \
132-
-arch x86_64 \
133-
ONLY_ACTIVE_ARCH=NO \
134-
CODE_SIGN_IDENTITY="-" \
135-
CODE_SIGNING_REQUIRED=NO \
136-
CODE_SIGNING_ALLOWED=NO \
137-
build
221+
echo "$CERTIFICATE_P12" | base64 --decode > certificate.p12
222+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
223+
security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME"
224+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
225+
security import certificate.p12 \
226+
-k "$KEYCHAIN_NAME" \
227+
-P "$CERTIFICATE_PASSWORD" \
228+
-T /usr/bin/codesign \
229+
-T /usr/bin/security
230+
security set-key-partition-list -S apple-tool:,apple:,codesign: \
231+
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
232+
security list-keychains -d user -s "$KEYCHAIN_NAME" $(security list-keychains -d user | tr -d '"')
233+
rm certificate.p12
234+
echo "CODESIGN_IDENTITY=${{ secrets.MACOS_SIGNING_IDENTITY }}" >> $GITHUB_ENV
235+
echo "CODESIGN_ENABLED=true" >> $GITHUB_ENV
236+
237+
- name: Set ad-hoc signing (fallback)
238+
if: ${{ secrets.MACOS_CERTIFICATE_P12 == '' }}
239+
run: |
240+
echo "CODESIGN_IDENTITY=-" >> $GITHUB_ENV
241+
echo "CODESIGN_ENABLED=false" >> $GITHUB_ENV
138242
139-
- name: Ad-hoc sign app bundle
243+
- name: Build x86_64
244+
run: |
245+
if [ "$CODESIGN_ENABLED" = "true" ]; then
246+
xcodebuild -project LangSwitcher.xcodeproj \
247+
-scheme LangSwitcher \
248+
-configuration Release \
249+
-derivedDataPath build \
250+
-arch x86_64 \
251+
ONLY_ACTIVE_ARCH=NO \
252+
CODE_SIGN_IDENTITY="$CODESIGN_IDENTITY" \
253+
CODE_SIGN_STYLE=Manual \
254+
build
255+
else
256+
xcodebuild -project LangSwitcher.xcodeproj \
257+
-scheme LangSwitcher \
258+
-configuration Release \
259+
-derivedDataPath build \
260+
-arch x86_64 \
261+
ONLY_ACTIVE_ARCH=NO \
262+
CODE_SIGN_IDENTITY="-" \
263+
CODE_SIGNING_REQUIRED=NO \
264+
CODE_SIGNING_ALLOWED=NO \
265+
build
266+
fi
267+
268+
- name: Sign app bundle
140269
run: |
141270
APP_PATH="build/Build/Products/Release/LangSwitcher.app"
142-
codesign --force --deep -s - "$APP_PATH"
271+
if [ "$CODESIGN_ENABLED" = "true" ]; then
272+
echo "=== Signing with Developer ID ==="
273+
codesign --force --options runtime --deep \
274+
-s "$CODESIGN_IDENTITY" "$APP_PATH"
275+
else
276+
echo "=== Ad-hoc signing ==="
277+
codesign --force --deep -s - "$APP_PATH"
278+
fi
143279
echo "=== Codesign verify ==="
144280
codesign -dvvv "$APP_PATH" 2>&1
145281
echo "=== spctl assess ==="
@@ -172,6 +308,10 @@ jobs:
172308
-ov -format UDZO \
173309
"$DMG_NAME"
174310
311+
if [ "$CODESIGN_ENABLED" = "true" ]; then
312+
codesign --force -s "$CODESIGN_IDENTITY" "$DMG_NAME"
313+
fi
314+
175315
echo "DMG_NAME=$DMG_NAME" >> $GITHUB_ENV
176316
177317
- name: Create ZIP with ditto (preserves permissions)
@@ -181,6 +321,28 @@ jobs:
181321
VERSION="${VERSION#v}"
182322
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "LangSwitcher-${VERSION}-x86_64.zip"
183323
324+
- name: Notarize DMG
325+
if: ${{ secrets.MACOS_CERTIFICATE_P12 != '' && secrets.MACOS_NOTARIZATION_APPLE_ID != '' }}
326+
env:
327+
APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
328+
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
329+
TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
330+
run: |
331+
echo "=== Submitting DMG for notarization ==="
332+
xcrun notarytool submit "$DMG_NAME" \
333+
--apple-id "$APPLE_ID" \
334+
--password "$APPLE_PASSWORD" \
335+
--team-id "$TEAM_ID" \
336+
--wait
337+
338+
echo "=== Stapling notarization ticket to DMG ==="
339+
xcrun stapler staple "$DMG_NAME"
340+
341+
- name: Cleanup keychain
342+
if: ${{ always() && secrets.MACOS_CERTIFICATE_P12 != '' }}
343+
run: |
344+
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
345+
184346
- name: Upload x86_64 DMG
185347
uses: actions/upload-artifact@v4
186348
with:
@@ -206,23 +368,70 @@ jobs:
206368
- name: Select Xcode
207369
run: sudo xcode-select -s /Applications/Xcode_15.4.app/Contents/Developer || sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
208370

209-
- name: Build Universal (arm64 + x86_64)
371+
- name: Setup codesigning
372+
if: ${{ secrets.MACOS_CERTIFICATE_P12 != '' }}
373+
env:
374+
CERTIFICATE_P12: ${{ secrets.MACOS_CERTIFICATE_P12 }}
375+
CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PASSWORD }}
210376
run: |
211-
xcodebuild -project LangSwitcher.xcodeproj \
212-
-scheme LangSwitcher \
213-
-configuration Release \
214-
-derivedDataPath build \
215-
-arch arm64 -arch x86_64 \
216-
ONLY_ACTIVE_ARCH=NO \
217-
CODE_SIGN_IDENTITY="-" \
218-
CODE_SIGNING_REQUIRED=NO \
219-
CODE_SIGNING_ALLOWED=NO \
220-
build
377+
echo "$CERTIFICATE_P12" | base64 --decode > certificate.p12
378+
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
379+
security set-keychain-settings -lut 21600 "$KEYCHAIN_NAME"
380+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
381+
security import certificate.p12 \
382+
-k "$KEYCHAIN_NAME" \
383+
-P "$CERTIFICATE_PASSWORD" \
384+
-T /usr/bin/codesign \
385+
-T /usr/bin/security
386+
security set-key-partition-list -S apple-tool:,apple:,codesign: \
387+
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_NAME"
388+
security list-keychains -d user -s "$KEYCHAIN_NAME" $(security list-keychains -d user | tr -d '"')
389+
rm certificate.p12
390+
echo "CODESIGN_IDENTITY=${{ secrets.MACOS_SIGNING_IDENTITY }}" >> $GITHUB_ENV
391+
echo "CODESIGN_ENABLED=true" >> $GITHUB_ENV
392+
393+
- name: Set ad-hoc signing (fallback)
394+
if: ${{ secrets.MACOS_CERTIFICATE_P12 == '' }}
395+
run: |
396+
echo "CODESIGN_IDENTITY=-" >> $GITHUB_ENV
397+
echo "CODESIGN_ENABLED=false" >> $GITHUB_ENV
221398
222-
- name: Ad-hoc sign app bundle
399+
- name: Build Universal (arm64 + x86_64)
400+
run: |
401+
if [ "$CODESIGN_ENABLED" = "true" ]; then
402+
xcodebuild -project LangSwitcher.xcodeproj \
403+
-scheme LangSwitcher \
404+
-configuration Release \
405+
-derivedDataPath build \
406+
-arch arm64 -arch x86_64 \
407+
ONLY_ACTIVE_ARCH=NO \
408+
CODE_SIGN_IDENTITY="$CODESIGN_IDENTITY" \
409+
CODE_SIGN_STYLE=Manual \
410+
build
411+
else
412+
xcodebuild -project LangSwitcher.xcodeproj \
413+
-scheme LangSwitcher \
414+
-configuration Release \
415+
-derivedDataPath build \
416+
-arch arm64 -arch x86_64 \
417+
ONLY_ACTIVE_ARCH=NO \
418+
CODE_SIGN_IDENTITY="-" \
419+
CODE_SIGNING_REQUIRED=NO \
420+
CODE_SIGNING_ALLOWED=NO \
421+
build
422+
fi
423+
424+
- name: Sign app bundle
223425
run: |
224426
APP_PATH="build/Build/Products/Release/LangSwitcher.app"
225-
codesign --force --deep -s - "$APP_PATH"
427+
if [ "$CODESIGN_ENABLED" = "true" ]; then
428+
echo "=== Signing with Developer ID ==="
429+
codesign --force --options runtime --deep \
430+
-s "$CODESIGN_IDENTITY" "$APP_PATH"
431+
else
432+
echo "=== Ad-hoc signing ==="
433+
codesign --force --deep -s - "$APP_PATH"
434+
fi
226435
echo "=== Codesign verify ==="
227436
codesign -dvvv "$APP_PATH" 2>&1
228437
echo "=== spctl assess ==="
@@ -255,6 +464,10 @@ jobs:
255464
-ov -format UDZO \
256465
"$DMG_NAME"
257466
467+
if [ "$CODESIGN_ENABLED" = "true" ]; then
468+
codesign --force -s "$CODESIGN_IDENTITY" "$DMG_NAME"
469+
fi
470+
258471
echo "DMG_NAME=$DMG_NAME" >> $GITHUB_ENV
259472
260473
- name: Create ZIP with ditto (preserves permissions)
@@ -264,6 +477,28 @@ jobs:
264477
VERSION="${VERSION#v}"
265478
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "LangSwitcher-${VERSION}-universal.zip"
266479
480+
- name: Notarize DMG
481+
if: ${{ secrets.MACOS_CERTIFICATE_P12 != '' && secrets.MACOS_NOTARIZATION_APPLE_ID != '' }}
482+
env:
483+
APPLE_ID: ${{ secrets.MACOS_NOTARIZATION_APPLE_ID }}
484+
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARIZATION_PASSWORD }}
485+
TEAM_ID: ${{ secrets.MACOS_NOTARIZATION_TEAM_ID }}
486+
run: |
487+
echo "=== Submitting DMG for notarization ==="
488+
xcrun notarytool submit "$DMG_NAME" \
489+
--apple-id "$APPLE_ID" \
490+
--password "$APPLE_PASSWORD" \
491+
--team-id "$TEAM_ID" \
492+
--wait
493+
494+
echo "=== Stapling notarization ticket to DMG ==="
495+
xcrun stapler staple "$DMG_NAME"
496+
497+
- name: Cleanup keychain
498+
if: ${{ always() && secrets.MACOS_CERTIFICATE_P12 != '' }}
499+
run: |
500+
security delete-keychain "$KEYCHAIN_NAME" 2>/dev/null || true
501+
267502
- name: Upload Universal DMG
268503
uses: actions/upload-artifact@v4
269504
with:

0 commit comments

Comments
 (0)