Skip to content

Commit 08a2902

Browse files
committed
ci(desktop): sign macOS releases and refuse unsigned tagged builds
The release job exported only the Apple ID notarization trio, so the App Store Connect API key path could not be used remotely. It also fell back to an unsigned build whenever the certificate secret was absent, logged a note and exited zero, which ships a release macOS refuses to install updates from. Accept the API key as a base64 secret, materialize it outside the workspace, and gate tagged releases on the existing signing preflight so a missing credential fails the job with the name of what is missing.
1 parent aeddc90 commit 08a2902

3 files changed

Lines changed: 52 additions & 1 deletion

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@pymodel/pythinker-code': patch
3+
---
4+
5+
Sign and notarize the macOS desktop build in the release pipeline, and fail a tagged release outright when the signing credentials are missing instead of quietly shipping an unsigned app.

.github/workflows/desktop-release.yml

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,40 @@ jobs:
136136
env:
137137
IN_CSC_LINK: ${{ secrets.MAC_CSC_LINK }}
138138
IN_CSC_KEY_PASSWORD: ${{ secrets.MAC_CSC_KEY_PASSWORD }}
139+
IN_CSC_NAME: ${{ secrets.MAC_CSC_NAME }}
139140
IN_APPLE_ID: ${{ secrets.APPLE_ID }}
140141
IN_APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
141142
IN_APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
143+
IN_APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
144+
IN_APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
145+
IN_APPLE_API_KEY_P8: ${{ secrets.APPLE_API_KEY_P8 }}
142146
run: |
143-
for name in CSC_LINK CSC_KEY_PASSWORD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do
147+
for name in CSC_LINK CSC_KEY_PASSWORD CSC_NAME APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID APPLE_API_KEY_ID APPLE_API_ISSUER; do
144148
input="IN_${name}"
145149
value="${!input:-}"
146150
if [ -n "$value" ]; then printf '%s<<__EOF__\n%s\n__EOF__\n' "$name" "$value" >> "$GITHUB_ENV"; fi
147151
done
152+
# The App Store Connect key is held as base64 because it is a file, not a
153+
# string. notarytool and electron-builder both want a path, so materialize
154+
# it outside the workspace to keep it out of the packaged app.
155+
if [ -n "${IN_APPLE_API_KEY_P8:-}" ]; then
156+
key_path="${RUNNER_TEMP}/AuthKey.p8"
157+
printf '%s' "$IN_APPLE_API_KEY_P8" | base64 -d > "$key_path"
158+
chmod 600 "$key_path"
159+
echo "APPLE_API_KEY=${key_path}" >> "$GITHUB_ENV"
160+
fi
148161
if [ -z "${IN_CSC_LINK:-}" ]; then
149162
echo 'CSC_IDENTITY_AUTO_DISCOVERY=false' >> "$GITHUB_ENV"
150163
echo 'No macOS signing certificate configured; building unsigned.'
151164
fi
165+
166+
# A tag build that quietly produces an unsigned app is worse than a failed
167+
# one: macOS rejects unsigned updates, so it ships a release users cannot
168+
# install or update from. Manual runs stay free to build unsigned.
169+
- name: Require signing for tagged releases
170+
if: startsWith(github.ref, 'refs/tags/desktop-v')
171+
working-directory: apps/desktop
172+
run: node --import tsx scripts/assert-release-signing.ts
152173
- name: Mint releases-repo token
153174
id: releases_token
154175
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # pinned from v3.2.0
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/** Fail a tagged desktop release that would ship an unsigned or un-notarized macOS build. */
2+
3+
import { spawnSync } from 'node:child_process'
4+
import { assertMacReleaseReady } from './release-preflight'
5+
6+
function listCodeSigningIdentities(): string {
7+
const result = spawnSync('security', ['find-identity', '-v', '-p', 'codesigning'], { encoding: 'utf8' })
8+
if (result.error !== undefined) throw result.error
9+
if (result.status !== 0) throw new Error(`security find-identity exited with ${String(result.status)}`)
10+
return result.stdout
11+
}
12+
13+
try {
14+
const result = assertMacReleaseReady({
15+
env: process.env,
16+
platform: process.platform,
17+
listCodeSigningIdentities,
18+
})
19+
console.log(
20+
`macOS release signing is configured: ${result.identity}; signing via ${result.signing}; notarization via ${result.notarization}`,
21+
)
22+
} catch (error) {
23+
console.error(error instanceof Error ? error.message : String(error))
24+
process.exit(1)
25+
}

0 commit comments

Comments
 (0)