-
Notifications
You must be signed in to change notification settings - Fork 3
383 lines (352 loc) · 16.7 KB
/
Copy pathbuild.yml
File metadata and controls
383 lines (352 loc) · 16.7 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
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
name: Build
on:
push:
branches:
- main
pull_request:
workflow_dispatch:
# Reusable entry point for the Release workflow for minor and majors: build a specific tagged
# commit and notarise/upload it. Triggers stay off tags, so a release build is
# never re-triggered by the tag the Release workflow pushes.
workflow_call:
inputs:
ref:
description: Git ref (tag or SHA) to build. Defaults to the triggering ref.
required: false
type: string
version:
description: Marketing version to stamp instead of the git-derived one (set by the release workflow so it can build before the tag exists).
required: false
type: string
default: ""
notarize:
description: Notarise, staple and upload artifacts (set by the release workflow).
required: false
type: boolean
default: false
secrets:
APP_APPLE_SIGNING_CERTIFICATE_BASE64:
required: true
APP_APPLE_SIGNING_CERTIFICATE_PASSWORD:
required: true
PKG_APPLE_SIGNING_CERTIFICATE_BASE64:
required: true
PKG_APPLE_SIGNING_CERTIFICATE_PASSWORD:
required: true
PKG_APPLE_DEVELOPER_TEAM_ID:
required: true
PKG_APPLE_ID_EMAIL:
required: true
PKG_APPLE_ID_APP_SPECIFIC_PASSWORD:
required: true
concurrency:
group: ${{ github.workflow }}-${{ inputs.ref || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
# Dependabot PRs don't get the Actions signing secrets (they only see the
# separate Dependabot secrets store), so a signed build can't run for them —
# skip the job entirely rather than fail on an empty certificate import.
if: >-
github.actor != 'dependabot[bot]' &&
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository)
runs-on: macos-26
env:
NOTARIZE: ${{ inputs.notarize == true || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref || github.ref }}
# Full history so the version/build number can be derived from git
# (nearest v* tag + distance, and the commit count); a shallow clone
# would collapse to the bootstrap fallback and ship wrong numbers.
fetch-depth: 0
# Nothing here pushes; don't leave the token in .git/config where it
# could leak into an uploaded artifact (zizmor: artipacked).
persist-credentials: false
# fetch-depth: 0 already retrieves tags, but re-fetch defensively so
# git describe in the derivation scripts always sees every v* tag.
- name: Fetch tags
run: git fetch --tags --force
# Creates a short-lived keychain isolated to this job. The 6-hour
# timeout (21600 s) exceeds any reasonable build duration.
- name: Create and unlock temporary macOS keychain
run: |
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
KEYCHAIN_PATH="$RUNNER_TEMP/signing.keychain-db"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
echo "KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
echo "KEYCHAIN_PASSWORD=$KEYCHAIN_PASSWORD" >> "$GITHUB_ENV"
- name: Import signing certificates
env:
APP_CERTIFICATE_BASE64: ${{ secrets.APP_APPLE_SIGNING_CERTIFICATE_BASE64 }}
APP_CERTIFICATE_PASSWORD: ${{ secrets.APP_APPLE_SIGNING_CERTIFICATE_PASSWORD }}
INSTALLER_CERTIFICATE_BASE64: ${{ secrets.PKG_APPLE_SIGNING_CERTIFICATE_BASE64 }}
INSTALLER_CERTIFICATE_PASSWORD: ${{ secrets.PKG_APPLE_SIGNING_CERTIFICATE_PASSWORD }}
run: |
APP_CERTIFICATE_PATH="$RUNNER_TEMP/app-certificate.p12"
INSTALLER_CERTIFICATE_PATH="$RUNNER_TEMP/installer-certificate.p12"
echo "$APP_CERTIFICATE_BASE64" | base64 --decode > "$APP_CERTIFICATE_PATH"
security import "$APP_CERTIFICATE_PATH" \
-k "$KEYCHAIN_PATH" \
-P "$APP_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/pkgbuild \
-T /usr/bin/productbuild
echo "$INSTALLER_CERTIFICATE_BASE64" | base64 --decode > "$INSTALLER_CERTIFICATE_PATH"
security import "$INSTALLER_CERTIFICATE_PATH" \
-k "$KEYCHAIN_PATH" \
-P "$INSTALLER_CERTIFICATE_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/pkgbuild \
-T /usr/bin/productbuild
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security list-keychains -d user -s "$KEYCHAIN_PATH" login.keychain
CODESIGN_IDENTITIES=$(security find-identity -v -p codesigning "$KEYCHAIN_PATH" || true)
BASIC_IDENTITIES=$(security find-identity -v -p basic "$KEYCHAIN_PATH" || true)
echo "$CODESIGN_IDENTITIES"
echo "$BASIC_IDENTITIES"
if ! echo "$CODESIGN_IDENTITIES" | grep -q "Developer ID Application"; then
echo "Missing Developer ID Application identity in imported keychain."
exit 1
fi
if ! echo "$BASIC_IDENTITIES" | grep -q "Developer ID Installer"; then
echo "Missing Developer ID Installer identity in imported keychain."
exit 1
fi
rm "$APP_CERTIFICATE_PATH" "$INSTALLER_CERTIFICATE_PATH"
- name: Preflight signing configuration
run: |
echo "Using app signing identity: Developer ID Application"
echo "Using installer signing identity: Developer ID Installer"
echo "Using team ID: ${{ secrets.PKG_APPLE_DEVELOPER_TEAM_ID }}"
- name: Resolve marketing version and build number
env:
VERSION_OVERRIDE: ${{ inputs.version }}
run: |
VERSION="${VERSION_OVERRIDE:-$(scripts/derive-version)}"
BUILD=$(scripts/derive-build-number)
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
echo "BUILD=$BUILD" >> "$GITHUB_ENV"
echo "Building version $VERSION (build $BUILD)"
- name: Build and archive
# The stamp build phase reads MARKETING_VERSION_OVERRIDE (a release build
# passes the version it is releasing; other builds leave it empty and
# derive from git).
env:
MARKETING_VERSION_OVERRIDE: ${{ inputs.version }}
run: |
# --timestamp requests a secure Apple timestamp (mandatory for
# Developer ID signing and notarisation); --options runtime enables
# the hardened runtime. Both are pinned here so codesign is
# deterministic on CI rather than relying on Xcode defaults. The
# version/build are injected by the stamp build phase, not here.
xcodebuild archive \
-project Homebrew.xcodeproj \
-scheme Brew \
-configuration Release \
-archivePath "$RUNNER_TEMP/Homebrew.xcarchive" \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Developer ID Application" \
DEVELOPMENT_TEAM="${{ secrets.PKG_APPLE_DEVELOPER_TEAM_ID }}" \
OTHER_CODE_SIGN_FLAGS="--keychain $KEYCHAIN_PATH --timestamp --options runtime"
# Fail fast with a clear message if the archived app did not get a secure
# timestamp, rather than surfacing it later at notarisation.
- name: Verify archived app signature
run: |
APP="$RUNNER_TEMP/Homebrew.xcarchive/Products/Applications/Homebrew.app"
codesign -dv --verbose=4 "$APP" 2>&1 | tee "$RUNNER_TEMP/archive-signature.txt"
if ! grep -q "^Timestamp=" "$RUNNER_TEMP/archive-signature.txt"; then
echo "Archived app is missing a secure timestamp."
exit 1
fi
- name: Export archive
run: |
cat > "$RUNNER_TEMP/ExportOptions.plist" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>method</key>
<string>developer-id</string>
<key>teamID</key>
<string>${{ secrets.PKG_APPLE_DEVELOPER_TEAM_ID }}</string>
<key>signingStyle</key>
<string>manual</string>
</dict>
</plist>
EOF
xcodebuild -exportArchive \
-archivePath "$RUNNER_TEMP/Homebrew.xcarchive" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" \
-exportPath "$RUNNER_TEMP/export"
- name: Verify app signature from export
run: |
APP_BUNDLE_PATH="$RUNNER_TEMP/export/Homebrew.app"
xcrun codesign -dv --verbose=4 "$APP_BUNDLE_PATH" 2>&1 | tee "$RUNNER_TEMP/app-signature.txt"
if ! grep -q "Authority=Developer ID Application" "$RUNNER_TEMP/app-signature.txt"; then
echo "Exported app is not signed with a Developer ID Application authority."
exit 1
fi
# Safety net: confirm the stamp build phase actually baked the derived
# version into the app. If it silently no-op'd (e.g. script sandboxing was
# re-enabled), the app would ship the xcconfig placeholder while the
# artifacts/release claim $VERSION — fail loudly instead.
- name: Verify app version matches derived version
run: |
APP_BUNDLE_PATH="$RUNNER_TEMP/export/Homebrew.app"
BUNDLE_VERSION=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' \
"$APP_BUNDLE_PATH/Contents/Info.plist")
if [ "$BUNDLE_VERSION" != "$VERSION" ]; then
echo "App version $BUNDLE_VERSION does not match derived version $VERSION."
exit 1
fi
echo "App version $BUNDLE_VERSION matches derived version $VERSION."
- name: Notarize and staple app
if: env.NOTARIZE == 'true'
env:
APPLE_ID: ${{ secrets.PKG_APPLE_ID_EMAIL }}
APPLE_ID_PASSWORD: ${{ secrets.PKG_APPLE_ID_APP_SPECIFIC_PASSWORD }}
run: |
APP="$RUNNER_TEMP/export/Homebrew.app"
# notarytool needs a container; ditto preserves the bundle's symlinks
# and extended attributes where a plain zip would not.
ditto -c -k --keepParent "$APP" "$RUNNER_TEMP/notarize-app.zip"
xcrun notarytool submit "$RUNNER_TEMP/notarize-app.zip" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "${{ secrets.PKG_APPLE_DEVELOPER_TEAM_ID }}" \
--wait
xcrun stapler staple "$APP"
# Homebrew.pkg is a *product archive* (a finished installer with
# its own setup scripts), so it cannot be nested as a productbuild
# sub-package. Instead we ship the file and invoke Apple's installer on
# it, which keeps Homebrew's own install logic and signature intact.
- name: Stage Homebrew installer and postinstall script
run: |
SCRIPTS_DIR="$RUNNER_TEMP/installer-scripts"
mkdir -p "$SCRIPTS_DIR"
curl -fsSL -o "$SCRIPTS_DIR/Homebrew.pkg" \
https://github.com/Homebrew/brew/releases/latest/download/Homebrew.pkg
# Verify the download before embedding it inside our own signed .pkg:
# pkgutil must report a valid signature AND the cert chain must carry
# Homebrew's Developer ID Installer team ID. This catches truncation,
# an HTML error page, and — because we pin the team ID — an asset swap
# to a different (even validly-signed) installer.
#
# MAINTENANCE: Homebrew signs with a *maintainer's* Developer ID, which
# has rotated before (Mike McQuaid 6248TWFRH6 -> Patrick Linnane
# 927JGANW46) and was announced only informally. If a legitimate
# Homebrew release starts failing here, confirm the new signer via
# https://github.com/orgs/Homebrew/discussions and update this ID.
EXPECTED_INSTALLER_TEAM_ID="927JGANW46" # Homebrew: Patrick Linnane
if ! sig="$(pkgutil --check-signature "$SCRIPTS_DIR/Homebrew.pkg" 2>&1)" \
|| ! grep -q "Developer ID Installer: .*(${EXPECTED_INSTALLER_TEAM_ID})" <<<"$sig"; then
echo "Homebrew.pkg is not validly signed by the expected team ID (${EXPECTED_INSTALLER_TEAM_ID}) — aborting." >&2
echo "$sig" >&2
exit 1
fi
cat > "$SCRIPTS_DIR/postinstall" <<'POSTINSTALL'
#!/bin/bash
# Installs the bundled Homebrew CLI when it is not already present, by
# running the official signed Homebrew.pkg staged next to this script.
set -euo pipefail
if [[ -x /opt/homebrew/bin/brew || -x /usr/local/bin/brew ]]; then
echo "postinstall: Homebrew already installed — skipping." >&2
exit 0
fi
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
echo "postinstall: installing bundled Homebrew…" >&2
installer -pkg "$SCRIPT_DIR/Homebrew.pkg" -target /
echo "postinstall: Homebrew installation complete." >&2
exit 0
POSTINSTALL
chmod +x "$SCRIPTS_DIR/postinstall"
- name: Build component package
run: |
pkgbuild \
--component "$RUNNER_TEMP/export/Homebrew.app" \
--identifier "sh.brew.app" \
--version "$VERSION" \
--install-location "/Applications" \
--scripts "$RUNNER_TEMP/installer-scripts" \
--sign "Developer ID Installer" \
--keychain "$KEYCHAIN_PATH" \
"$RUNNER_TEMP/Homebrew-component.pkg"
- name: Build distribution package
run: |
PKG_PATH="$RUNNER_TEMP/Homebrew-${VERSION}.pkg"
productbuild \
--package "$RUNNER_TEMP/Homebrew-component.pkg" \
--sign "Developer ID Installer" \
--keychain "$KEYCHAIN_PATH" \
"$PKG_PATH"
echo "PKG_PATH=$PKG_PATH" >> "$GITHUB_ENV"
- name: Verify package signature
run: |
pkgutil --check-signature "$PKG_PATH" | tee "$RUNNER_TEMP/pkg-signature.txt"
if ! grep -q "Developer ID Installer" "$RUNNER_TEMP/pkg-signature.txt"; then
echo "Package is not signed with a Developer ID Installer authority."
exit 1
fi
- name: Notarize package
if: env.NOTARIZE == 'true'
env:
APPLE_ID: ${{ secrets.PKG_APPLE_ID_EMAIL }}
APPLE_ID_PASSWORD: ${{ secrets.PKG_APPLE_ID_APP_SPECIFIC_PASSWORD }}
run: |
xcrun notarytool submit "$PKG_PATH" \
--apple-id "$APPLE_ID" \
--password "$APPLE_ID_PASSWORD" \
--team-id "${{ secrets.PKG_APPLE_DEVELOPER_TEAM_ID }}" \
--wait
- name: Staple notarization ticket to package
if: env.NOTARIZE == 'true'
run: xcrun stapler staple "$PKG_PATH"
- name: Create app zip
if: env.NOTARIZE == 'true'
run: |
APP_ZIP="$RUNNER_TEMP/Homebrew-${VERSION}.zip"
ditto -c -k --keepParent "$RUNNER_TEMP/export/Homebrew.app" "$APP_ZIP"
echo "APP_ZIP=$APP_ZIP" >> "$GITHUB_ENV"
- name: Zip dSYMs
if: env.NOTARIZE == 'true'
run: |
DSYM_ZIP="$RUNNER_TEMP/Homebrew-${VERSION}.dSYMs.zip"
DSYM_DIR="$RUNNER_TEMP/Homebrew.xcarchive/dSYMs"
if [ -z "$(ls -A "$DSYM_DIR" 2>/dev/null)" ]; then
echo "No dSYMs in archive — a release build must produce debug symbols."
exit 1
fi
ditto -c -k --keepParent "$DSYM_DIR" "$DSYM_ZIP"
echo "DSYM_ZIP=$DSYM_ZIP" >> "$GITHUB_ENV"
- name: Upload package artifact
if: env.NOTARIZE == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Homebrew-pkg
path: ${{ env.PKG_PATH }}
retention-days: 90
- name: Upload app artifact
if: env.NOTARIZE == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Homebrew-app
path: ${{ env.APP_ZIP }}
retention-days: 90
- name: Upload dSYMs artifact
if: env.NOTARIZE == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Homebrew-dsyms
path: ${{ env.DSYM_ZIP }}
retention-days: 90
- name: Delete temporary keychain
if: always()
run: security delete-keychain "$KEYCHAIN_PATH"