Safari 云签名在 GitHub Actions 中的两个陷阱
本文最后更新于:2026年8月5日 下午
背景
在 使用 GitHub Actions 全自动发布 Safari 扩展 里,我写过一套在 macOS runner 上用 Xcode 云签名(-allowProvisioningUpdates)自动构建、签名、上传 Safari 扩展的流程。流程本身是能跑起来的,但在跑通之前,我踩过两个坑,而且巧的是,这两个坑都有同一个特点:报错信息含糊不清,搜索引擎上几乎搜不到答案。这篇文章把它们记录下来,包括现象、原因和修复方法。
两个坑追根溯源是同一件事:无状态的 CI runner,打破了苹果签名工具原本假设的前提。
陷阱一:「maximum number of certificates」
场景是这样的:GitHub 提供的 macos-* runner,开着 -allowProvisioningUpdates 让云签名自动处理证书。一开始一切正常,版本一个接一个发出去。然后某一天,你什么都没改,构建却突然报错,说证书数量达到了上限。
真正发生的事情是这样的:GitHub 的每个 runner 启动时都是一个全新的、空的 keychain。云签名找不到可用的签名身份时,不会直接失败,而是很「贴心」地帮你的账号新建一张 Apple Distribution 证书,把私钥放进这次 runner 的 keychain 里。job 结束后 runner 被销毁,私钥跟着一起消失。证书本身还留在你的 Apple Developer 账号里,永久躺在那儿,再也没有对应的私钥能用它签名。
也就是说,每次发布都在悄悄烧掉一张证书。苹果对每个账号的 Distribution 证书数量设了上限,一旦顶到这个上限,云签名就没法再新建证书,构建从此开始失败。最阴险的地方在于:撞上限之前,每一次构建都是成功的,你完全看不出哪里不对,直到它毫无征兆地、永久性地坏掉。

这是攒了一段时间之后的样子——十几张证书,全部叫「Created via API」,全部挤在大约一周之内。它们无一例外都是孤儿证书:创建它们的 runner 早就没了,私钥自然也不存在了。
解决办法就是不要再让 CI 自己新建证书:
- 自己手动创建一张 Apple Distribution 证书(Xcode → Settings → Accounts → Manage Certificates,或者去 开发者后台)
- 导出为带密码的
.p12 - 把证书和密码都存进仓库的 secrets(
.p12需要先 base64) - 在 CI job 一开始、
xcodebuild跑之前,把它导入 keychain
1 | |
keychain 里已经有一个可用的签名身份之后,云签名每次都会复用它,而不是每次都新建一个。
注:修好这个问题之后,记得去开发者后台的证书列表,把 CI 之前留下的那一堆孤儿证书清理掉——找不到对应私钥的那些就是。
陷阱二:「Cloud signing permission error」
传给 xcodebuild 的 App Store Connect API Key 是有角色权限的,创建的时候就定死了。坑就在这里:一个 Developer 或 App Manager 角色的 key,身份认证是能通过的,然后构建会在签名进行到一半时失败,只甩给你一句语焉不详的「Cloud signing permission error」。
正是这种失败方式让人摸不着头脑:key 本身没有被拒绝,上传之类的其他 App Store Connect API 操作用这个权限也都正常,唯独云签名这一步会死,报错里连「权限」两个字都没提。

按我的实测,云签名要求 key 必须是 Admin 角色。角色在创建时就固定了、后续改不了,唯一的办法是重新生成一个:App Store Connect → Users and Access → Integrations → App Store Connect API → 生成一个 Admin 角色的 key,下载 .p8(只有这一次下载机会)。
注:Admin 角色的
.p8要当成整套流程里真正的机密来对待,它是这里唯一的真机密。issuer id、key id、team id 这些都不是机密,.p8和.p12才是。
总结
两个坑追根溯源都是同一件事:苹果的签名工具链假设你在用一台长期存在的开发机,而无状态的 runner 打破了这个假设,失败得又晚又含糊,而不是又早又清楚。
写上一篇文章之后,我把这整套流程——生成 Xcode 工程、用上面这两个修复方式签名、上传、提交审核——都收进了 extport,也就是我现在给所有扩展发版用的工具。如果你不想自己维护这堆 YAML,它就是这篇和上一篇里讲的这一整套东西的打包版本。