-
Notifications
You must be signed in to change notification settings - Fork 2
216 lines (198 loc) · 8.08 KB
/
Copy pathrelease-macos.yml
File metadata and controls
216 lines (198 loc) · 8.08 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
# Auto-build, sign, notarize, and release the macOS native app on tag push.
#
# Trigger: push a tag matching `v*-macos` (e.g. v1.4.0-macos).
#
# Required GitHub Secrets (Settings → Secrets and variables → Actions → New repository secret):
#
# APPLE_DEVELOPER_ID_P12
# Base64-encoded .p12 export of "Developer ID Application: INNO-HI Inc.".
# Create with:
# security find-certificate -c "Developer ID Application: INNO-HI Inc." -p > cert.p12
# (or export via Keychain Access → File → Export Items → .p12)
# base64 -i cert.p12 | pbcopy
#
# APPLE_DEVELOPER_ID_P12_PASSWORD
# Password used when exporting the .p12.
#
# APPLE_NOTARY_APPLE_ID
# Apple ID email registered with the Developer Program (e.g. board@innohi.ai.kr).
#
# APPLE_NOTARY_TEAM_ID
# Team ID (4AL4PF4BK4 for INNO-HI).
#
# APPLE_NOTARY_PASSWORD
# App-specific password generated at appleid.apple.com → Sign-In and Security
# → App-Specific Passwords. Do NOT use the Apple ID password directly.
#
# SPARKLE_ED_PRIVATE_KEY
# Base64-encoded Sparkle EdDSA private key. Export with:
# vendor/bin/generate_keys -x - | base64 | pbcopy
#
# PAGES_DEPLOY_TOKEN
# Fine-grained Personal Access Token with `contents: write` on this repo,
# used to push the updated appcast.xml back to main. Create at
# https://github.com/settings/personal-access-tokens.
name: Release macOS App
on:
push:
tags:
- "v*-macos"
workflow_dispatch:
inputs:
tag:
description: "Existing tag to (re)build (e.g. v1.4.0-macos)"
required: true
type: string
permissions:
contents: write
jobs:
build:
runs-on: macos-14
timeout-minutes: 30
defaults:
run:
working-directory: macos
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Show toolchain versions
run: |
xcodebuild -version
swift --version
xcrun notarytool --version
- name: Import Developer ID certificate
env:
P12_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_P12 }}
P12_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ github.run_id }}
run: |
echo "$P12_BASE64" | base64 --decode > /tmp/cert.p12
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -lut 21600 build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security import /tmp/cert.p12 -k build.keychain -P "$P12_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
security set-key-partition-list -S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
rm /tmp/cert.p12
security find-identity -p codesigning -v build.keychain
- name: Store notary credentials
env:
APPLE_ID: ${{ secrets.APPLE_NOTARY_APPLE_ID }}
TEAM_ID: ${{ secrets.APPLE_NOTARY_TEAM_ID }}
NOTARY_PASSWORD: ${{ secrets.APPLE_NOTARY_PASSWORD }}
run: |
xcrun notarytool store-credentials "ClaudeUsageWidget" \
--apple-id "$APPLE_ID" \
--team-id "$TEAM_ID" \
--password "$NOTARY_PASSWORD"
- name: Restore Sparkle EdDSA private key
env:
ED_KEY: ${{ secrets.SPARKLE_ED_PRIVATE_KEY }}
run: |
mkdir -p ~/Library/Application\ Support/Sparkle
# Sparkle stores the key in Keychain; sign_update reads it from there.
# The simpler path: write the base64 decoded key to ~/.private/sparkle_ed.key
# and pipe it through sign_update's stdin. But Sparkle 2 sign_update
# only reads from Keychain. So we import via security add-generic-password.
KEY_RAW=$(echo "$ED_KEY" | base64 --decode)
security add-generic-password \
-a ed25519 \
-s "https://sparkle-project.org" \
-w "$KEY_RAW" \
-U \
build.keychain || true
- name: Build, sign, notarize, DMG
env:
NOTARY_PROFILE: ClaudeUsageWidget
SIGN_IDENTITY: "Developer ID Application: INNO-HI Inc. (${{ secrets.APPLE_NOTARY_TEAM_ID }})"
run: bash build.sh
- name: Capture EdDSA signature and length for appcast
id: sparkle
run: |
set -e
OUTPUT=$(./vendor/bin/sign_update build/ClaudeUsageWidget.dmg)
# OUTPUT looks like: sparkle:edSignature="..." length="..."
SIG=$(echo "$OUTPUT" | sed -n 's/.*sparkle:edSignature="\([^"]*\)".*/\1/p')
LEN=$(echo "$OUTPUT" | sed -n 's/.*length="\([^"]*\)".*/\1/p')
SHA=$(shasum -a 256 build/ClaudeUsageWidget.dmg | awk '{print $1}')
echo "signature=$SIG" >> "$GITHUB_OUTPUT"
echo "length=$LEN" >> "$GITHUB_OUTPUT"
echo "sha256=$SHA" >> "$GITHUB_OUTPUT"
echo "Sparkle signature: $SIG"
echo "Length: $LEN"
echo "SHA256: $SHA"
- name: Upload DMG to GitHub Release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.event.inputs.tag || github.ref_name }}
working-directory: ${{ github.workspace }}
run: |
# Create the release if it doesn't exist (no-op if it does)
gh release view "$TAG" --repo "$GITHUB_REPOSITORY" >/dev/null 2>&1 || \
gh release create "$TAG" \
--repo "$GITHUB_REPOSITORY" \
--title "${TAG#v}" \
--notes "Automated release — see CHANGELOG.md."
gh release upload "$TAG" macos/build/ClaudeUsageWidget.dmg --clobber
- name: Update appcast.xml
env:
SIG: ${{ steps.sparkle.outputs.signature }}
LEN: ${{ steps.sparkle.outputs.length }}
TAG: ${{ github.event.inputs.tag || github.ref_name }}
GH_TOKEN: ${{ secrets.PAGES_DEPLOY_TOKEN }}
working-directory: ${{ github.workspace }}
run: |
set -e
VERSION="${TAG%-macos}"
VERSION="${VERSION#v}"
PUBDATE=$(date -u "+%a, %d %b %Y %H:%M:%S +0000")
REPO="$GITHUB_REPOSITORY"
python3 - <<PY
import re, pathlib, os
path = pathlib.Path('docs/appcast.xml')
src = path.read_text(encoding='utf-8')
version = os.environ['VERSION'] if 'VERSION' in os.environ else "${VERSION}"
tag = "${TAG}"
pubdate = "${PUBDATE}"
sig = "${SIG}"
length = "${LEN}"
repo = "${REPO}"
new_item = f''' <item>
<title>Version {version}</title>
<sparkle:shortVersionString>{version}</sparkle:shortVersionString>
<pubDate>{pubdate}</pubDate>
<description><![CDATA[
<p>Auto-released by CI — see <a href="https://github.com/{repo}/releases/tag/{tag}">{tag}</a> notes.</p>
]]></description>
<enclosure
url="https://github.com/{repo}/releases/download/{tag}/ClaudeUsageWidget.dmg"
length="{length}"
type="application/octet-stream"
sparkle:edSignature="{sig}" />
<sparkle:minimumSystemVersion>13.0</sparkle:minimumSystemVersion>
</item>
'''
# Insert the new item right after <channel>...</link>...
out = re.sub(
r'(<link>[^<]+</link>\s*\n)',
r'\1\n' + new_item,
src,
count=1
)
path.write_text(out, encoding='utf-8')
PY
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add docs/appcast.xml
if ! git diff --staged --quiet; then
git commit -m "Sparkle: publish $VERSION via CI"
git push origin HEAD:main
else
echo "appcast.xml already up to date"
fi
- name: Cleanup keychain
if: always()
run: |
security delete-keychain build.keychain || true