Skip to content

Commit de5867c

Browse files
committed
Add release workflow and update infrastructure
1 parent 5001f4e commit de5867c

9 files changed

Lines changed: 403 additions & 1 deletion

File tree

.github/workflows/release.yml

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
name: Release macOS App
2+
3+
on:
4+
push:
5+
tags:
6+
- "v*"
7+
8+
workflow_dispatch:
9+
inputs:
10+
version:
11+
description: "Version number (e.g., 1.0.0)"
12+
required: false
13+
type: string
14+
15+
env:
16+
SCHEME: "Archives"
17+
PROJECT: "Archives.xcodeproj"
18+
PRODUCT_NAME: "Archives"
19+
DMG_BACKGROUND_FILE_NAME: "Assets/background@2x.png"
20+
21+
jobs:
22+
build:
23+
runs-on: macos-26
24+
25+
steps:
26+
- name: Checkout repository
27+
uses: actions/checkout@v5
28+
29+
- name: Get version
30+
id: version
31+
run: |
32+
if [ -n "${{ inputs.version }}" ]; then
33+
echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
34+
elif [[ "$GITHUB_REF" == refs/tags/v* ]]; then
35+
echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
36+
else
37+
echo "version=$(date +%Y%m%d%H%M%S)" >> $GITHUB_OUTPUT
38+
fi
39+
40+
- name: Select Xcode
41+
run: |
42+
sudo xcode-select -s /Applications/Xcode_26.1.app/Contents/Developer
43+
xcodebuild -version
44+
45+
- name: Set version in Xcode project
46+
run: |
47+
sed -i '' 's/MARKETING_VERSION = [^;]*;/MARKETING_VERSION = ${{ steps.version.outputs.version }};/g' "$PROJECT/project.pbxproj"
48+
sed -i '' 's/CURRENT_PROJECT_VERSION = [^;]*;/CURRENT_PROJECT_VERSION = ${{ steps.version.outputs.version }};/g' "$PROJECT/project.pbxproj"
49+
50+
- name: Import Code Signing Certificate
51+
env:
52+
CERTIFICATE_BASE64: ${{ secrets.CERTIFICATE_BASE64 }}
53+
CERTIFICATE_PASSWORD: ${{ secrets.CERTIFICATE_PASSWORD }}
54+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
55+
run: |
56+
# Create variables
57+
CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12
58+
KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db
59+
60+
# Decode certificate from base64
61+
echo -n "$CERTIFICATE_BASE64" | base64 --decode -o $CERTIFICATE_PATH
62+
63+
# Create temporary keychain
64+
security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
65+
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
66+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
67+
68+
# Import certificate to keychain
69+
security import $CERTIFICATE_PATH -P "$CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
70+
security set-key-partition-list -S apple-tool:,apple: -k "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
71+
security list-keychain -d user -s $KEYCHAIN_PATH
72+
73+
- name: Build and Archive
74+
env:
75+
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
76+
run: |
77+
xcodebuild archive \
78+
-project "$PROJECT" \
79+
-scheme "$SCHEME" \
80+
-configuration Release \
81+
-destination "generic/platform=macOS" \
82+
-archivePath $RUNNER_TEMP/archive.xcarchive \
83+
DEVELOPMENT_TEAM="$DEVELOPMENT_TEAM" \
84+
CODE_SIGN_STYLE=Manual \
85+
CODE_SIGN_IDENTITY="Developer ID Application" \
86+
PROVISIONING_PROFILE_SPECIFIER="" \
87+
CODE_SIGN_INJECT_BASE_ENTITLEMENTS=NO \
88+
OTHER_CODE_SIGN_FLAGS="--timestamp"
89+
90+
- name: Create ExportOptions.plist
91+
env:
92+
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
93+
run: |
94+
cat > $RUNNER_TEMP/ExportOptions.plist << EOF
95+
<?xml version="1.0" encoding="UTF-8"?>
96+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
97+
<plist version="1.0">
98+
<dict>
99+
<key>method</key>
100+
<string>developer-id</string>
101+
<key>teamID</key>
102+
<string>${DEVELOPMENT_TEAM}</string>
103+
<key>signingStyle</key>
104+
<string>manual</string>
105+
<key>signingCertificate</key>
106+
<string>Developer ID Application</string>
107+
</dict>
108+
</plist>
109+
EOF
110+
111+
- name: Export App
112+
run: |
113+
xcodebuild -exportArchive \
114+
-archivePath $RUNNER_TEMP/archive.xcarchive \
115+
-exportPath $RUNNER_TEMP/export \
116+
-exportOptionsPlist $RUNNER_TEMP/ExportOptions.plist
117+
118+
- name: Notarize App
119+
env:
120+
APPLE_ID: ${{ secrets.APPLE_ID }}
121+
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
122+
APPLE_TEAM_ID: ${{ secrets.DEVELOPMENT_TEAM }}
123+
run: |
124+
# Create ZIP for notarization
125+
cd $RUNNER_TEMP/export
126+
ditto -c -k --keepParent "$PRODUCT_NAME.app" "$PRODUCT_NAME-notorized.zip"
127+
128+
# Submit for notarization
129+
xcrun notarytool submit "$PRODUCT_NAME-notorized.zip" \
130+
--apple-id "$APPLE_ID" \
131+
--password "$APPLE_ID_PASSWORD" \
132+
--team-id "$APPLE_TEAM_ID" \
133+
--wait
134+
135+
# Staple the notarization ticket
136+
xcrun stapler staple "$PRODUCT_NAME.app"
137+
138+
- name: "DMG: Install tool"
139+
run: |
140+
curl -L -o dmgs.tar.gz https://github.com/velocityzen/dmgs/releases/latest/download/dmgs.tar.gz
141+
tar -xzf dmgs.tar.gz
142+
chmod +x dmgs
143+
sudo mv dmgs /usr/local/bin/
144+
145+
- name: "DMG: create .dmg"
146+
env:
147+
DEVELOPMENT_TEAM: ${{ secrets.DEVELOPMENT_TEAM }}
148+
run: dmgs create "$RUNNER_TEMP/export/$PRODUCT_NAME.app" "$DMG_BACKGROUND_FILE_NAME" --sign "$DEVELOPMENT_TEAM"
149+
150+
- name: "DMG: Upload DMG Artifact"
151+
uses: actions/upload-artifact@v4
152+
with:
153+
name: ${{ env.PRODUCT_NAME }}-${{ steps.version.outputs.version }}
154+
path: ${{ env.PRODUCT_NAME }}.dmg
155+
156+
- name: "Sparkle: Install CLI tools"
157+
run: |
158+
# Download Sparkle release with CLI tools
159+
mkdir sparkle
160+
cd sparkle
161+
curl -L -o sparkle.tar.xz https://github.com/sparkle-project/Sparkle/releases/download/2.8.1/Sparkle-2.8.1.tar.xz
162+
tar -xf sparkle.tar.xz
163+
# Make tools accessible
164+
chmod +x bin/generate_appcast
165+
sudo mv bin/generate_appcast /usr/local/bin/
166+
chmod +x bin/sign_update
167+
sudo mv bin/sign_update /usr/local/bin/
168+
cd ..
169+
rm -rf sparkle
170+
171+
- name: "Sparkle: Sign update package"
172+
env:
173+
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
174+
run: |
175+
# Sign the update with Sparkle's private key
176+
SIGNATURE=$(echo "$SPARKLE_PRIVATE_KEY" | sign_update --ed-key-file - "$PRODUCT_NAME.dmg")
177+
echo "SPARKLE_SIGNATURE=$SIGNATURE" >> $GITHUB_ENV
178+
FILE_SIZE=$(stat -f%z "$PRODUCT_NAME.dmg")
179+
echo "FILE_SIZE=$FILE_SIZE" >> $GITHUB_ENV
180+
181+
- name: Create GitHub Release
182+
env:
183+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
184+
run: |
185+
gh release create "${{ steps.version.outputs.version }}" \
186+
"${{ env.PRODUCT_NAME }}.dmg" \
187+
--title "${{ steps.version.outputs.version }}" \
188+
--notes "Version ${{ steps.version.outputs.version }}"
189+
190+
- name: "Sparkle: Update appcast.xml"
191+
env:
192+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
193+
run: |
194+
VERSION="${{ steps.version.outputs.version }}"
195+
# if PRODUCT_NAME has spaces, replace them with dots
196+
DOWNLOAD_DMG_NAME="${PRODUCT_NAME// /.}.dmg"
197+
DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/$VERSION/$DOWNLOAD_DMG_NAME"
198+
PUB_DATE=$(date -R)
199+
200+
# Create the new item XML in a temp file
201+
cat > item.xml << EOF
202+
203+
<item>
204+
<title>Version $VERSION</title>
205+
<sparkle:version>$VERSION</sparkle:version>
206+
<pubDate>$PUB_DATE</pubDate>
207+
<sparkle:minimumSystemVersion>15.0</sparkle:minimumSystemVersion>
208+
<enclosure
209+
url="$DOWNLOAD_URL"
210+
${{ env.SPARKLE_SIGNATURE }}
211+
type="application/octet-stream" />
212+
</item>
213+
EOF
214+
215+
# Insert the new item after <!-- new first --> marker (newest items first)
216+
sed -i '' '/<!-- new first -->/r item.xml' docs/appcast.xml
217+
218+
- name: "Sparkle: Commit and push appcast.xml & new version changes"
219+
env:
220+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
221+
run: |
222+
git config user.name "github-actions[bot]"
223+
git config user.email "github-actions[bot]@users.noreply.github.com"
224+
git add docs/appcast.xml
225+
git add "$PROJECT/project.pbxproj"
226+
git commit -m "Update appcast.xml for version ${{ steps.version.outputs.version }}"
227+
git push origin HEAD:main
228+
229+
- name: Cleanup Keychain
230+
if: always()
231+
run: |
232+
security delete-keychain $RUNNER_TEMP/app-signing.keychain-db || true

.gitignore

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
# Xcode
2+
build/
3+
DerivedData/
4+
*.xcodeproj/xcuserdata/
5+
*.xcworkspace/xcuserdata/
6+
*.xcodeproj/project.xcworkspace/xcuserdata/
7+
8+
# User-specific project settings
9+
*.xcuserstate
10+
*.xcuserdatad/
11+
xcuserdata/
12+
13+
# Build products
14+
*.ipa
15+
*.dSYM.zip
16+
*.dSYM
17+
18+
# Dependency management
19+
Carthage/Build/
20+
Carthage/Checkouts/
21+
.build/
22+
Packages/
23+
Package.resolved
24+
*.xcframework
25+
26+
# CocoaPods
27+
Pods/
28+
Podfile.lock
29+
30+
# Swift Package Manager
31+
.swiftpm/
32+
33+
# AppCode
34+
.idea/
35+
36+
# Fastlane
37+
fastlane/report.xml
38+
fastlane/Preview.html
39+
fastlane/screenshots/**/*.png
40+
fastlane/test_output
41+
42+
# Code coverage
43+
*.gcno
44+
*.gcda
45+
*.profdata
46+
*.profraw
47+
48+
# Playgrounds
49+
timeline.xctimeline
50+
playground.xcworkspace
51+
52+
# OS generated files
53+
.DS_Store
54+
.DS_Store?
55+
._*
56+
.Spotlight-V100
57+
.Trashes
58+
ehthumbs.db
59+
Thumbs.db
60+
61+
# Backup files
62+
*~
63+
*.orig
64+
*.bak
65+
66+
# Local configuration
67+
*.xcconfig.local
68+
69+
# Secrets
70+
.github/.secrets
71+
*.secrets
72+
.env
73+
.env.*

Archives.xcodeproj/project.pbxproj

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
objects = {
88

99
/* Begin PBXBuildFile section */
10+
270B111E2EFF25EF0074B743 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 270B111D2EFF25EF0074B743 /* Sparkle */; };
1011
270FDB342EF70529002E835A /* Subprocess in Frameworks */ = {isa = PBXBuildFile; productRef = 270FDA8E2EF70300002E835A /* Subprocess */; };
1112
270FDB352EF70529002E835A /* FP in Frameworks */ = {isa = PBXBuildFile; productRef = 270FDA902EF70400002E835A /* FP */; };
1213
/* End PBXBuildFile section */
@@ -70,6 +71,7 @@
7071
isa = PBXFrameworksBuildPhase;
7172
buildActionMask = 2147483647;
7273
files = (
74+
270B111E2EFF25EF0074B743 /* Sparkle in Frameworks */,
7375
270FDB352EF70529002E835A /* FP in Frameworks */,
7476
270FDB342EF70529002E835A /* Subprocess in Frameworks */,
7577
);
@@ -134,6 +136,7 @@
134136
packageProductDependencies = (
135137
270FDA8E2EF70300002E835A /* Subprocess */,
136138
270FDA902EF70400002E835A /* FP */,
139+
270B111D2EFF25EF0074B743 /* Sparkle */,
137140
);
138141
productName = Archives;
139142
productReference = 270FDA632EF70203002E835A /* Archives.app */;
@@ -220,6 +223,7 @@
220223
packageReferences = (
221224
270FDA8D2EF70300002E835A /* XCRemoteSwiftPackageReference "swift-subprocess" */,
222225
270FDA8F2EF70400002E835A /* XCRemoteSwiftPackageReference "fp-swift" */,
226+
270B111C2EFF25EF0074B743 /* XCRemoteSwiftPackageReference "Sparkle" */,
223227
);
224228
preferredProjectObjectVersion = 77;
225229
productRefGroup = 270FDA642EF70203002E835A /* Products */;
@@ -613,6 +617,14 @@
613617
/* End XCConfigurationList section */
614618

615619
/* Begin XCRemoteSwiftPackageReference section */
620+
270B111C2EFF25EF0074B743 /* XCRemoteSwiftPackageReference "Sparkle" */ = {
621+
isa = XCRemoteSwiftPackageReference;
622+
repositoryURL = "https://github.com/sparkle-project/Sparkle";
623+
requirement = {
624+
kind = upToNextMajorVersion;
625+
minimumVersion = 2.8.1;
626+
};
627+
};
616628
270FDA8D2EF70300002E835A /* XCRemoteSwiftPackageReference "swift-subprocess" */ = {
617629
isa = XCRemoteSwiftPackageReference;
618630
repositoryURL = "https://github.com/swiftlang/swift-subprocess.git";
@@ -632,6 +644,11 @@
632644
/* End XCRemoteSwiftPackageReference section */
633645

634646
/* Begin XCSwiftPackageProductDependency section */
647+
270B111D2EFF25EF0074B743 /* Sparkle */ = {
648+
isa = XCSwiftPackageProductDependency;
649+
package = 270B111C2EFF25EF0074B743 /* XCRemoteSwiftPackageReference "Sparkle" */;
650+
productName = Sparkle;
651+
};
635652
270FDA8E2EF70300002E835A /* Subprocess */ = {
636653
isa = XCSwiftPackageProductDependency;
637654
package = 270FDA8D2EF70300002E835A /* XCRemoteSwiftPackageReference "swift-subprocess" */;

Archives.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved

Lines changed: 10 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)