使用 GitHub Actions 全自动发布 Safari 扩展
本文最后更新于:2026年7月20日 上午
背景
我之前写过两篇关于 Safari 扩展的博客:转换 Chrome Extension 为 Safari 版本,以及 发布 Safari 扩展到 iOS 应用商店。它们解决的是”第一次怎么做成”。但随着我的 Safari 扩展越来越多,每次发版的手动流程变得越来越难以忍受:打开 Xcode、改版本号、Archive、Validate、Distribute,再打开 App Store Connect 选 bundle、填 release note、点提交——每个扩展十分钟起步,而且不能出错。
这篇文章介绍我现在的发布流程:改一下 package.json 里的 version,push 到 GitHub,然后就没有然后了。构建、签名、上传、提交审核,全部在 GitHub Actions 里自动完成。

最终效果
先看终态。这是一个真实项目里 Safari 发布相关的完整 workflow 配置:
1 | |
配好这份 YAML 和它引用的 secrets,就是全部了。它用到了我为此写的两个工具:
- wxt-module-safari-xcode:WXT 模块,在构建时自动把扩展转换为配置好的 Xcode 项目
- safari-webext-publish-action:GitHub Action,负责签名、上传和提交审核
为什么 Safari 需要专门的工具?Chrome/Firefox/Edge 的发布 WXT 已经做得很好了,一条命令的事。但 Safari 扩展本质上是一个 App——浏览器扩展的代码被嵌在一个 macOS/iOS 应用里,发布走的是 App Store,这意味着要面对 Xcode 项目、证书、签名、公证、provisioning profile 这一整套 Apple 的分发体系。复杂度的直观展示——这是一个全平台扩展需要的所有 secrets:

看起来吓人,但每一个都只需要配置一次。下面按顺序讲清楚每一步在做什么、每个 secret 从哪里来。
顺带一提:这套流程也是逐步演化出来的。最早我只用 wxt-module-safari-xcode 生成项目,仍然在 Xcode 里手动构建上传;后来 action 的 build 模式让我不再需要打开 Xcode;最后加上 submit 模式,才把最后”打开 App Store Connect 点两三分钟”的步骤也消灭掉。如果你想了解完整的手动流程(以及为什么它值得被自动化),见开头那两篇博客。
第一步:配置 WXT,把扩展转换为 Xcode 项目
Apple 官方提供了转换工具 xcrun safari-web-extension-converter,但它只做最基础的转换:生成的 Xcode 项目还需要手动设置版本号、App 分类、开发团队等。wxt-module-safari-xcode 把这些集成进了 WXT 的构建流程——运行 wxt build -b safari 时自动完成转换和全部配置注入。
1 | |
四个配置项的来源:
projectName:扩展的显示名,包含大写字母或空格都没问题。appCategory:App Store 的应用分类,可选值见 Apple 官方分类列表。bundleIdentifier:全局唯一标识,惯例是反转域名 + 应用名,全小写、不含空格。developmentTeam:你的 Apple Developer Team ID,在 developer.apple.com/account 的 Membership 页面可以找到。

运行 pnpm wxt build -b safari 后,Xcode 项目会出现在 .output/<projectName>,版本号已经从 package.json 自动同步(模块会同时更新 MARKETING_VERSION 和 CFBundleVersion,后者按 major×10000 + minor×100 + patch 换算成递增的数字版本——Apple 要求每次上传的 build number 必须递增,这个换算规则让它跟着语义化版本自动满足)。
真实项目的配置示例:https://github.com/rxliuli/imp-translate/blob/main/wxt.config.ts
第二步:准备 secrets
这是整个流程中最繁琐的部分,但每项只做一次。分三组。
证书(APPLE_CERTIFICATE_BASE64 / APPLE_CERTIFICATE_PASSWORD)
CI 里签名的原理是:把你的分发证书(含私钥)导出成 .p12 文件,以 base64 形式存进 GitHub secrets,action 在运行时把它导入 runner 上的临时 keychain 供 Xcode 签名使用。步骤:
- 在 developer.apple.com 的 Certificates 页面 创建三张分发证书:Mac App Distribution(对应 “3rd Party Mac Developer Application” 签名身份)、Mac Installer Distribution(对应 “3rd Party Mac Developer Installer”。看到”Installer”别疑惑——Mac App Store 规定的上传格式就是签过名的 .pkg,它只是提交给 Apple 时的运输容器,用户实际安装走的是 App Store 自己的机制,永远不会接触这个 .pkg。action 内部用
productbuild打包时需要这张证书签名),以及 Apple Distribution——注意一个命名陷阱:创建页面上它叫 “Apple Distribution”,但创建后在证书列表里只显示为 “Distribution“(Platform 列为 “All”),是同一张证书。它是跨平台的,负责 iOS 分发签名。创建过程中需要用本机钥匙串生成 CSR(证书签名请求),按页面提示操作即可。这三张证书都是账户级的,不绑定任何具体扩展——配置一次,所有项目通用。 - 下载证书后双击导入本机钥匙串,然后打开「钥匙串访问」→「我的证书」,同时选中这三张证书(确认每张都能展开看到私钥),右键导出为单个 .p12 文件——action 支持一个 .p12 内包含全部签名身份,不需要分开传。导出时设置的密码就是
APPLE_CERTIFICATE_PASSWORD。 - 把 .p12 转成 base64 存入 secrets:
base64 -i Certificates.p12 | pbcopy,粘贴为APPLE_CERTIFICATE_BASE64。

Provisioning Profiles(4 个 *_PROVISIONING_PROFILE_BASE64)
在 Profiles 页面 为每个 target 创建 App Store 类型的 profile。转换器生成的项目里有 App 和 Extension 两个 target,各自有独立的 bundle id——Extension 的 id 是主 id 加 .Extension 后缀(例如 com.example.my-extension.Extension),两个 bundle id 都需要先在 Identifiers 页面注册。macOS 和 iOS 各一套,所以一共 4 个 profile。每个下载后同样 base64 编码存入对应的 secret。

注意:这一步是按扩展算的——每新增一个 Safari 扩展,都要为它创建一套新的 profile。这也是所有 secrets 里唯一不能跨项目复用的部分(证书和 API key 都是账户级的,配一次全部扩展通用)。
App Store Connect API(APPLE_API_KEY / APPLE_API_KEY_ID / APPLE_API_ISSUER)
在 App Store Connect 的 Integrations 页面 创建团队 API 密钥,角色选 App Manager 就足够(最小权限原则,不要用 Admin)。创建后:
- 下载 .p8 私钥文件——只能下载这一次,妥善保存。用
base64 -i AuthKey_XXXXXXXXXX.p8 | pbcopy转成 base64 后存入APPLE_API_KEY - 页面上的 Key ID 存入
APPLE_API_KEY_ID - 页面顶部的 Issuer ID 存入
APPLE_API_ISSUER
这组密钥承担两个职责:build 模式用它上传 bundle(替代了 Xcode 的 Distribute 步骤),submit 模式用它调用 App Store Connect API 创建版本并提交审核。

找不到这些页面入口是正常的,Apple 的后台并不以易用著称。一个现代解法:让能操作浏览器的 LLM 帮你导航到对应页面,比自己翻文档快得多。
第三步:理解 workflow 的两个 job
回头看开头那份 YAML,Safari 发布被拆成了两个 job,这个拆分有讲究:
safari(build 模式) 跑在 macOS runner 上——只有这一步真正需要 macOS 和 Xcode(注意必须是 macos-26 及以上,action 会检查 Xcode 26 SDK)。它做四件事:导入证书到临时 keychain、用 xcodebuild 构建并签名(macOS 产出 x86_64+arm64 通用二进制)、打包、通过 API 上传到 App Store Connect。跑完之后,bundle 已经出现在 App Store Connect 的构建版本列表里,等价于你在 Xcode 里点完了 Archive → Distribute。一个省心的细节:action 会自动从 Xcode 项目的 scheme 检测目标平台——项目里只有 macOS scheme 就自动跳过 iOS 步骤,反之亦然,不需要额外配置。
safari-submit(submit 模式) 跑在 ubuntu runner 上——它只调用 App Store Connect API,不需要 macOS,用便宜的 runner 就好。它等价于你在网页上手动做的那套:轮询等待 bundle 处理完成、创建新版本、关联刚上传的 bundle、填写 What’s New、提交审核。也因此它只需要 API 三件套这 3 个 secrets。开头抱怨过的”每次都要手动填 release note”也在这里被解决了:通过 release-notes 输入传入,不传则默认 “Bug fixes and improvements.”。

总结
诚实地说:如果你只有一两个 Safari 扩展,Xcode 的手动流程是可以忍受的,它替你隐藏了证书和签名的大部分复杂性,为它配置这一整套 secrets 未必划算。但如果你有三个以上的 Safari 扩展,或者单个扩展发版频繁,这条流水线会彻底改变你的发版体验——一次性的配置成本,换来之后每次发版为零的心智负担。我的十几个 Safari 扩展现在全部走这条流水线,配置完成之后,我再也没有为发版打开过 Xcode。