-
Notifications
You must be signed in to change notification settings - Fork 63
363 lines (324 loc) · 15 KB
/
Copy pathrelease.yml
File metadata and controls
363 lines (324 loc) · 15 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
name: Release
on:
push:
tags:
- 'release-v*'
permissions:
contents: write
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false
jobs:
build:
name: Build macOS App
runs-on: macos-latest
outputs:
version: ${{ steps.version.outputs.version }}
artifact_name: ${{ steps.package.outputs.artifact_name }}
artifact_sha256: ${{ steps.package.outputs.artifact_sha256 }}
dmg_name: ${{ steps.package.outputs.dmg_name }}
dmg_sha256: ${{ steps.package.outputs.dmg_sha256 }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Extract version from tag
id: version
run: |
TAG="${GITHUB_REF#refs/tags/}"
VERSION="${TAG#release-v}"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "Releasing version: $VERSION (tag: $TAG)"
- name: Select Xcode
run: sudo xcode-select -s /Applications/Xcode.app
- name: Show toolchain info
run: |
xcodebuild -version
swift --version
- name: Build (release, universal)
run: |
set -euo pipefail
# Build for both architectures so we can ship a universal binary.
swift build -c release --arch arm64 --arch x86_64
- name: Run tests
run: |
set -euo pipefail
if swift test --list-tests 2>/dev/null | grep -q .; then
swift test -c release
else
echo "No tests defined; skipping."
fi
continue-on-error: false
- name: Assemble .app bundle
id: package
env:
MACOS_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: |
set -euo pipefail
VERSION="${{ steps.version.outputs.version }}"
APP_NAME="capcap"
BUILD_DIR=".build/apple/Products/Release"
# Fallback if universal build path differs
if [ ! -f "$BUILD_DIR/$APP_NAME" ]; then
BUILD_DIR=".build/release"
fi
APP_DIR="build/${APP_NAME}.app"
CONTENTS="$APP_DIR/Contents"
MACOS="$CONTENTS/MacOS"
RESOURCES="$CONTENTS/Resources"
PLUGINS="$CONTENTS/PlugIns"
EXTENSION_PRODUCT_NAME="CapcapShareExtension"
EXTENSION_NAME="$EXTENSION_PRODUCT_NAME.appex"
EXTENSION_DIR="$PLUGINS/$EXTENSION_NAME"
EXTENSION_CONTENTS="$EXTENSION_DIR/Contents"
EXTENSION_MACOS="$EXTENSION_CONTENTS/MacOS"
EXTENSION_RESOURCES="$EXTENSION_CONTENTS/Resources"
rm -rf "$APP_DIR"
mkdir -p "$MACOS" "$RESOURCES" "$EXTENSION_MACOS" "$EXTENSION_RESOURCES"
cp "$BUILD_DIR/$APP_NAME" "$MACOS/$APP_NAME"
cp "capcap/App/Info.plist" "$CONTENTS/Info.plist"
if [ ! -f "$BUILD_DIR/$EXTENSION_PRODUCT_NAME" ]; then
echo "error: share extension binary missing at $BUILD_DIR/$EXTENSION_PRODUCT_NAME" >&2
exit 1
fi
cp "$BUILD_DIR/$EXTENSION_PRODUCT_NAME" "$EXTENSION_MACOS/$EXTENSION_PRODUCT_NAME"
cp "capcap-share-extension/Info.plist" "$EXTENSION_CONTENTS/Info.plist"
APP_SHORT_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$CONTENTS/Info.plist")"
APP_BUNDLE_VERSION="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$CONTENTS/Info.plist")"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $APP_SHORT_VERSION" "$EXTENSION_CONTENTS/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $APP_BUNDLE_VERSION" "$EXTENSION_CONTENTS/Info.plist"
# Copy app icon — Info.plist declares CFBundleIconFile=AppIcon, so the
# bundle needs Resources/AppIcon.icns or it ships with no icon.
if [ ! -f "Resources/AppIcon.icns" ]; then
echo "error: Resources/AppIcon.icns missing" >&2; exit 1
fi
cp "Resources/AppIcon.icns" "$RESOURCES/AppIcon.icns"
cp "Resources/AppIcon.icns" "$EXTENSION_RESOURCES/AppIcon.icns"
# Copy the status bar icon. StatusBarController loads this bundled
# SVG at runtime and falls back to the old SF Symbol when it is absent.
if [ ! -f "design/menuBarIcon.svg" ]; then
echo "error: design/menuBarIcon.svg missing" >&2; exit 1
fi
cp "design/menuBarIcon.svg" "$RESOURCES/MenuBarIcon.svg"
# Copy localization bundles (.lproj). The app loads these directly
# for its in-app language picker — see Localizer.swift. Without them
# every UI string falls back to its raw key.
shopt -s nullglob
LPROJ_COUNT=0
for lproj in Resources/*.lproj; do
[ -d "$lproj" ] || continue
cp -R "$lproj" "$RESOURCES/"
LPROJ_COUNT=$((LPROJ_COUNT + 1))
done
shopt -u nullglob
if [ "$LPROJ_COUNT" -eq 0 ]; then
echo "error: no Resources/*.lproj localization bundles found" >&2
exit 1
fi
echo "Copied $LPROJ_COUNT .lproj localization bundle(s)"
# Copy SwiftPM resource bundles. PermissionFlow uses Bundle.module
# when the authorization helper panel is rendered; shipping the
# binary without this bundle crashes at runtime on first access.
PERMISSION_FLOW_BUNDLE="$BUILD_DIR/${APP_NAME}_PermissionFlow.bundle"
if [ ! -d "$PERMISSION_FLOW_BUNDLE" ]; then
echo "error: missing SwiftPM resource bundle: $PERMISSION_FLOW_BUNDLE" >&2
exit 1
fi
cp -R "$PERMISSION_FLOW_BUNDLE" "$RESOURCES/"
# Fail fast if the shipped binary isn't a universal (arm64 + x86_64)
# slice — Intel Mac users can't run an arm64-only build.
ARCHS="$(lipo -archs "$MACOS/$APP_NAME")"
echo "Binary archs: $ARCHS"
case "$ARCHS" in
*arm64*x86_64*|*x86_64*arm64*) : ;;
*) echo "error: shipped binary is not universal (archs: $ARCHS)" >&2; exit 1 ;;
esac
EXTENSION_ARCHS="$(lipo -archs "$EXTENSION_MACOS/$EXTENSION_PRODUCT_NAME")"
echo "Share extension archs: $EXTENSION_ARCHS"
case "$EXTENSION_ARCHS" in
*arm64*x86_64*|*x86_64*arm64*) : ;;
*) echo "error: share extension binary is not universal (archs: $EXTENSION_ARCHS)" >&2; exit 1 ;;
esac
# Copy compiled asset catalog if present
if [ -d "$BUILD_DIR/${APP_NAME}_capcap.bundle" ]; then
cp -R "$BUILD_DIR/${APP_NAME}_capcap.bundle" "$RESOURCES/"
fi
# ---------------------------------------------------------------
# Code signing
# ---------------------------------------------------------------
# Sign every release with ONE reusable self-signed certificate so the
# app keeps a stable code-signing identity. macOS TCC keys Screen
# Recording / Accessibility grants to that identity, so users no
# longer have to re-authorize on every update.
#
# This is NOT a Developer ID cert — Gatekeeper still warns about an
# "unidentified developer" on first launch. To remove that too, get an
# Apple Developer account and switch to a notarized Developer ID cert.
#
# Generate the cert once with scripts/generate-signing-cert.sh, then
# set these GitHub repo secrets:
# MACOS_CERTIFICATE — base64 of capcap-signing.p12
# MACOS_CERTIFICATE_PWD — the .p12 export password
# MACOS_SIGNING_IDENTITY — cert common name, e.g. "capcap Self-Signed"
# KEYCHAIN_PASSWORD — any throwaway string
#
# If the secrets are absent the build falls back to ad-hoc signing
# (still launchable, but the re-authorize-every-update problem returns).
if [ -n "${MACOS_CERTIFICATE:-}" ]; then
echo "Signing with self-signed certificate: $MACOS_SIGNING_IDENTITY"
echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -lut 21600 build.keychain
security import certificate.p12 -k build.keychain \
-P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
codesign --force \
--entitlements scripts/capcap-share-extension.entitlements \
--sign "$MACOS_SIGNING_IDENTITY" \
"$EXTENSION_DIR"
codesign --force \
--entitlements scripts/capcap.entitlements \
--sign "$MACOS_SIGNING_IDENTITY" \
"$APP_DIR"
codesign --verify --strict --verbose=2 "$EXTENSION_DIR"
codesign --verify --strict --verbose=2 "$APP_DIR"
rm -f certificate.p12
else
echo "warning: MACOS_CERTIFICATE secret not set — falling back to ad-hoc signing." >&2
echo "warning: users will have to re-authorize permissions on every update." >&2
codesign --force --entitlements scripts/capcap-share-extension.entitlements --sign - "$EXTENSION_DIR" || true
codesign --force --entitlements scripts/capcap.entitlements --sign - "$APP_DIR" || true
fi
# Package as zip
ARTIFACT="capcap-${VERSION}-macos.zip"
ditto -c -k --sequesterRsrc --keepParent "$APP_DIR" "$ARTIFACT"
# Compute checksum for transparency
SHA256="$(shasum -a 256 "$ARTIFACT" | awk '{print $1}')"
printf '%s %s\n' "$SHA256" "$ARTIFACT" > "${ARTIFACT}.sha256"
# Package as a draggable DMG for users who prefer the native
# Applications-folder install flow.
DMG_ARTIFACT="capcap-${VERSION}-macos.dmg"
bash scripts/create-dmg.sh "$APP_DIR" "$DMG_ARTIFACT" "capcap"
DMG_SHA256="$(shasum -a 256 "$DMG_ARTIFACT" | awk '{print $1}')"
printf '%s %s\n' "$DMG_SHA256" "$DMG_ARTIFACT" > "${DMG_ARTIFACT}.sha256"
echo "artifact_name=$ARTIFACT" >> "$GITHUB_OUTPUT"
echo "artifact_sha256=$SHA256" >> "$GITHUB_OUTPUT"
echo "dmg_name=$DMG_ARTIFACT" >> "$GITHUB_OUTPUT"
echo "dmg_sha256=$DMG_SHA256" >> "$GITHUB_OUTPUT"
ls -lh "$ARTIFACT" "${ARTIFACT}.sha256" "$DMG_ARTIFACT" "${DMG_ARTIFACT}.sha256"
# ---------------------------------------------------------------
# Notarization (DISABLED — uncomment when signing is enabled)
# ---------------------------------------------------------------
# Required GitHub Secrets:
# AC_USERNAME — Apple ID email
# AC_PASSWORD — app-specific password
# AC_TEAM_ID — Apple Developer Team ID
#
# - name: Notarize
# env:
# AC_USERNAME: ${{ secrets.AC_USERNAME }}
# AC_PASSWORD: ${{ secrets.AC_PASSWORD }}
# AC_TEAM_ID: ${{ secrets.AC_TEAM_ID }}
# run: |
# ARTIFACT="${{ steps.package.outputs.artifact_name }}"
# xcrun notarytool submit "$ARTIFACT" \
# --apple-id "$AC_USERNAME" \
# --password "$AC_PASSWORD" \
# --team-id "$AC_TEAM_ID" \
# --wait
# # Staple the ticket so Gatekeeper accepts the app offline
# ditto -x -k "$ARTIFACT" notarized
# xcrun stapler staple "notarized/capcap.app"
# rm "$ARTIFACT"
# ditto -c -k --sequesterRsrc --keepParent "notarized/capcap.app" "$ARTIFACT"
- name: Upload artifact
uses: actions/upload-artifact@v7
with:
name: capcap-${{ steps.version.outputs.version }}
path: |
${{ steps.package.outputs.artifact_name }}
${{ steps.package.outputs.artifact_name }}.sha256
${{ steps.package.outputs.dmg_name }}
${{ steps.package.outputs.dmg_name }}.sha256
if-no-files-found: error
retention-days: 30
release:
name: Create GitHub Release
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout (for CHANGELOG)
uses: actions/checkout@v6
- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: capcap-${{ needs.build.outputs.version }}
path: dist
- name: Extract release notes from CHANGELOG
id: notes
run: |
set -euo pipefail
VERSION="${{ needs.build.outputs.version }}"
NOTES_FILE="release-notes.md"
if [ -f CHANGELOG.md ]; then
awk -v ver="$VERSION" '
BEGIN { found = 0 }
/^## \[/ {
if (found) { exit }
# Match e.g. ## [1.2.3] - 2026-04-09 or ## [1.2.3]
if ($0 ~ ("\\[" ver "\\]")) { found = 1; next }
}
found { print }
' CHANGELOG.md > "$NOTES_FILE"
fi
if [ ! -s "$NOTES_FILE" ]; then
{
echo "Release v${VERSION}"
echo
echo "_See commit history for details._"
} > "$NOTES_FILE"
fi
echo "---- release notes ----"
cat "$NOTES_FILE"
echo "-----------------------"
echo "notes_file=$NOTES_FILE" >> "$GITHUB_OUTPUT"
- name: Create GitHub Release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ github.ref_name }}
name: v${{ needs.build.outputs.version }}
body_path: ${{ steps.notes.outputs.notes_file }}
draft: false
prerelease: false
fail_on_unmatched_files: true
files: |
dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Dispatch Homebrew tap bump
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
VERSION: ${{ needs.build.outputs.version }}
SHA256: ${{ needs.build.outputs.artifact_sha256 }}
run: |
set -euo pipefail
if [ -z "${HOMEBREW_TAP_TOKEN:-}" ]; then
echo "HOMEBREW_TAP_TOKEN is not set; skipping Homebrew tap dispatch."
exit 0
fi
payload="{\"event_type\":\"capcap_release_published\",\"client_payload\":{\"version\":\"${VERSION}\",\"sha256\":\"${SHA256}\"}}"
curl -fsSL \
-X POST \
-H "Accept: application/vnd.github+json" \
-H "Authorization: Bearer ${HOMEBREW_TAP_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
https://api.github.com/repos/realskyrin/homebrew-tap/dispatches \
-d "${payload}"