我做「听懂」时,一开始把“打包”和“更新”想得太接近:PR 合入,GitHub Actions 生成一个 DMG,放到 Release,应用发现新版,不就完成了吗?真正试着从一个正在运行的旧版本走到新版,才发现这至少是两件事:生产一个可信的包,以及让用户手里的应用确认并安装它。
第一段:合并之后真的有人负责打包#
最早的打包工作流只在我手动触发或推版本 tag 时运行。它能证明 CI 会打包,却不能保证每次合入主线后都有一个可下载的版本。9 月 22 日改完后,main 上的 push 成了第三个入口:算下一个 build 号,提交回主线,运行同一份打包脚本,签名、公证、验证,最后创建带 DMG 和 SHA-256 文件的 GitHub prerelease。手动验证构建仍只产出 Actions artifact,不对外发布。
关键不是多写一个 on: push,而是让三个入口共用同一套构建步骤。本地靠 scripts/package_app.sh,CI 也调用它;版本号从一份 product.json 读取;Release 的更新说明和应用内看到的版本记录同源。否则很容易出现“CI 绿了,但用户下载安装的是另一种拼法”的错觉。
在 GitHub Actions 里,自动入口的核心其实很短:
on:
push:
branches: [main]
jobs:
macos:
runs-on: [self-hosted, macOS, ARM64]
steps:
- uses: actions/checkout@v4
- run: ./scripts/package_app.sh
这段只展示什么时候、在哪里、调用哪条本地构建命令,还不能照抄来发布。我的实际工作流在它前后处理 build 号、签名环境、公证、包验证、Release 资产断言与密钥清理;把这些省掉,“合并即出包”只会自动生产一个没有身份保障的文件。
如果要自己做一个最小版本,先从构建结果可辨认开始:为每次合并分配单调递增的 build 号;让 tag 能唯一指向那次构建;把 DMG 和校验和放在同一个 Release;创建前检查 tag 是否已存在,别用覆盖参数把旧资产悄悄换掉。对 macOS 站外分发,还得用 Developer ID 签名并公证,发布前让系统验证公证票真的在包里。Apple 的公证说明解释了这一步和 Gatekeeper 的关系。
我的工作流也有过“看起来很会自动化”的错误:第二个合并紧跟第一个合并时,如果两个排队的 job 都按自己触发时的旧提交算 build 号,就可能撞号;tag 若在公证完成时才按当下主线创建,又可能指到不是这个 DMG 的源码提交。因此 job 串行,从最新主线重新算 build;Release 明确指向实际打包的 commit;同号已经存在就失败。这些检查并不漂亮,但比让更新器面对两个同名、内容不同的包强得多。
第二段:应用得知道“有没有比我新的包”#
听懂的仓库是私有的,所以我没有把长期 PAT 塞进 App。应用在用户打开版本面板时,借这台机器上已登录的 gh 查询 Releases。没有 gh、没登录或没有仓库权限,都明确报出来;不能把“查询失败”显示成“已经是最新版”。这条路线适合我自己的受控设备,不是面向陌生用户的通用更新方案。公开分发的应用通常需要另外设计可访问的更新源和验证机制。
比较版本时,我只接受 build 号更高的包,而不是猜 Release 标题里哪段数字算版本。下载时 DMG 和 .sha256 一起取,校验和缺失或不一致就删除下载文件,不让一个来历不明的安装包留在文件夹里等人下周误点。到这里,用户已经能看到版本、读更新记录、拿到经过核对的包。但“能下载”仍不是“能替换正在运行的自己”。
第三段:旧 App 怎么安全退出,新 App 怎么接上#
9 月 25 日,我把最后一步做成「下载并更新」→「新版已就绪」→「重启并更新」。安装前检查 bundle ID、版本递增、签名团队、codesign、Gatekeeper 和公证票;在旧 App 旁边准备新包,等旧进程退出后再替换。替换中途失败,应把旧包放回去。听懂可能正在同传、录音或助答,所以会话进行中不允许点重启更新。
这一步的边界我也要说清:当时 652 项自动测试通过,替换脚本在临时路径里测过成功和失败回滚,真实的已发布 DMG 通过签名、公证和身份检查;完整从旧版界面点击一次、退出并以新版重开的现场链路还没有跑通验收。软件发布的自动化程度,不该靠按钮文案来判断。下一次真正的更新测试,要看旧进程是否结束、磁盘上的包是否换了、重开的进程是否确实是更高 build。
如果你也要把 GitHub Actions 接到桌面软件,我会按这个顺序做:先把本地打包变成一条可重复命令;再让 PR 检查它、主线合并触发构建;之后加签名、公证、Release 和校验和;最后才写应用内的检查、下载、验证和替换。把这些阶段拆开,任何一处红了都知道是哪一道门没过,用户也不必靠猜测决定是否安装。