Skip to content

Commit 2448382

Browse files
committed
feat: add GitHub Actions release workflow with code signing
- Add release.yml for building macOS app (arm64, x64, universal) - Enable code signing with Developer ID certificate - Enable notarization for Gatekeeper approval - Add entitlements for iCloud, Push Notifications, Sign in with Apple, App Groups
1 parent 4be1084 commit 2448382

File tree

2 files changed

+333
-0
lines changed

2 files changed

+333
-0
lines changed

.github/workflows/release.yml

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
# SPDX-FileCopyrightText: 2026 AprilNEA <dev@aprilnea.me>
2+
# SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3+
4+
name: Release
5+
6+
on:
7+
push:
8+
tags:
9+
- "v*.*.*"
10+
workflow_dispatch:
11+
inputs:
12+
tag:
13+
description: "Release tag (e.g., v0.1.0)"
14+
required: false
15+
type: string
16+
17+
concurrency:
18+
group: ${{ github.workflow }}-${{ github.ref }}
19+
cancel-in-progress: true
20+
21+
env:
22+
CARGO_TERM_COLOR: always
23+
MACOSX_DEPLOYMENT_TARGET: "12.0"
24+
25+
jobs:
26+
build-macos:
27+
name: Build macOS (${{ matrix.target }})
28+
runs-on: ${{ matrix.runner }}
29+
strategy:
30+
fail-fast: false
31+
matrix:
32+
include:
33+
- target: aarch64-apple-darwin
34+
runner: macos-14
35+
arch: arm64
36+
- target: x86_64-apple-darwin
37+
runner: macos-13
38+
arch: x64
39+
40+
steps:
41+
- uses: actions/checkout@v4
42+
43+
- name: Setup Rust
44+
run: |
45+
rustup toolchain install nightly-2026-01-18 --profile minimal
46+
rustup default nightly-2026-01-18
47+
rustup target add ${{ matrix.target }}
48+
49+
- name: Setup Rust cache
50+
uses: Swatinem/rust-cache@v2
51+
with:
52+
prefix-key: "v1-rust"
53+
shared-key: ${{ matrix.target }}
54+
55+
- name: Install cargo-bundle
56+
run: cargo install cargo-bundle
57+
58+
- name: Build release binary
59+
run: cargo build --release --target ${{ matrix.target }}
60+
61+
- name: Bundle macOS app
62+
run: cargo bundle --release --target ${{ matrix.target }}
63+
64+
- name: Upload app bundle
65+
uses: actions/upload-artifact@v4
66+
with:
67+
name: ChatGPUI-app-${{ matrix.arch }}
68+
path: target/${{ matrix.target }}/release/bundle/osx/ChatGPUI.app
69+
if-no-files-found: error
70+
71+
create-universal-binary:
72+
name: Create Universal Binary
73+
needs: build-macos
74+
runs-on: macos-14
75+
steps:
76+
- uses: actions/checkout@v4
77+
78+
- name: Setup Rust
79+
run: |
80+
rustup toolchain install nightly-2026-01-18 --profile minimal
81+
rustup default nightly-2026-01-18
82+
rustup target add aarch64-apple-darwin x86_64-apple-darwin
83+
84+
- name: Setup Rust cache
85+
uses: Swatinem/rust-cache@v2
86+
with:
87+
prefix-key: "v1-rust"
88+
shared-key: "universal"
89+
90+
- name: Install cargo-bundle
91+
run: cargo install cargo-bundle
92+
93+
- name: Build both architectures
94+
run: |
95+
cargo build --release --target aarch64-apple-darwin
96+
cargo build --release --target x86_64-apple-darwin
97+
98+
- name: Create universal binary
99+
run: |
100+
mkdir -p target/universal-apple-darwin/release
101+
lipo -create \
102+
target/aarch64-apple-darwin/release/chatgpui \
103+
target/x86_64-apple-darwin/release/chatgpui \
104+
-output target/universal-apple-darwin/release/chatgpui
105+
106+
- name: Bundle universal app
107+
run: |
108+
APP_NAME="ChatGPUI"
109+
110+
# Bundle from arm64 target first
111+
cargo bundle --release --target aarch64-apple-darwin
112+
113+
# Copy bundled app
114+
cp -r "target/aarch64-apple-darwin/release/bundle/osx/${APP_NAME}.app" \
115+
"target/universal-apple-darwin/release/"
116+
117+
# Replace binary with universal binary
118+
cp target/universal-apple-darwin/release/chatgpui \
119+
"target/universal-apple-darwin/release/${APP_NAME}.app/Contents/MacOS/chatgpui"
120+
121+
- name: Upload universal app bundle
122+
uses: actions/upload-artifact@v4
123+
with:
124+
name: ChatGPUI-app-universal
125+
path: target/universal-apple-darwin/release/ChatGPUI.app
126+
if-no-files-found: error
127+
128+
sign-and-notarize:
129+
name: Sign and Notarize (${{ matrix.arch }})
130+
needs: [build-macos, create-universal-binary]
131+
runs-on: macos-14
132+
strategy:
133+
fail-fast: false
134+
matrix:
135+
arch: [arm64, x64, universal]
136+
steps:
137+
- uses: actions/checkout@v4
138+
with:
139+
sparse-checkout: bundle/ChatGPUI.entitlements
140+
sparse-checkout-cone-mode: false
141+
142+
- name: Download app bundle
143+
uses: actions/download-artifact@v4
144+
with:
145+
name: ChatGPUI-app-${{ matrix.arch }}
146+
path: ChatGPUI.app
147+
148+
- name: Import certificates
149+
env:
150+
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
151+
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
152+
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
153+
run: |
154+
# Create temporary keychain
155+
echo "$APPLE_CERTIFICATE" | base64 --decode > certificate.p12
156+
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
157+
security default-keychain -s build.keychain
158+
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
159+
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
160+
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
161+
162+
# Verify certificate is available
163+
security find-identity -v -p codesigning build.keychain
164+
165+
- name: Sign app
166+
env:
167+
APPLE_IDENTITY: ${{ secrets.APPLE_IDENTITY }}
168+
run: |
169+
ENTITLEMENTS="bundle/ChatGPUI.entitlements"
170+
171+
# Sign all nested components first (frameworks, libraries, etc.)
172+
find ChatGPUI.app/Contents -type f \( -name "*.dylib" -o -name "*.so" -o -perm +111 \) -exec \
173+
codesign --force --options runtime --sign "$APPLE_IDENTITY" --timestamp {} \;
174+
175+
# Sign the main app bundle with entitlements
176+
codesign --force --options runtime \
177+
--sign "$APPLE_IDENTITY" \
178+
--timestamp \
179+
--entitlements "$ENTITLEMENTS" \
180+
ChatGPUI.app
181+
182+
# Verify signature
183+
codesign --verify --deep --strict --verbose=2 ChatGPUI.app
184+
185+
# Display entitlements for verification
186+
codesign -d --entitlements :- ChatGPUI.app
187+
188+
- name: Create DMG
189+
run: |
190+
APP_NAME="ChatGPUI"
191+
DMG_NAME="ChatGPUI-${{ matrix.arch }}.dmg"
192+
193+
mkdir -p dmg-contents
194+
cp -r ChatGPUI.app dmg-contents/
195+
ln -s /Applications dmg-contents/Applications
196+
197+
hdiutil create -volname "${APP_NAME}" \
198+
-srcfolder dmg-contents \
199+
-ov -format UDZO \
200+
"${DMG_NAME}"
201+
202+
rm -rf dmg-contents
203+
204+
- name: Notarize DMG
205+
env:
206+
APPLE_ID: ${{ secrets.APPLE_ID }}
207+
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
208+
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
209+
run: |
210+
DMG_NAME="ChatGPUI-${{ matrix.arch }}.dmg"
211+
212+
# Submit for notarization
213+
xcrun notarytool submit "${DMG_NAME}" \
214+
--apple-id "$APPLE_ID" \
215+
--team-id "$APPLE_TEAM_ID" \
216+
--password "$APPLE_APP_SPECIFIC_PASSWORD" \
217+
--wait
218+
219+
# Staple the notarization ticket
220+
xcrun stapler staple "${DMG_NAME}"
221+
222+
- name: Upload signed DMG
223+
uses: actions/upload-artifact@v4
224+
with:
225+
name: ChatGPUI-macos-${{ matrix.arch }}-signed
226+
path: ChatGPUI-${{ matrix.arch }}.dmg
227+
if-no-files-found: error
228+
229+
- name: Cleanup keychain
230+
if: always()
231+
run: |
232+
security delete-keychain build.keychain || true
233+
234+
release:
235+
name: Create Release
236+
needs: sign-and-notarize
237+
runs-on: ubuntu-latest
238+
if: startsWith(github.ref, 'refs/tags/v')
239+
permissions:
240+
contents: write
241+
steps:
242+
- uses: actions/checkout@v4
243+
with:
244+
fetch-depth: 0
245+
246+
- name: Download all signed artifacts
247+
uses: actions/download-artifact@v4
248+
with:
249+
pattern: ChatGPUI-macos-*-signed
250+
path: artifacts
251+
252+
- name: Prepare release assets
253+
run: |
254+
mkdir -p release-assets
255+
find artifacts -name "*.dmg" -exec cp {} release-assets/ \;
256+
ls -la release-assets/
257+
258+
- name: Generate changelog
259+
id: changelog
260+
run: |
261+
# Get the previous tag
262+
PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
263+
CURRENT_TAG=${GITHUB_REF#refs/tags/}
264+
265+
echo "## What's Changed" > CHANGELOG.md
266+
echo "" >> CHANGELOG.md
267+
268+
if [ -n "$PREV_TAG" ]; then
269+
git log ${PREV_TAG}..HEAD --pretty=format:"- %s (%h)" >> CHANGELOG.md
270+
else
271+
git log --pretty=format:"- %s (%h)" -20 >> CHANGELOG.md
272+
fi
273+
274+
echo "" >> CHANGELOG.md
275+
echo "" >> CHANGELOG.md
276+
echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${CURRENT_TAG}" >> CHANGELOG.md
277+
278+
- name: Create GitHub Release
279+
uses: softprops/action-gh-release@v2
280+
with:
281+
files: release-assets/*
282+
body_path: CHANGELOG.md
283+
draft: false
284+
prerelease: ${{ contains(github.ref, '-alpha') || contains(github.ref, '-beta') || contains(github.ref, '-rc') }}
285+
generate_release_notes: true
286+
env:
287+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

bundle/ChatGPUI.entitlements

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<!-- Hardened Runtime -->
6+
<key>com.apple.security.app-sandbox</key>
7+
<false/>
8+
<key>com.apple.security.hardened-runtime</key>
9+
<true/>
10+
11+
<!-- Network access for LLM API calls -->
12+
<key>com.apple.security.network.client</key>
13+
<true/>
14+
15+
<!-- iCloud -->
16+
<key>com.apple.developer.icloud-container-identifiers</key>
17+
<array>
18+
<string>iCloud.com.aprilnea.chatgpui</string>
19+
</array>
20+
<key>com.apple.developer.icloud-services</key>
21+
<array>
22+
<string>CloudDocuments</string>
23+
<string>CloudKit</string>
24+
</array>
25+
<key>com.apple.developer.ubiquity-container-identifiers</key>
26+
<array>
27+
<string>iCloud.com.aprilnea.chatgpui</string>
28+
</array>
29+
30+
<!-- Push Notifications -->
31+
<key>com.apple.developer.aps-environment</key>
32+
<string>production</string>
33+
34+
<!-- Sign in with Apple -->
35+
<key>com.apple.developer.applesignin</key>
36+
<array>
37+
<string>Default</string>
38+
</array>
39+
40+
<!-- App Groups (for sharing data with iOS app) -->
41+
<key>com.apple.security.application-groups</key>
42+
<array>
43+
<string>group.com.aprilnea.chatgpui</string>
44+
</array>
45+
</dict>
46+
</plist>

0 commit comments

Comments
 (0)