Steven
Steven2 分钟阅读

把发布放进 CI,两次

GeekBye v1.8.4 把 macOS 和 Windows 的发布构建都搬进了 CI。changelog 没说的是:这事我们早先试过一次,一个月后又因为 runner 花销把它删了,直到第二次才让它扎下根——因为到那时候,发布脚本已经能靠手跑通了。

工程
CI/CD
桌面端
GeekBye 发布
把发布放进 CI,两次

v1.8.4 的 changelog 里有一行听上去像纯粹的家务活:“发布构建现在 macOS 和 Windows 都来自 CI。”读起来像那种一次干净利落地发生、之后再也不会去想的事。它不是。团队早就把发布放进过一次 CI——又把它拔了出来。这个版本是第二次尝试,而它扎下根的原因才是真正的工程教训,所以让我们按顺序讲。

第一次尝试,以及它为什么死了

回到 2025 年 10 月,那里有一个 release.yml。它由 tag 推送触发——推一个 v*.*.* 的 tag,一个 macOS runner 就会转起来,构建,发布。它自己手动做签名的准备:一个步骤解码签名证书,创建并解锁一个临时 keychain,把证书导入进去好让 codesign 找得到它,末尾还有一个对应的清理步骤。它跑得通。它也只跑 macOS——Windows 发布压根不在 CI 里。

11 月 4 日,它被删了。那条 commit 消息对原因异常坦白:"remove GitHub Actions release workflow to conserve minutes / macOS runners cost 10x multiplier (100 billed minutes per 10-minute release). Releases will be done locally." 这就是第一次尝试整个故事的一句话版本。GitHub 按 Linux 十倍的费率计 macOS 的 runner 时间,而一个 tag 触发的发布意味着每一个 tag——包括那些用完就扔的、重打的、“糟糕,忘了 bump 版本”的——都悄悄花掉一百分钟的计费。你控制不了的自动化,就是那种趁你睡觉时花钱的自动化。于是接下来的三个半月,发布退回到了某个开发者的 Mac 上。

难题真正被解决的地方

这里就是让第二次尝试奏效的那部分,而它完全发生在 CI 之外。在那三个半月的本地发布里,scripts/release.js——那个开发者手动运行的普通 Node 脚本——把这个应用有过的每一个打包问题都吸收了进去,一次修一个:

  • white-label 还原。 这是一个从同一份源码里同时构建 GeekBye 和 Pavleur 的 white-label 代码库,也就是说一次发布会为目标产品换掉 package.json 字段和图标资源。两个修复教会了脚本事后把一切放回原位,于是构建 Pavleur 不会让你的 git 树带着 Pavleur 的身份残留脏东西。
  • 双架构 Windows。 一个修复,改成同时构建 x64 和 arm64 两个 Windows 安装包,而不是一个。
  • 未签名 Windows 的绕法。 更新的 electron-builder 无视了那个本该关掉 Windows 签名的配置标志,于是脚本学会了改用在环境里设 CSC_IDENTITY_AUTO_DISCOVERY=false 来强制一次未签名构建——这是那种你只有撞上它才会发现的褶皱。
  • 公证自动检测。 脚本学会了看自己的环境然后做决定:有签名凭据吗?签。也有 Apple 公证凭据吗?公证。都没有?构建成未签名的。没有要记的标志;那些 secret 的存在本身_就是_配置。

这些没有哪一样光鲜。它们全都是那种事——如果它在一个 CI runner 里给你惊吓,每次尝试都要花你一个小时的推-等-败-读日志,而且是 10 倍计费。在一台你正坐在它面前的 Mac 上解决,每一个只花一分钟。

第二次尝试:六十六行

当 CI 在 v1.8.4 里回来时,那个工作流是 66 行,而它的决定性特质在于它做得有多少。commit 平实地描述了它:"Manual workflow_dispatch trigger that builds both platforms in parallel, reusing existing release.js script. macOS builds are signed and notarized, Windows builds are unsigned." 里面每一个设计决定都是第一次尝试留下的伤疤:

  • workflow_dispatch,而非 tag 触发。 你靠点“Run workflow”来开始一次发布。一个人给每一分钟付费的 macOS 时间把关。杀死第一版的那个成本问题,靠着干脆不自动化那个触发就解决了——这是自动化曾经在积极地帮倒忙的唯一一处。
  • 一个 product 输入。 dispatch 接一个下拉框——geekbyepavleur——于是同一个工作流两个品牌都能出。white-label 那道接缝一路延伸到了发布按钮上。
  • 两个并行 job。 macos-latest 上的 build-macwindows-latest 上的 build-win,同时在跑。CI 里的 Windows 在这里是真正新的东西;10 月那个工作流从没构建过它。
  • YAML 里没有签名逻辑。 这就是整个要点。没有 keychain 杂耍,没有证书导入步骤,没有清理。Mac job 运行 node scripts/release.js <product> --publish,Windows job 运行同样的东西加上 --no-sign。第一个工作流曾在 YAML 里手动做的一切,如今都住在那个早已能跑的脚本里。工作流是一个编排器,不是一个实现。

两个平台想要相反的东西,而这些 job 诚实地映出了这一点。macOS 在加固运行时下构建,通过 electron-builder 的内建路径(@electron/notarize)签名并公证,凭据由仓库 secret 提供、由脚本自动检测。Windows 为 x64 和 arm64 构建未签名的 NSIS 安装包。一个工作流,两个 job,两种完全不同的“完成”的概念。

证据在于没发生的事

你怎么知道次序是对的:在同一天那两次给这些运行命名、修好一个 token secret 名字的微调之后,release.yml 四个月没再被碰过。没有签名失败热修。没有公证被拒的手忙脚乱。没有“runner 上那个原生二进制不见了”的恐慌。对一条代码签名加公证的流水线——那种最以来回折腾出名的 CI——来说,四个月的沉默几乎是闻所未闻。

它安静,是因为噪音早已在某个更便宜的地方发生过了。那些通常在一个 CI 日志页里、按 10 倍计费、一次一个 force-push 地上演的战斗,在本地那段时期已经在开发者们的 Mac 上打过了。CI 不必成为打包被 debug 的地方,因为打包已经 debug 完了。这就是一句话的论点:只有当你的 Mac 不再给你惊吓之后,才把构建从它身上搬走。

那一处仍可能咬人的耦合

它并非全无软肋,而这一点值得点名,因为它很微妙。macOS 应用依赖的那些 Swift 二进制——OCR、屏幕捕捉、那几个转录器——是提交进 git 的。但发布并不出货那些提交进去的副本;构建会在 runner 上重新编译它们。而一个 check-swift-version.js 关卡会在编译器不是那个锁定版本时把整个发布硬性判败。在这个版本时,那个锁定是 Swift 6.2.x,而工作流里没有任何东西去安装它——那个 job 只是单纯信任 macos-latest 的默认 Swift 会匹配。GitHub 把它的 runner 镜像升过那个锁定版本的那一天,发布就停了,不是因为应用有什么毛病,而是因为流水线悄悄地耦合到了一个它控制不了的机器镜像上。这是这个薄工作流唯一一处没有把它下推进某个自己拥有的东西里的实现细节。

发布的另一半:更小的 payload

这个版本捎带了一个值得一提的无关功能,因为它是一个靠减法做延迟修复的干净例子。Assist Me 这个动作在每次请求时都往后端发了比它需要的更多的上下文。一个 commit 削掉了两样东西:一并发送的转录历史从最近 30 条降到最近 15 条,而用户的档案上下文文件——在_每一次_调用里都注入进 system prompt——被硬性封顶到 4,000 字符并带一个截断标记。要序列化的更少,要上传的更少,模型开始回答前要读的更少,而这正是这个 commit 在优化的那个指标:time-to-first-token。为诚实起见:这个 commit 声称了提速,却没有测它——历史里没有一个前/后的数字,所以把它当作一次论证充分的削减,而不是一次有基准背书的胜利。

这个版本教给我们的三件事

  1. CI 是架在一个早已能跑的脚本之上的一层薄薄的封装。 那 66 行的工作流没有自己的签名逻辑;它调用一个三个月本地发布早已 debug 完的脚本。把机制放进一个你能手动运行的东西里,让 CI 只去决定_何时_运行它。
  2. 自动化那件活,不一定要自动化那个触发。 第一次尝试之所以死,是因为 tag 触发的发布自动花掉了按 10 倍计费的 runner 分钟。手动 workflow_dispatch 保住了自动化,去掉了那个在花钱的部分——有时候,回路里的那个人本身就是功能。
  3. 在迭代便宜的地方 debug。 每一场在开发者 Mac 上解决掉的打包战斗,都是一场永远不会在 CI 日志页里以十倍价格上演的战斗。把东西搬离 Mac,是最后一步,不是第一步。

关于 v1 故事的上一章,见没有安全网就发布三十种语言(v1.8.3);整段弧线则见把软件打磨到完美地发布出去的解剖学