惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

WordPress大学
WordPress大学
A
About on SuperTechFans
量子位
B
Blog RSS Feed
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
博客园_首页
MongoDB | Blog
MongoDB | Blog
小众软件
小众软件
Blog — PlanetScale
Blog — PlanetScale
Microsoft Azure Blog
Microsoft Azure Blog
V
V2EX
Google DeepMind News
Google DeepMind News
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
H
Hackread – Cybersecurity News, Data Breaches, AI and More
G
Google Developers Blog
U
Unit 42
D
DataBreaches.Net
博客园 - Franky
D
Docker
宝玉的分享
宝玉的分享
Y
Y Combinator Blog
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Hugging Face - Blog
Hugging Face - Blog

rxliuli blog

App Store Connect 发布事故回顾 Safari 扩展 Popup 中的滚动迟缓问题 使用 GitHub Actions 全自动发布 Safari 扩展 旅行 - 东福山岛 旅行 - 新加坡 - 越南 旅行 - 马来西亚 - 下 旅行 - 马来西亚 - 上 Browser Extension Dev - 08. 发布 Chrome Web Store Browser Extension Dev - 07. Popup UI Browser Extension Dev - 06. 按需注入脚本 Browser Extension Dev - 05. 存储和配置 Browser Extension Dev Extra - User Script 介绍 Browser Extension Dev - 04. Background Script Browser Extension Dev Extra - 如何找到锁定滚动的元素 Browser Extension Dev - 03. 注入 UI Browser Extension Dev - 02. 使用 WXT Browser Extension Dev - 01. 介绍基本概念 Chrome => Firefox 扩展移植的那些坑 发布 Safari 扩展到 iOS 应用商店 再游新疆 -- 自驾 实践: 使用 Hono 开发全栈应用 Web 流式写入文件 在构建时而非运行时编译 Markdown 在 Web 中解压大型 ZIP 并保持目录结构
Safari 云签名在 GitHub Actions 中的两个陷阱
rxliuli · 2026-08-05 · via rxliuli blog

背景

使用 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 证书数量设了上限,一旦顶到这个上限,云签名就没法再新建证书,构建从此开始失败。最阴险的地方在于:撞上限之前,每一次构建都是成功的,你完全看不出哪里不对,直到它毫无征兆地、永久性地坏掉。

Snipaste\_2026-07-28\_08-34-22.png

这是攒了一段时间之后的样子——十几张证书,全部叫「Created via API」,全部挤在大约一周之内。它们无一例外都是孤儿证书:创建它们的 runner 早就没了,私钥自然也不存在了。

解决办法就是不要再让 CI 自己新建证书:

  1. 自己手动创建一张 Apple Distribution 证书(Xcode → Settings → Accounts → Manage Certificates,或者去 开发者后台
  2. 导出为带密码的 .p12
  3. 把证书和密码都存进仓库的 secrets(.p12 需要先 base64)
  4. 在 CI job 一开始、xcodebuild 跑之前,把它导入 keychain
1
2
3
4
5
6
7
8
9
10
11
12
- name: Import signing certificate
run: |
echo "$APPLE_CERTIFICATE_BASE64" | base64 --decode > certificate.p12
security create-keychain -p actions build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p actions build.keychain
security import certificate.p12 -k build.keychain \
-P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k actions build.keychain
env:
APPLE_CERTIFICATE_BASE64: ${{ secrets.APPLE_CERTIFICATE_BASE64 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}

keychain 里已经有一个可用的签名身份之后,云签名每次都会复用它,而不是每次都新建一个。

注:修好这个问题之后,记得去开发者后台的证书列表,把 CI 之前留下的那一堆孤儿证书清理掉——找不到对应私钥的那些就是。

陷阱二:「Cloud signing permission error」

传给 xcodebuild 的 App Store Connect API Key 是有角色权限的,创建的时候就定死了。坑就在这里:一个 DeveloperApp Manager 角色的 key,身份认证是能通过的,然后构建会在签名进行到一半时失败,只甩给你一句语焉不详的「Cloud signing permission error」。

正是这种失败方式让人摸不着头脑:key 本身没有被拒绝,上传之类的其他 App Store Connect API 操作用这个权限也都正常,唯独云签名这一步会死,报错里连「权限」两个字都没提。

1785936111555.jpg

按我的实测,云签名要求 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,它就是这篇和上一篇里讲的这一整套东西的打包版本。