-
-
Notifications
You must be signed in to change notification settings - Fork 2
374 lines (340 loc) · 14.6 KB
/
Copy pathrelease.yml
File metadata and controls
374 lines (340 loc) · 14.6 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
name: Release
on:
push:
tags:
- "v*.*.*"
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Tag to release (e.g. v0.1.0)"
required: true
permissions:
contents: write
jobs:
check-signing:
name: Check signing secrets
runs-on: ubuntu-latest
outputs:
enabled: ${{ steps.check.outputs.enabled }}
steps:
- name: Verify DEVELOPER_ID_CERT_P12 is configured
id: check
env:
DEVELOPER_ID_CERT_P12: ${{ secrets.DEVELOPER_ID_CERT_P12 }}
run: |
if [[ -z "${DEVELOPER_ID_CERT_P12}" ]]; then
echo "enabled=false" >> "$GITHUB_OUTPUT"
echo "::notice::Signing secrets not yet configured — skipping binary release build. Configure them in repo Settings → Secrets and variables → Actions when you have a Developer ID certificate."
else
echo "enabled=true" >> "$GITHUB_OUTPUT"
fi
release:
name: Build, Sign & Release (arm64)
needs: check-signing
if: needs.check-signing.outputs.enabled == 'true'
runs-on: macos-26
# arm64-only: Bòcan targets macOS 26+, which runs exclusively on Apple Silicon.
# Intel Macs cannot run macOS 26, so there is no x86_64 user base to support.
# A universal binary would double CI build time with no benefit. FFmpeg dylibs
# and fpcalc in Resources/ are arm64-only as well. Revisit if the deployment
# target is ever lowered to macOS 14 or 15.
env:
ARCHS: arm64
ONLY_ACTIVE_ARCH: "NO"
MARKETING_VERSION: ${{ github.event.inputs.tag || github.ref_name }}
HAS_SPARKLE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY != '' && 'true' || 'false' }}
steps:
- name: Checkout (full history for changelog)
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Select Xcode 26
run: sudo xcode-select -s /Applications/Xcode_26.4.app
- name: Cache Homebrew packages
uses: actions/cache@v5
with:
path: ~/Library/Caches/Homebrew
key: ${{ runner.os }}-brew-${{ hashFiles('Brewfile.lock.json') }}
restore-keys: |
${{ runner.os }}-brew-
- name: Cache SPM packages
uses: actions/cache@v5
with:
path: |
~/Library/Developer/Xcode/DerivedData/**/SourcePackages
~/.swiftpm
key: ${{ runner.os }}-spm-release-${{ hashFiles('**/Package.resolved') }}
restore-keys: |
${{ runner.os }}-spm-release-
${{ runner.os }}-spm-
- name: Install Brewfile dependencies
run: brew bundle install --no-upgrade
- name: Set pkg-config path for FFmpeg and TagLib
run: |
echo "PKG_CONFIG_PATH=/opt/homebrew/opt/ffmpeg/lib/pkgconfig:/opt/homebrew/opt/taglib/lib/pkgconfig" >> $GITHUB_ENV
- name: Stub Secrets.xcconfig (xcodegen requires it)
env:
ACOUSTID_API_KEY: ${{ secrets.ACOUSTID_API_KEY }}
BOCAN_LASTFM_API_KEY: ${{ secrets.BOCAN_LASTFM_API_KEY }}
BOCAN_LASTFM_SHARED_SECRET: ${{ secrets.BOCAN_LASTFM_SHARED_SECRET }}
run: |
if [[ ! -f Secrets.xcconfig ]]; then
cp Secrets.xcconfig.template Secrets.xcconfig
fi
if [[ -n "${ACOUSTID_API_KEY:-}" ]]; then
/usr/bin/sed -i '' "s|^ACOUSTID_API_KEY.*|ACOUSTID_API_KEY = ${ACOUSTID_API_KEY}|" Secrets.xcconfig
fi
if [[ -n "${BOCAN_LASTFM_API_KEY:-}" ]]; then
/usr/bin/sed -i '' "s|^BOCAN_LASTFM_API_KEY.*|BOCAN_LASTFM_API_KEY = ${BOCAN_LASTFM_API_KEY}|" Secrets.xcconfig
fi
if [[ -n "${BOCAN_LASTFM_SHARED_SECRET:-}" ]]; then
/usr/bin/sed -i '' "s|^BOCAN_LASTFM_SHARED_SECRET.*|BOCAN_LASTFM_SHARED_SECRET = ${BOCAN_LASTFM_SHARED_SECRET}|" Secrets.xcconfig
fi
- name: Generate Xcode project
run: make generate
- name: Run tests before release (fail fast)
run: make test-coverage 2>&1 | xcbeautify
- name: Import Developer ID certificate
env:
DEVELOPER_ID_CERT_P12: ${{ secrets.DEVELOPER_ID_CERT_P12 }}
DEVELOPER_ID_CERT_PASSWORD: ${{ secrets.DEVELOPER_ID_CERT_PASSWORD }}
run: |
KEYCHAIN_PATH="$RUNNER_TEMP/bocan.keychain-db"
KEYCHAIN_PASSWORD=$(openssl rand -base64 32)
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 "$DEVELOPER_ID_CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" \
-k "$KEYCHAIN_PATH" \
-P "$DEVELOPER_ID_CERT_PASSWORD" \
-T /usr/bin/codesign
security list-keychain -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | xargs)
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
- name: Bundle fpcalc + FFmpeg dylibs into Resources/
env:
SIGNING_IDENTITY: ${{ secrets.DEVELOPER_ID_IDENTITY }}
run: make bundle-fpcalc
- name: Stamp version into Info.plist
run: |
VERSION="${MARKETING_VERSION#v}"
BUILD_NUMBER=$(date +%s)
# CFBundleShortVersionString: stamp the marketing version string directly.
# release-please also updates this, but re-stamping here is safe and ensures
# the archive always matches the tag even on workflow_dispatch runs.
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" Resources/Info.plist
# CFBundleVersion is NOT stamped here — Info.plist uses $(CURRENT_PROJECT_VERSION)
# which xcodebuild resolves from the build setting we pass on the archive command line.
echo "RELEASE_VERSION=$VERSION" >> "$GITHUB_ENV"
echo "BUILD_NUMBER=$BUILD_NUMBER" >> "$GITHUB_ENV"
- name: Archive (arm64 only)
run: |
set -o pipefail
xcodebuild archive \
-project Bocan.xcodeproj \
-scheme Bocan \
-configuration Release \
-destination 'generic/platform=macOS' \
-archivePath build/Bocan.xcarchive \
ARCHS=arm64 \
ONLY_ACTIVE_ARCH=NO \
CURRENT_PROJECT_VERSION="$BUILD_NUMBER" \
MARKETING_VERSION="$RELEASE_VERSION" \
CODE_SIGN_STYLE=Manual \
DEVELOPMENT_TEAM="${{ secrets.APPLE_TEAM_ID }}" \
CODE_SIGN_IDENTITY="${{ secrets.DEVELOPER_ID_IDENTITY }}" \
OTHER_CODE_SIGN_FLAGS="--timestamp --options=runtime" \
ENABLE_HARDENED_RUNTIME=YES \
| xcbeautify
- name: Export (Developer ID)
env:
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
# Generate ExportOptions with the real team ID ($(VAR) syntax is not
# expanded by xcodebuild -exportArchive, so we substitute it here).
sed "s/\$(APPLE_TEAM_ID)/${APPLE_TEAM_ID}/g" Scripts/ExportOptions.plist \
> /tmp/ExportOptions.plist
set -o pipefail
xcodebuild -exportArchive \
-archivePath build/Bocan.xcarchive \
-exportOptionsPlist /tmp/ExportOptions.plist \
-exportPath build/export \
| xcbeautify
echo "=== Export contents ==="
ls -la build/export/
- name: Bundle Homebrew dylibs into app (TagLib, FFmpeg, etc.)
env:
SIGNING_IDENTITY: ${{ secrets.DEVELOPER_ID_IDENTITY }}
run: bash Scripts/embed-deps.sh build/export/Bocan.app
- name: Verify code signature & hardened runtime
run: |
APP="build/export/Bocan.app"
codesign --verify --deep --strict --verbose=2 "$APP"
codesign --display --verbose=4 "$APP" 2>&1 | tee /tmp/codesign.txt
if ! grep -q "flags=.*runtime" /tmp/codesign.txt; then
echo "::error::Hardened runtime flag missing from signature."
exit 1
fi
- name: Notarize app
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
run: |
# notarytool requires a zip for .app submission.
ditto -c -k --keepParent "build/export/Bocan.app" "build/Bocan-notarize.zip"
RESULT=$(xcrun notarytool submit "build/Bocan-notarize.zip" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--wait --output-format plist)
echo "$RESULT"
SUBMISSION_ID=$(/usr/libexec/PlistBuddy -c "Print :id" /dev/stdin <<< "$RESULT")
STATUS=$(/usr/libexec/PlistBuddy -c "Print :status" /dev/stdin <<< "$RESULT")
if [[ "$STATUS" != "Accepted" ]]; then
echo "::error::Notarization was not accepted (status: $STATUS). Fetching rejection log..."
xcrun notarytool log "$SUBMISSION_ID" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APP_SPECIFIC_PASSWORD" || true
exit 1
fi
- name: Staple app
# Apple's CloudKit CDN can take a moment to propagate the ticket
# even after notarytool --wait returns. Retry up to 5 times.
run: |
for attempt in $(seq 1 5); do
xcrun stapler staple build/export/Bocan.app && break
if [[ $attempt -lt 5 ]]; then
echo "Staple attempt $attempt failed; waiting 30 s before retry..."
sleep 30
else
echo "::error::xcrun stapler failed after $attempt attempts."
exit 1
fi
done
- name: Create DMG
run: |
create-dmg \
--volname "Bòcan ${RELEASE_VERSION}" \
--background "Resources/Distribution/dmg-background.png" \
--window-pos 200 120 \
--window-size 540 380 \
--icon-size 128 \
--icon "Bocan.app" 140 195 \
--hide-extension "Bocan.app" \
--app-drop-link 400 195 \
--volicon "Resources/Distribution/VolumeIcon.icns" \
"build/Bocan.dmg" \
"build/export/" \
|| (sleep 5 && create-dmg \
--volname "Bòcan ${RELEASE_VERSION}" \
--background "Resources/Distribution/dmg-background.png" \
--window-pos 200 120 \
--window-size 540 380 \
--icon-size 128 \
--icon "Bocan.app" 140 195 \
--hide-extension "Bocan.app" \
--app-drop-link 400 195 \
--volicon "Resources/Distribution/VolumeIcon.icns" \
"build/Bocan.dmg" \
"build/export/")
- name: Sign DMG
run: |
codesign --force --sign "${{ secrets.DEVELOPER_ID_IDENTITY }}" \
--timestamp build/Bocan.dmg
- name: Notarize DMG
env:
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
APP_SPECIFIC_PASSWORD: ${{ secrets.APP_SPECIFIC_PASSWORD }}
run: |
xcrun notarytool submit "build/Bocan.dmg" \
--apple-id "$APPLE_ID" \
--team-id "$APPLE_TEAM_ID" \
--password "$APP_SPECIFIC_PASSWORD" \
--wait
xcrun stapler staple build/Bocan.dmg
- name: Gatekeeper assessment
run: spctl --assess --type open --context context:primary-signature --verbose=4 build/Bocan.dmg
- name: Compute checksums
id: checksums
run: |
cd build
shasum -a 256 Bocan.dmg | tee Bocan.dmg.sha256
SHA256=$(awk '{print $1}' Bocan.dmg.sha256)
echo "sha256=$SHA256" >> "$GITHUB_OUTPUT"
- name: Generate release notes from CHANGELOG
id: notes
run: |
chmod +x Scripts/release-notes.sh
NOTES_FILE=$(mktemp)
Scripts/release-notes.sh "${MARKETING_VERSION#v}" > "$NOTES_FILE" || \
echo "_See commit history for changes._" > "$NOTES_FILE"
{
echo "notes_path=$NOTES_FILE"
} >> "$GITHUB_OUTPUT"
- name: Sign appcast entry (Sparkle EdDSA)
if: env.HAS_SPARKLE_KEY == 'true'
env:
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
run: |
chmod +x Scripts/sparkle-update.sh
Scripts/sparkle-update.sh \
--dmg build/Bocan.dmg \
--version "${RELEASE_VERSION}" \
--build "${BUILD_NUMBER}" \
--output build/appcast-entry.xml
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
name: ${{ env.RELEASE_VERSION }}
tag_name: ${{ github.event.inputs.tag || github.ref_name }}
body_path: ${{ steps.notes.outputs.notes_path }}
files: |
build/Bocan.dmg
build/Bocan.dmg.sha256
build/appcast-entry.xml
fail_on_unmatched_files: false
draft: false
prerelease: ${{ contains(github.ref_name, '-beta') || contains(github.ref_name, '-rc') }}
- name: Update appcast.xml
if: env.HAS_SPARKLE_KEY == 'true'
env:
SPARKLE_ED_PRIVATE_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
run: |
FEED="website/static/appcast.xml"
if [[ "$GITHUB_REF_NAME" == *"-beta"* || "$GITHUB_REF_NAME" == *"-rc"* ]]; then
FEED="website/static/appcast-beta.xml"
fi
chmod +x Scripts/sparkle-update.sh
Scripts/sparkle-update.sh \
--dmg build/Bocan.dmg \
--version "${RELEASE_VERSION}" \
--build "${BUILD_NUMBER}" \
--prepend-to "$FEED"
echo "APPCAST_FEED=$FEED" >> "$GITHUB_ENV"
- name: Commit and push updated appcast
if: env.HAS_SPARKLE_KEY == 'true'
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add website/static/appcast.xml website/static/appcast-beta.xml
git diff --cached --quiet && echo "No appcast changes to commit" && exit 0
git commit -m "chore(release): update appcast for v${RELEASE_VERSION}"
git push origin HEAD:main
- name: Trigger cask update
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
repository: bocan/homebrew-bocan
event-type: new-release
client-payload: |
{
"version": "${{ env.RELEASE_VERSION }}",
"sha256": "${{ steps.checksums.outputs.sha256 }}"
}