Skip to content

Commit d955267

Browse files
committed
feat: add sparkle updater pipeline
1 parent 72df445 commit d955267

31 files changed

Lines changed: 893 additions & 49 deletions

.github/workflows/release.yml

Lines changed: 101 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ env:
1313
MACOS_SIGNING_IDENTITY: ${{ secrets.MACOS_SIGNING_IDENTITY }}
1414
MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
1515
MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
16+
SPARKLE_ENABLE: ${{ vars.SPARKLE_ENABLE }}
17+
SPARKLE_APPCAST_BRANCH: ${{ vars.SPARKLE_APPCAST_BRANCH }}
18+
SPARKLE_FEED_URL: ${{ secrets.SPARKLE_FEED_URL }}
19+
SPARKLE_PRIVATE_ED_KEY: ${{ secrets.SPARKLE_PRIVATE_ED_KEY }}
20+
SPARKLE_PUBLIC_ED_KEY: ${{ secrets.SPARKLE_PUBLIC_ED_KEY }}
1621
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true
1722

1823
jobs:
@@ -53,17 +58,17 @@ jobs:
5358
goarch: arm64
5459
wails-platform: darwin/arm64
5560
package-ext: dmg
56-
asset-name: GetTokens_darwin_arm64.dmg
57-
updater-asset-name: GetTokens_darwin_arm64.tar.gz
61+
asset-name: GetTokens_macOS_AppleSilicon.dmg
62+
updater-asset-name: GetTokens_macOS_AppleSilicon.tar.gz
5863

5964
- runner: macos-15-intel
6065
os-name: macOS amd64
6166
goos: darwin
6267
goarch: amd64
6368
wails-platform: darwin/amd64
6469
package-ext: dmg
65-
asset-name: GetTokens_darwin_amd64.dmg
66-
updater-asset-name: GetTokens_darwin_amd64.tar.gz
70+
asset-name: GetTokens_macOS_Intel.dmg
71+
updater-asset-name: GetTokens_macOS_Intel.tar.gz
6772

6873
steps:
6974
- uses: actions/checkout@v5
@@ -158,6 +163,20 @@ jobs:
158163
chmod +x build/bin/GetTokens.app/Contents/MacOS/cli-proxy-api
159164
file build/bin/GetTokens.app/Contents/MacOS/cli-proxy-api
160165
166+
- name: Configure Sparkle metadata
167+
if: runner.os == 'macOS' && env.SPARKLE_ENABLE == '1'
168+
shell: bash
169+
run: |
170+
chmod +x scripts/configure-sparkle-macos.sh
171+
scripts/configure-sparkle-macos.sh "build/bin/GetTokens.app"
172+
173+
- name: Embed Sparkle framework
174+
if: runner.os == 'macOS' && env.SPARKLE_ENABLE == '1'
175+
shell: bash
176+
run: |
177+
chmod +x scripts/prepare-sparkle-framework.sh scripts/embed-sparkle-framework.sh
178+
scripts/embed-sparkle-framework.sh "build/bin/GetTokens.app"
179+
161180
# ── Package ───────────────────────────────────────────────────────────
162181
- name: Sign and notarize macOS app
163182
if: runner.os == 'macOS'
@@ -198,10 +217,83 @@ jobs:
198217
fi
199218
rm -f "$RUNNER_TEMP/developer-id.p12" "${MACOS_NOTARY_KEY_PATH:-}"
200219
220+
sparkle-appcast:
221+
name: Publish Sparkle appcast
222+
if: vars.SPARKLE_ENABLE == '1'
223+
needs: build
224+
runs-on: macos-latest
225+
steps:
226+
- uses: actions/checkout@v5
227+
with:
228+
fetch-depth: 0
229+
230+
- uses: actions/download-artifact@v5
231+
with:
232+
path: dist/release/
233+
merge-multiple: true
234+
235+
- name: Restore previous appcast
236+
shell: bash
237+
run: |
238+
mkdir -p dist/sparkle-feed
239+
APPCAST_BRANCH="${SPARKLE_APPCAST_BRANCH:-sparkle-appcast}"
240+
if git ls-remote --exit-code origin "refs/heads/${APPCAST_BRANCH}" >/dev/null 2>&1; then
241+
git fetch origin "${APPCAST_BRANCH}:${APPCAST_BRANCH}"
242+
if git cat-file -e "${APPCAST_BRANCH}:appcast.xml" 2>/dev/null; then
243+
git show "${APPCAST_BRANCH}:appcast.xml" > dist/sparkle-feed/appcast.xml
244+
fi
245+
fi
246+
247+
- name: Generate Sparkle appcast
248+
shell: bash
249+
run: |
250+
chmod +x scripts/prepare-sparkle-framework.sh scripts/generate-sparkle-appcast.sh
251+
scripts/generate-sparkle-appcast.sh dist/release dist/sparkle-feed
252+
env:
253+
SPARKLE_RELEASE_BASE_URL: https://github.com/${{ github.repository }}/releases/download/${{ github.ref_name }}
254+
SPARKLE_FULL_RELEASE_NOTES_URL: https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }}
255+
SPARKLE_PRODUCT_URL: https://github.com/${{ github.repository }}
256+
257+
- name: Publish Sparkle appcast branch
258+
shell: bash
259+
run: |
260+
APPCAST_BRANCH="${SPARKLE_APPCAST_BRANCH:-sparkle-appcast}"
261+
PUBLISH_DIR="$RUNNER_TEMP/sparkle-appcast-publish"
262+
REPO_URL="https://x-access-token:${GITHUB_TOKEN}@github.com/${GITHUB_REPOSITORY}.git"
263+
264+
rm -rf "${PUBLISH_DIR}"
265+
if git ls-remote --exit-code origin "refs/heads/${APPCAST_BRANCH}" >/dev/null 2>&1; then
266+
git clone --branch "${APPCAST_BRANCH}" --single-branch "${REPO_URL}" "${PUBLISH_DIR}"
267+
else
268+
git clone --single-branch "${REPO_URL}" "${PUBLISH_DIR}"
269+
(
270+
cd "${PUBLISH_DIR}"
271+
git checkout --orphan "${APPCAST_BRANCH}"
272+
)
273+
fi
274+
275+
(
276+
cd "${PUBLISH_DIR}"
277+
find . -mindepth 1 -maxdepth 1 ! -name .git -exec rm -rf {} +
278+
cp "${GITHUB_WORKSPACE}/dist/sparkle-feed/appcast.xml" ./appcast.xml
279+
git add appcast.xml
280+
if git diff --cached --quiet; then
281+
echo "Sparkle appcast unchanged; skipping commit."
282+
exit 0
283+
fi
284+
git config user.name "github-actions[bot]"
285+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
286+
git commit -m "chore: update Sparkle appcast for ${GITHUB_REF_NAME}"
287+
git push origin HEAD:"${APPCAST_BRANCH}"
288+
)
289+
201290
# ── Publish GitHub Release ────────────────────────────────────────────────
202291
release:
203292
name: Publish Release
204-
needs: build
293+
needs:
294+
- build
295+
- sparkle-appcast
296+
if: ${{ always() && needs.build.result == 'success' && (needs.sparkle-appcast.result == 'success' || needs.sparkle-appcast.result == 'skipped') }}
205297
runs-on: ubuntu-latest
206298
steps:
207299
- uses: actions/checkout@v5
@@ -220,9 +312,9 @@ jobs:
220312
uses: softprops/action-gh-release@v3
221313
with:
222314
files: |
223-
dist/release/GetTokens_darwin_arm64.dmg
224-
dist/release/GetTokens_darwin_arm64.tar.gz
225-
dist/release/GetTokens_darwin_amd64.dmg
226-
dist/release/GetTokens_darwin_amd64.tar.gz
315+
dist/release/GetTokens_macOS_AppleSilicon.dmg
316+
dist/release/GetTokens_macOS_AppleSilicon.tar.gz
317+
dist/release/GetTokens_macOS_Intel.dmg
318+
dist/release/GetTokens_macOS_Intel.tar.gz
227319
dist/release/checksums.txt
228320
generate_release_notes: true

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ dist/
99
frontend/dist
1010
frontend/node_modules
1111
.playwright-cli
12+
.local/

README.md

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,16 +82,17 @@ Releases are published on GitHub Releases:
8282

8383
The current release workflow produces the following asset types:
8484

85-
- macOS Apple Silicon:`GetTokens_darwin_arm64.dmg`
86-
- macOS Apple Silicon updater asset:`GetTokens_darwin_arm64.tar.gz`
87-
- macOS Intel:`GetTokens_darwin_amd64.dmg`
88-
- macOS Intel updater asset:`GetTokens_darwin_amd64.tar.gz`
85+
- macOS Apple Silicon:`GetTokens_macOS_AppleSilicon.dmg`
86+
- macOS Apple Silicon updater asset:`GetTokens_macOS_AppleSilicon.tar.gz`
87+
- macOS Intel:`GetTokens_macOS_Intel.dmg`
88+
- macOS Intel updater asset:`GetTokens_macOS_Intel.tar.gz`
8989
- Checksums:`checksums.txt`
9090

9191
## 自动更新说明 | Auto Update Notes
9292

9393
- macOS 出于已签名 `.app` bundle 完整性约束,只执行“检查更新 + 打开 release 页面下载 DMG”。
9494
- On macOS, due to signed `.app` bundle integrity constraints, the app uses “check update + open release page for DMG download” instead of in-place bundle replacement.
95+
- 实验链路:当 release workflow 启用 `SPARKLE_ENABLE=1` 且提供 Sparkle feed / public key 后,macOS 构建会预埋 Sparkle 所需 metadata 与 framework,为后续原生更新切换做准备。
9596

9697
## 项目结构 | Project Layout
9798

app.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,10 @@ func (a *App) CanApplyUpdate() bool {
188188
return a.core.CanApplyUpdate()
189189
}
190190

191+
func (a *App) UsesNativeUpdaterUI() bool {
192+
return a.core.UsesNativeUpdaterUI()
193+
}
194+
191195
func (a *App) CheckUpdate() (*updater.ReleaseInfo, error) {
192196
return a.core.CheckUpdate()
193197
}

docs-linhay/dev/20260426-release-prep-guide.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
当前阶段先只支持 macOS release,资产分成两类:
1212

1313
1. 用户下载安装包
14-
- macOS Apple Silicon: `GetTokens_darwin_arm64.dmg`
15-
- macOS Intel: `GetTokens_darwin_amd64.dmg`
14+
- macOS Apple Silicon: `GetTokens_macOS_AppleSilicon.dmg`
15+
- macOS Intel: `GetTokens_macOS_Intel.dmg`
1616
2. 自动升级资产
17-
- macOS Apple Silicon: `GetTokens_darwin_arm64.tar.gz`
18-
- macOS Intel: `GetTokens_darwin_amd64.tar.gz`
17+
- macOS Apple Silicon: `GetTokens_macOS_AppleSilicon.tar.gz`
18+
- macOS Intel: `GetTokens_macOS_Intel.tar.gz`
1919

2020
说明:
2121
- macOS 保留 `tar.gz` 资产用于检测最新版本和统一校验链,但签名发布包不做 bundle 内原地替换,设置页会跳转到 release 页面安装。
@@ -84,14 +84,25 @@ CI release workflow 需要以下 secrets:
8484
App Store Connect API Key 的 issuer id
8585
6. `MACOS_NOTARY_API_KEY_BASE64`
8686
`AuthKey_<KEY_ID>.p8` 文件内容做 base64 后存入
87+
7. `SPARKLE_FEED_URL`(可选,Sparkle 实验链路)
88+
推荐固定为 `https://raw.githubusercontent.com/AxApp/GetTokens/sparkle-appcast/appcast.xml`
89+
8. `SPARKLE_PUBLIC_ED_KEY`(可选,Sparkle 实验链路)
90+
9. `SPARKLE_PRIVATE_ED_KEY`(可选,Sparkle 实验链路)
91+
用于 `generate_appcast` 在 CI 中对 `appcast.xml` 做 EdDSA 签名
92+
93+
补充:
94+
95+
1. 只有在 GitHub Actions variable `SPARKLE_ENABLE=1` 时,release workflow 才会尝试写入 Sparkle metadata、嵌入 `Sparkle.framework`、生成并发布 `appcast.xml`
96+
2. `SPARKLE_APPCAST_BRANCH` 默认为 `sparkle-appcast`,workflow 会把最新 `appcast.xml` 推到该分支,再由 `raw.githubusercontent.com` 提供稳定 feed URL
97+
3. 未启用时,现有 GitHub Release + DMG 发布链路保持不变
8798

8899
## 原则
89100
1. 自动升级资产必须可直接解压出目标可执行文件,不能是安装器。
90101
2. 自动升级比较继续使用语义化版本 tag,例如 `v0.1.0`
91102
3. UI 展示版本时间使用 `ReleaseLabel`,不和 `Version` 混用。
92103
4. macOS updater 资产名称必须按目标架构区分,避免 `arm64` / `amd64` 互相误命中。
93104
5. macOS release workflow 必须先把源码构建出来的目标架构 sidecar 回填进 `.app`,再 notarize `.app`,然后从已 stapled 的 `.app` 生成 DMG,最后再 notarize DMG。
94-
6. 已签名 macOS `.app` 在当前框架下只支持“检查更新 + 跳转 release 页面”,不支持 bundle 内 `ApplyUpdate`
105+
6. 未启用 Sparkle 时,已签名 macOS `.app` 只支持“检查更新 + 跳转 release 页面”;启用 Sparkle 后,更新入口会切到原生更新 UI
95106

96107
## 建议发布步骤
97108
1. 确认工作区只包含本次准备发布的变更。
@@ -103,6 +114,7 @@ CI release workflow 需要以下 secrets:
103114
- 安装包资产存在
104115
- updater 资产存在
105116
- `checksums.txt` 包含全部资产
117+
- 若启用 Sparkle:`sparkle-appcast` 分支上的 `appcast.xml` 已更新,且 `SUFeedURL` 指向固定 raw URL
106118
- macOS DMG 已经 stapled,`xcrun stapler validate` 通过
107119
7. 使用非 dev 构建验证:
108120
- `CheckUpdate` 能发现新版本
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# macOS Sparkle Updater 接入方案
2+
3+
## 背景
4+
5+
GetTokens 当前在 macOS 上使用 `go-selfupdate` 做“检查更新”,但不执行 bundle 内原地替换,只打开 GitHub Release 页面让用户手动下载 DMG。
6+
7+
原因不是更新检测能力不足,而是:
8+
9+
1. macOS signed / notarized `.app` bundle 不适合直接用当前方式做二进制替换
10+
2. 现有发布产物和 updater 校验链更偏跨平台统一方案,不是 macOS 原生升级方案
11+
12+
## 目标
13+
14+
对 macOS 引入 Sparkle,同时保留:
15+
16+
1. GitHub Release 作为最终公开发布源
17+
2. 现有 DMG / updater 资产分发链
18+
3. 非 macOS 平台继续沿用当前 updater 逻辑
19+
20+
## 分层设计
21+
22+
### 1. 前端层
23+
24+
前端不直接理解 Sparkle framework,只消费一个更抽象的“macOS 可原生更新”能力。
25+
26+
建议边界:
27+
28+
1. `CanApplyUpdate()` 不再仅以 `runtime.GOOS != "darwin"` 判定
29+
2. macOS 在 Sparkle 可用后可返回 `true`
30+
3. 前端按钮语义可保持“检查更新 / 安装更新”,但实际走 Sparkle 原生流程
31+
32+
### 2. Go / Wails 层
33+
34+
建议新增一个 macOS 专属 updater bridge:
35+
36+
1. 负责启动 Sparkle updater
37+
2. 暴露“用户触发检查更新”入口
38+
3. 暴露“应用启动时自动检查”策略
39+
40+
当前阶段先不落 framework 联编,只先把发布基础设施补齐。
41+
42+
### 3. Bundle / 发布层
43+
44+
Sparkle 至少需要:
45+
46+
1. `SUFeedURL`
47+
2. `SUPublicEDKey`
48+
3. 正确递增的 `CFBundleVersion`
49+
4. app bundle 内可加载 Sparkle framework
50+
51+
当前阶段先完成第 1、2、3 项的预留;第 4 项放到下一阶段。
52+
53+
## 第一阶段落地内容
54+
55+
1. 新增脚本:`scripts/configure-sparkle-macos.sh`
56+
2. 新增脚本:`scripts/prepare-sparkle-framework.sh`
57+
3. 新增脚本:`scripts/embed-sparkle-framework.sh`
58+
4. `configure-sparkle-macos.sh` 负责把 Sparkle 元数据写入 `GetTokens.app/Contents/Info.plist`
59+
5. `embed-sparkle-framework.sh` 负责把官方 release 中的 `Sparkle.framework` 拷入 app bundle
60+
6. release workflow 在 secrets 就绪时可选调用 plist 注入;framework 嵌入在下一阶段切换为正式启用
61+
62+
## 第二阶段当前进度
63+
64+
当前已额外完成:
65+
66+
1. 新增 `internal/sparkle/` darwin bridge skeleton
67+
2. bridge 现已改为动态加载 `Sparkle.framework`,不依赖编译期静态链接
68+
3. `wailsapp` 在检测到 Sparkle 可用时,会把 macOS 更新入口切到原生更新 UI 模式
69+
4. 前端设置页已能识别“原生更新 UI”模式,并在该模式下只保留单次“检查更新”入口
70+
5. release workflow 现已支持生成签名后的 `appcast.xml`,并推送到固定分支供 `raw.githubusercontent.com` 托管
71+
72+
当前仍未完成:
73+
74+
1. Sparkle API 的回调事件还没有回流到前端状态栏
75+
2. 还没有用真实 feed 完成端到端升级回归
76+
77+
## Secrets / 环境变量建议
78+
79+
1. `SPARKLE_FEED_URL`
80+
2. `SPARKLE_PUBLIC_ED_KEY`
81+
3. `SPARKLE_PRIVATE_ED_KEY`
82+
4. `SPARKLE_APPCAST_BRANCH`(推荐固定为 `sparkle-appcast`
83+
84+
当前 appcast 发布策略:
85+
86+
1. release 产物继续上传到 GitHub Release
87+
2. `generate_appcast` 只消费 notarized `.dmg`,不消费纯二进制 `.tar.gz`
88+
3. workflow 从既有 `appcast.xml` 增量生成新 feed,并把结果推送到 `sparkle-appcast` 分支
89+
4. `SUFeedURL` 固定指向 `https://raw.githubusercontent.com/AxApp/GetTokens/sparkle-appcast/appcast.xml`
90+
91+
## 官方要求摘要
92+
93+
根据 Sparkle 官方文档:
94+
95+
1. 外部构建系统需要自己负责 link framework、拷贝到 `Contents/Frameworks/`、设置 rpath
96+
2. `Info.plist` 需要包含 `SUFeedURL``SUPublicEDKey`
97+
3. `CFBundleVersion` 必须可递增
98+
4. appcast 推荐使用 `generate_appcast` 生成并签名
99+
100+
参考:
101+
102+
1. https://sparkle-project.org/documentation/programmatic-setup/
103+
2. https://sparkle-project.org/documentation/publishing/
104+
105+
## 当前未完成项
106+
107+
1. Sparkle framework 已支持通过脚本下载并嵌入 app bundle,运行时也已通过动态 bridge 接入,但缺少升级事件回流
108+
2. `appcast.xml` 生成与分支发布链路已接入 workflow,但还没有在真实 release 上启用验证
109+
3. 前端设置页已切到“原生更新 UI”识别模式,但仍缺少 Sparkle 状态反馈
110+
4. 还没有真实的 Sparkle 升级回归

docs-linhay/memory/2026-04-26.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@
164164
- 发布版本继续上调到 `v0.1.3`,避免复用已失败的 `v0.1.1` / `v0.1.2` tag。
165165
- 产品发布范围调整:当前阶段先只支持 macOS,release workflow 暂时移除 Windows / Linux 构建与资产发布,只保留 `GetTokens_darwin_universal.dmg``GetTokens_darwin_universal.tar.gz``checksums.txt`
166166
- 发布治理:为消除 GitHub Actions 的 Node 20 deprecation 警告,release workflow 已升级到 Node 24 运行时对应的大版本:`actions/checkout@v5``actions/setup-node@v6``actions/setup-go@v6``actions/download-artifact@v5``actions/upload-artifact@v6``softprops/action-gh-release@v3`
167-
- 发布策略调整:macOS 不再使用单一 `universal` DMG / updater 资产,改为按架构拆分为 `GetTokens_darwin_arm64.*``GetTokens_darwin_amd64.*` 两套产物
167+
- 发布策略调整:macOS 不再使用单一 `universal` DMG / updater 资产,改为按架构拆分为 `GetTokens_macOS_AppleSilicon.*``GetTokens_macOS_Intel.*` 两套对外产物;内部构建与平台判断仍沿用 `arm64/amd64`
168168
- 实现同步:release workflow 已切为两个 macOS job,分别在 `macos-latest`(arm64)和 `macos-15-intel`(amd64)上构建、签名、公证并发布;`internal/updater` 也移除了 `UniversalArch=universal` 绑定,改为让 `go-selfupdate` 按实际架构匹配资产。
169169

170170
## Account Rotation Controls

0 commit comments

Comments
 (0)