1+ name : Release Build
2+
3+ on :
4+ push :
5+ branches : [ main ]
6+ workflow_dispatch :
7+ inputs :
8+ release_type :
9+ description : ' Release type (patch, minor, major)'
10+ required : false
11+ default : ' patch'
12+ type : choice
13+ options :
14+ - patch
15+ - minor
16+ - major
17+
18+ permissions :
19+ contents : write
20+
21+ concurrency :
22+ group : release-${{ github.ref }}
23+ cancel-in-progress : true
24+
25+ jobs :
26+ build-and-release :
27+ name : Build, Sign, Release
28+ runs-on : macos-15
29+
30+ steps :
31+ - name : Checkout code
32+ uses : actions/checkout@v4
33+ with :
34+ fetch-depth : 0 # Need full history for release notes
35+
36+ - name : Setup Xcode
37+ uses : maxim-lobanov/setup-xcode@v1
38+ with :
39+ xcode-version : latest-stable
40+
41+ - name : Get current version
42+ id : get_version
43+ run : |
44+ VERSION=$(xcodebuild -project Recap.xcodeproj -target Recap -showBuildSettings 2>/dev/null | grep "MARKETING_VERSION" | head -1 | sed 's/.*= //' | tr -d ' ')
45+ BUILD=$(xcodebuild -project Recap.xcodeproj -target Recap -showBuildSettings 2>/dev/null | grep "CURRENT_PROJECT_VERSION" | head -1 | sed 's/.*= //' | tr -d ' ')
46+
47+ VERSION=${VERSION:-"1.0"}
48+ BUILD=${BUILD:-"1"}
49+
50+ if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
51+ if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+$ ]]; then
52+ VERSION="${VERSION}.0"
53+ else
54+ VERSION="1.0.0"
55+ fi
56+ fi
57+
58+ echo "current_version=$VERSION" >> $GITHUB_OUTPUT
59+ echo "current_build=$BUILD" >> $GITHUB_OUTPUT
60+ echo "Current version: $VERSION ($BUILD)"
61+
62+ - name : Calculate next version
63+ id : next_version
64+ run : |
65+ CURRENT="${{ steps.get_version.outputs.current_version }}"
66+ TYPE="${{ github.event.inputs.release_type || 'patch' }}"
67+
68+ IFS='.' read -r MAJOR MINOR PATCH <<< "$CURRENT"
69+
70+ case $TYPE in
71+ major)
72+ MAJOR=$((MAJOR + 1))
73+ MINOR=0
74+ PATCH=0
75+ ;;
76+ minor)
77+ MINOR=$((MINOR + 1))
78+ PATCH=0
79+ ;;
80+ patch)
81+ PATCH=$((PATCH + 1))
82+ ;;
83+ esac
84+
85+ NEW_VERSION="$MAJOR.$MINOR.$PATCH"
86+ NEW_BUILD="${{ github.run_number }}"
87+
88+ echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
89+ echo "build=$NEW_BUILD" >> $GITHUB_OUTPUT
90+ echo "Next version: $NEW_VERSION ($NEW_BUILD)"
91+
92+ - name : Update version in project
93+ run : |
94+ sed -i '' "s/MARKETING_VERSION = [^;]*/MARKETING_VERSION = ${{ steps.next_version.outputs.version }}/g" Recap.xcodeproj/project.pbxproj
95+ sed -i '' "s/CURRENT_PROJECT_VERSION = [^;]*/CURRENT_PROJECT_VERSION = ${{ steps.next_version.outputs.build }}/g" Recap.xcodeproj/project.pbxproj
96+
97+ echo "Updated project version to ${{ steps.next_version.outputs.version }} (${{ steps.next_version.outputs.build }})"
98+
99+ - name : Install Apple certificates
100+ env :
101+ BUILD_CERTIFICATE_BASE64 : ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
102+ KEYCHAIN_PASSWORD : ${{ secrets.KEYCHAIN_PASSWORD }}
103+ P12_PASSWORD : ${{ secrets.P12_PASSWORD }}
104+ run : |
105+ CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
106+ KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
107+
108+ echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
109+
110+ security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
111+ security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
112+ security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
113+
114+ security import $CERTIFICATE_PATH -k $KEYCHAIN_PATH -P "$P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
115+
116+ security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
117+
118+ security list-keychain -d user -s $KEYCHAIN_PATH login.keychain
119+
120+ echo "Certificates in new keychain:"
121+ security find-identity -v -p codesigning $KEYCHAIN_PATH
122+
123+ echo "✓ Certificates installed"
124+
125+ - name : Cache Swift Package Manager
126+ uses : actions/cache@v4
127+ with :
128+ path : |
129+ ~/Library/Developer/Xcode/DerivedData/*/SourcePackages
130+ ~/Library/Caches/org.swift.swiftpm
131+ key : ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
132+ restore-keys : |
133+ ${{ runner.os }}-spm-
134+
135+ - name : Resolve Dependencies
136+ run : |
137+ xcodebuild -resolvePackageDependencies \
138+ -project Recap.xcodeproj \
139+ -scheme Recap \
140+ -configuration Release
141+
142+ - name : Build and Archive
143+ env :
144+ DEVELOPMENT_TEAM : ${{ secrets.DEVELOPMENT_TEAM }}
145+ CODE_SIGN_IDENTITY : ${{ secrets.CODE_SIGN_IDENTITY }}
146+ run : |
147+ xcodebuild archive \
148+ -project Recap.xcodeproj \
149+ -scheme Recap \
150+ -configuration Release \
151+ -archivePath $RUNNER_TEMP/Recap.xcarchive \
152+ -destination 'generic/platform=macOS' \
153+ -derivedDataPath $RUNNER_TEMP/DerivedData \
154+ -skipMacroValidation \
155+ -allowProvisioningUpdates \
156+ ONLY_ACTIVE_ARCH=YES \
157+ ARCHS=arm64 \
158+ VALID_ARCHS=arm64 \
159+ EXCLUDED_ARCHS=x86_64 \
160+ DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \
161+ CODE_SIGN_STYLE="Manual" \
162+ CODE_SIGN_IDENTITY="$CODE_SIGN_IDENTITY" \
163+ PROVISIONING_PROFILE_SPECIFIER="" \
164+ CODE_SIGNING_REQUIRED=YES \
165+ CODE_SIGNING_ALLOWED=YES \
166+ SWIFT_VERSION=5.0 \
167+ SWIFT_TREAT_WARNINGS_AS_ERRORS=NO \
168+ GCC_TREAT_WARNINGS_AS_ERRORS=NO
169+
170+ - name : Export Archive
171+ env :
172+ DEVELOPMENT_TEAM : ${{ secrets.DEVELOPMENT_TEAM }}
173+ run : |
174+ if [ ! -d "$RUNNER_TEMP/Recap.xcarchive" ]; then
175+ echo "Archive not found at $RUNNER_TEMP/Recap.xcarchive"
176+ exit 1
177+ fi
178+
179+ echo "Available signing certificates:"
180+ security find-identity -v -p codesigning
181+
182+ cat > $RUNNER_TEMP/ExportOptions.plist <<EOF
183+ <?xml version="1.0" encoding="UTF-8"?>
184+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
185+ <plist version="1.0">
186+ <dict>
187+ <key>method</key>
188+ <string>developer-id</string>
189+ <key>teamID</key>
190+ <string>$DEVELOPMENT_TEAM</string>
191+ <key>signingStyle</key>
192+ <string>automatic</string>
193+ <key>uploadBitcode</key>
194+ <false/>
195+ <key>uploadSymbols</key>
196+ <false/>
197+ </dict>
198+ </plist>
199+ EOF
200+
201+ xcodebuild -exportArchive \
202+ -archivePath $RUNNER_TEMP/Recap.xcarchive \
203+ -exportPath $RUNNER_TEMP/export \
204+ -exportOptionsPlist $RUNNER_TEMP/ExportOptions.plist \
205+ -allowProvisioningUpdates
206+
207+ - name : Notarize app
208+ env :
209+ APPLE_ID : ${{ secrets.APPLE_ID }}
210+ APPLE_PASSWORD : ${{ secrets.APPLE_PASSWORD }}
211+ TEAM_ID : ${{ secrets.DEVELOPMENT_TEAM }}
212+ run : |
213+ if [ ! -d "$RUNNER_TEMP/export" ]; then
214+ echo "Export directory not found at $RUNNER_TEMP/export"
215+ echo "Contents of RUNNER_TEMP:"
216+ ls -la $RUNNER_TEMP
217+ exit 1
218+ fi
219+
220+ xcrun notarytool store-credentials "notarytool-profile" \
221+ --apple-id "$APPLE_ID" \
222+ --password "$APPLE_PASSWORD" \
223+ --team-id "$TEAM_ID"
224+
225+ cd $RUNNER_TEMP/export
226+
227+ if [ ! -d "Recap.app" ]; then
228+ echo "Recap.app not found in export directory"
229+ echo "Contents of export directory:"
230+ ls -la
231+ exit 1
232+ fi
233+
234+ ditto -c -k --keepParent "Recap.app" "Recap.zip"
235+
236+ xcrun notarytool submit "Recap.zip" \
237+ --keychain-profile "notarytool-profile" \
238+ --wait \
239+ --timeout 30m
240+
241+ xcrun stapler staple "Recap.app"
242+
243+ - name : Create DMG
244+ run : |
245+ brew install create-dmg
246+
247+ cd $RUNNER_TEMP
248+
249+ ICON_PATH=""
250+ if [ -f "$GITHUB_WORKSPACE/Recap/Assets.xcassets/AppIcon.appiconset/mac512.png" ]; then
251+ ICON_PATH="$GITHUB_WORKSPACE/Recap/Assets.xcassets/AppIcon.appiconset/mac512.png"
252+ elif [ -f "$GITHUB_WORKSPACE/Recap/Recap/Assets.xcassets/AppIcon.appiconset/mac512.png" ]; then
253+ ICON_PATH="$GITHUB_WORKSPACE/Recap/Recap/Assets.xcassets/AppIcon.appiconset/mac512.png"
254+ else
255+ echo "Warning: mac512.png not found, using app's icon"
256+ ICON_PATH=""
257+ fi
258+
259+ mkdir dmg_source
260+ cp -R export/Recap.app dmg_source/
261+
262+ if [ -n "$ICON_PATH" ]; then
263+ create-dmg \
264+ --volname "Recap" \
265+ --volicon "$ICON_PATH" \
266+ --window-pos 200 120 \
267+ --window-size 600 400 \
268+ --icon-size 100 \
269+ --icon "Recap.app" 150 180 \
270+ --hide-extension "Recap.app" \
271+ --app-drop-link 450 180 \
272+ --no-internet-enable \
273+ "Recap-${{ steps.next_version.outputs.version }}.dmg" \
274+ "dmg_source/"
275+ else
276+ create-dmg \
277+ --volname "Recap" \
278+ --window-pos 200 120 \
279+ --window-size 600 400 \
280+ --icon-size 100 \
281+ --icon "Recap.app" 150 180 \
282+ --hide-extension "Recap.app" \
283+ --app-drop-link 450 180 \
284+ --no-internet-enable \
285+ "Recap-${{ steps.next_version.outputs.version }}.dmg" \
286+ "dmg_source/"
287+ fi
288+
289+ xcrun notarytool submit "Recap-${{ steps.next_version.outputs.version }}.dmg" \
290+ --keychain-profile "notarytool-profile" \
291+ --wait \
292+ --timeout 30m
293+
294+ xcrun stapler staple "Recap-${{ steps.next_version.outputs.version }}.dmg"
295+
296+ - name : Generate Release Notes
297+ id : release_notes
298+ run : |
299+ LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || git rev-list --max-parents=0 HEAD)
300+
301+ cat > $RUNNER_TEMP/release_notes.md <<EOF
302+ # Recap v${{ steps.next_version.outputs.version }}
303+
304+ ## What's Changed
305+
306+ EOF
307+
308+ git log --pretty=format:"- %s (%h)" $LAST_TAG..HEAD >> $RUNNER_TEMP/release_notes.md
309+
310+ cat >> $RUNNER_TEMP/release_notes.md <<EOF
311+
312+
313+ ## Installation
314+
315+ 1. Download the DMG file below
316+ 2. Open the DMG and drag Recap to your Applications folder
317+ 3. On first launch, right-click and select "Open"
318+
319+ ## Checksums
320+
321+ EOF
322+
323+ cd $RUNNER_TEMP
324+ echo '```' >> release_notes.md
325+ echo "SHA256 (DMG): $(shasum -a 256 Recap-${{ steps.next_version.outputs.version }}.dmg | cut -d' ' -f1)" >> release_notes.md
326+ echo '```' >> release_notes.md
327+
328+ echo "RELEASE_NOTES<<EOF" >> $GITHUB_OUTPUT
329+ cat release_notes.md >> $GITHUB_OUTPUT
330+ echo "EOF" >> $GITHUB_OUTPUT
331+
332+ - name : Create GitHub Release
333+ uses : softprops/action-gh-release@v1
334+ with :
335+ tag_name : v${{ steps.next_version.outputs.version }}
336+ name : Recap v${{ steps.next_version.outputs.version }}
337+ body : ${{ steps.release_notes.outputs.RELEASE_NOTES }}
338+ draft : false
339+ prerelease : false
340+ files : |
341+ ${{ runner.temp }}/Recap-${{ steps.next_version.outputs.version }}.dmg
342+
343+ - name : Upload Build Artifacts
344+ if : always()
345+ uses : actions/upload-artifact@v4
346+ with :
347+ name : build-artifacts
348+ path : |
349+ ${{ runner.temp }}/Recap.xcarchive
350+ ${{ runner.temp }}/export/
351+ ${{ runner.temp }}/*.dmg
352+
353+ - name : Clean up keychain
354+ if : always()
355+ run : |
356+ security delete-keychain $RUNNER_TEMP/app-signing.keychain-db
0 commit comments