个人 Mac 能正常发布,远程节点却提示找不到私钥、钥匙串无法解锁,通常不是 Xcode 本身的问题,而是签名身份没有完整迁移。

最快的处理方式是:新项目直接用 fastlane match 统一管理;已有生产签名先导入现有证书与私钥,再保留旧链路做双轨验证;CI 默认使用临时钥匙串和 readonly,同步签名资产必须早于构建与归档。不要把 match nuke 当成默认迁移步骤。

这篇文章适合以下读者:

  • 维护 iOS 或 macOS 自动签名流水线、希望消除个人 Mac 依赖的 DevOps 工程师;
  • 负责证书、私钥、描述文件和 App Store 发布权限的签名管理员;
  • 准备用远程 Mac 作为长期构建、测试或发布节点的研发负责人。

先确认签名迁移的边界

CI 代码签名不是“把几个文件下载到远程 Mac”这么简单。一个可用的签名身份至少要同时包含证书和对应私钥;只有证书、没有私钥时,节点不能完成有效签名。Apple 的代码签名资料也明确说明,证书与私钥必须作为完整身份使用,私钥丢失后不能靠重新下载证书恢复。参考 Apple 关于代码签名身份同步的说明

远程迁移前,至少要盘点以下对象:

  • 证书:开发、Ad Hoc、App Store 或 macOS 分发所使用的类型;
  • 私钥:对应证书是否仍存在于原发布 Mac 的 macOS Keychain 中;
  • 描述文件:是否匹配正确的 Bundle Identifier、Target、设备和能力;
  • 项目目标:主应用、Extension、Widget、Watch App 是否各自拥有正确配置;
  • 发布权限:签名仓库、Apple Developer 账户、App Store Connect 和发布令牌是否由不同责任人管理。

iOS 或 iPadOS 的不同分发方式会对应不同类型的描述文件;macOS 应用还可能涉及 Mac App Store、Developer ID 或开发用途。不要因为主应用归档成功,就认定所有附属 Target 都已经完成签名。查看官方分发与描述文件流程

存量证书可以在不撤销生产身份的情况下纳入 match 吗?

可以,但前提是现有证书、对应私钥和描述文件都能取得,并且迁移过程不主动执行撤销或重置操作。官方文档提供了 fastlane match import,用于导入 .cer.p12 以及 .mobileprovision.provisionprofile 文件;因此,存量项目优先走导入和验证,而不是先清空线上资产。查看 match 的导入说明

第一步:新项目先建立唯一签名源

新项目没有历史发布链路,也没有需要保护的生产证书时,可以直接初始化 match。签名仓库可以使用私有 Git 存储或对象存储,文件由 match 加密保存;仓库访问凭据和解密口令应分别管理,不要把它们写入项目仓库或同一个 CI 变量中。

一个只展示占位符的配置可以这样组织:

git_url("https://<SIGNING_STORAGE>/<REPOSITORY>.git")
git_branch("<TEAM_BRANCH>")
app_identifier([
  "<BUNDLE_ID_MAIN>",
  "<BUNDLE_ID_EXTENSION>"
])
username("<APPLE_DEVELOPER_ACCOUNT>")

首次初始化只负责建立配置,并不等于签名资产已经正确生成。完成初始化后,还要按发布用途分别创建并同步:

bundle exec fastlane match development
bundle exec fastlane match adhoc
bundle exec fastlane match appstore

具体命令参数应以当前 fastlane 文档和项目版本为准,不要直接复制没有经过验证的旧脚本。新项目的基线不是“证书文件下载成功”,而是远程 Mac 能够完成最小构建、归档,并检查归档内的签名与描述文件。

建议将以下信息分开登记:

  • <TEAM_ID>:Apple 团队标识;
  • <BUNDLE_ID_MAIN>:主应用标识;
  • <BUNDLE_ID_EXTENSION>:扩展或 Widget 标识;
  • <SIGNING_REPOSITORY>:签名存储地址;
  • <MATCH_PASSWORD>:签名仓库解密口令;
  • <PUBLISH_TOKEN>:发布服务令牌。

任何真实账户、仓库地址、口令、令牌和证书名称都不应写入示例配置,更不能直接提交到代码仓库。

第二步:存量项目先导入,再做双轨迁移

已有项目最危险的动作,是为了让新节点“快速成功”而立即执行 match nuke、撤销证书或删除原有钥匙串。生产证书可能仍被旧 Mac、发布服务器或其他团队成员使用,误撤销后,问题不会只停留在当前构建任务。

推荐顺序如下:

盘点旧发布链路

在原发布 Mac 上确认:

  • 证书是否显示为“证书加私钥”的完整身份;
  • .p12 导出是否成功,并且拥有独立保护口令;
  • 描述文件是否覆盖当前 App ID 和全部必需能力;
  • 最近一次成功发布使用了哪一个 Target、Scheme 和导出方式;
  • 旧流水线是否依赖登录钥匙串、固定用户会话或图形化确认。

Apple 官方资料指出,证书存在但缺少对应私钥时,无法进行代码签名;因此只复制证书文件并不能完成迁移。查看私钥恢复与身份导入说明

使用隔离仓库试运行

先创建隔离的 match 存储位置,或使用独立分支,将现有 .cer.p12 和描述文件导入:

bundle exec fastlane match import \
  --git_url "<ISOLATED_SIGNING_REPOSITORY>" \
  --git_branch "<MIGRATION_BRANCH>"

如果没有 Developer Portal 查询权限,但证书、私钥和描述文件来源可靠,才考虑使用跳过匹配校验的参数。该选项不能证明资产仍然适用于当前团队,也不能替代归档签名验证。

⚠️ match import 解决的是“把已有资产纳入统一存储”,不是“证明所有 Target 和发布渠道都正确”。导入后必须在全新的远程 Mac 上重新同步并构建。

用同一提交做新旧对照

旧 Mac 和远程 Mac 应使用同一 Git 提交、同一 Scheme、同一导出方式,比较以下结果:

  • 归档是否成功;
  • 主应用与附属 Target 是否都签名;
  • 导出的 IPA 或 macOS 包是否包含预期描述文件;
  • 测试设备、测试分发渠道或 App Store Connect 是否接受;
  • 失败时能否立即切回旧发布节点。

迁移验收通过后,再决定是否把正式发布任务切换到远程节点。一次构建成功只能说明当前组合可用,不能证明证书轮换、节点重启和后续发布仍然可恢复。

第三步:多应用、多 Target 与多团队分别隔离

同一个 Apple 团队下,多个应用可以复用签名证书,但描述文件通常仍然要按 Bundle Identifier 分别维护。fastlane match 支持在同一存储中为多个 Bundle Identifier 同步资产,也支持在同一团队内复用签名身份。查看 match 的多 Target 说明

可以采用以下占位结构:

team_id("<TEAM_ID_A>")
git_branch("<TEAM_A_BRANCH>")
app_identifier([
  "<APP_A_BUNDLE_ID>",
  "<APP_A_WIDGET_BUNDLE_ID>",
  "<APP_A_WATCH_BUNDLE_ID>"
])

不同 Apple 团队不应只靠变量名区分。更稳妥的做法是使用独立分支,或者直接使用独立存储空间,并在 Appfile 或 CI 环境变量中明确团队标识。fastlane 的 Appfile 支持配置 Bundle Identifier、Apple 账户和团队选择,可用于减少多团队任务误选团队的风险。查看 Appfile 团队配置说明

多个应用与多个 Apple 团队需要怎样划分签名仓库?

同一团队的多个应用,可以在同一分支中复用证书、分别维护描述文件;不同团队则应至少使用不同分支,并将团队标识、账户和 Bundle Identifier 一起固定在任务配置中。若团队权限、发布责任或合规边界完全不同,独立存储空间比共享仓库更容易审计和回滚。

验收时不能只运行主应用。应为每个附属 Target 至少准备一次构建或归档任务,特别检查:

  • Extension 的签名标识是否与主应用匹配;
  • Widget 是否获得对应的描述文件;
  • Watch App 与 Watch Extension 是否分别同步;
  • macOS 辅助目标是否使用了正确的分发类型;
  • 多团队任务是否不会读取另一个团队的证书分支。

第四步:让远程 Mac CI 进入无人值守模式

远程 Mac 的 CI 节点与个人开发 Mac 最大的不同,是它不能依赖当前用户已经打开的会话、解锁的登录钥匙串或人工点击确认。setup_ci 的作用正是为 CI 创建临时钥匙串、让 match 默认进入 readonly,并设置便于收集的日志和测试结果路径。查看 setup_ci 官方文档

Fastfile 可以按以下思路组织:

before_all do
  setup_ci
end

lane :build_app do
  match(
    type: "appstore",
    app_identifier: ["<BUNDLE_ID_MAIN>", "<BUNDLE_ID_EXTENSION>"],
    readonly: true
  )

  build_app(
    scheme: "<SCHEME>",
    export_method: "app-store"
  )
end

这里的顺序很重要:先创建临时钥匙串并同步签名,再执行构建和归档。CI 任务不应在构建中途才发现私钥没有导入,也不应让普通构建任务临时创建新的生产证书。

临时钥匙串对远程 Mac CI 的作用是什么?

临时钥匙串可以把本次 Job 的证书和私钥与长期登录环境分开,减少不同任务之间残留凭据、用户会话变化和并发任务互相影响的风险。setup_ci 还会调整钥匙串相关设置,使非交互任务不必等待图形化确认;它不是权限替代品,但能降低 CI 因钥匙串状态不同而失败的概率。

需要单独管理的凭据至少包括:

  • 签名仓库读取凭据;
  • match 解密口令;
  • Apple Developer 或 App Store Connect 认证;
  • 发布平台令牌;
  • 远程 Mac 登录和节点管理凭据。

readonly 只适合读取已存在的签名资产,不会替代证书续期或描述文件更新流程。写入、创建和续期应放到受控管理任务中,并要求明确审批、备份和回滚入口。

哪些 CI 任务应固定使用 readonly 模式?

凡是普通 CI 构建、测试、归档、Pull Request 校验和重复发布任务,都应优先开启 readonly。只有签名管理员主动执行的初始化、导入、证书轮换或描述文件更新任务,才允许进入写入流程;正式流水线不应因为缺少资产而自动创建或撤销生产身份。

第五步:验证共享节点不会串用凭据

团队共享远程 Mac 时,应将开发构建、测试归档和正式发布分成不同账户、工作区、任务变量与钥匙串策略。普通任务不应拥有生产签名仓库的写入权限,也不应读取正式发布所需的全部令牌。

长期节点尤其要测试以下情况:

  • 节点重启后,临时钥匙串是否会被错误保留或完全丢失;
  • CI 用户变化后,签名同步是否仍然指向预期钥匙串;
  • 两个 Job 连续运行时,后一个任务是否继承前一个任务的证书;
  • 并发任务使用不同团队或不同分支时,是否出现描述文件串用;
  • 任务失败后,日志是否泄露口令、私钥路径或仓库认证信息。

建议准备两个隔离任务:一个只读同步开发签名,另一个只读同步发布签名。先连续执行,再在资源允许时并行执行,最后清理工作区并重新运行。验收重点不是任务都成功,而是任务之间不会读取错误身份、残留凭据或对方的描述文件。

✅ 如果远程 Mac 需要长期作为构建节点,重启恢复、账户隔离、SSH 管理和完整 root 权限都应在正式迁移前验证。RUVCLOUD 的远程 Mac 节点方案更适合先搭建隔离测试节点,再决定是否承载正式发布。

第六步:把轮换、故障和回退写成流程

签名资产会因为证书到期、描述文件能力变化、Bundle Identifier 调整、团队成员变更或远程节点替换而失效。Apple 的资料显示,描述文件可能因为证书、设备或 App ID 能力变化而需要重新生成;因此“仓库里有文件”不等于“当前项目仍然可以签名”。查看描述文件编辑与重新生成说明

生产流程至少要定义以下责任:

  1. 谁负责发现证书或描述文件即将失效;
  2. 谁拥有写入 match 存储和 Apple Developer Portal 的权限;
  3. 轮换前如何导出并保存当前可用身份;
  4. 新资产如何在隔离分支验证;
  5. 新旧资产如何在同一提交上比较;
  6. 失败时如何恢复旧节点、旧分支和旧发布凭据。

macOS 应用若涉及分发签名,还要检查应用包中的嵌入描述文件与受限权限。Apple 官方文档说明,某些受限 entitlement 需要由匹配的分发描述文件授权,主应用与附属代码项也可能需要分别处理。查看 macOS 分发签名说明

迁移前勾选清单

  • [ ] 已确认每个证书都有对应私钥;
  • [ ] 已导出并保护现有 .p12
  • [ ] 已备份当前描述文件和旧发布配置;
  • [ ] 已记录主应用、Extension、Widget、Watch App 的 Bundle Identifier;
  • [ ] 已为不同团队准备独立分支或存储空间;
  • [ ] 已在非生产分支完成 match import
  • [ ] 已在远程 Mac 上使用临时钥匙串同步;
  • [ ] 普通 CI 已开启 readonly
  • [ ] 已用同一提交对比旧 Mac 与远程 Mac 的归档结果;
  • [ ] 已验证设备或分发渠道可以接受新归档;
  • [ ] 已测试节点重启、任务失败和旧链路回退;
  • [ ] 已确认日志不会暴露口令、令牌或私钥信息。

用三种结论决定是否切换生产

验收结果 建议结论 适合的后续动作
新项目无历史签名,远程 Mac 能同步、归档并完成目标渠道验证 直接迁移 由受控流程创建正式签名资产,CI 持续使用 readonly
存量签名已导入,但旧链路与远程节点尚未完成完整对照 继续双轨 保留旧发布 Mac,先让远程 Mac 承担测试归档和非关键发布
缺少私钥、Target 未覆盖、节点重启后钥匙串异常或无法回退 暂缓上线 不执行 nuke 或撤销操作,先补齐备份、恢复和权限边界

如果当前方案仍依赖个人 Mac,常见缺点是签名私钥只留在个人登录钥匙串、节点无法在无人值守状态下恢复、构建任务与发布任务共享同一高权限环境,而且人员离职或设备故障会直接中断发布。把这些任务搬到普通 Linux 云主机也不能解决 Xcode、macOS Keychain 和 Apple 专属工具链的问题。

对于需要临时迁移、测试新签名流程或运行长期 CI 的团队,更稳妥的路径是先租用一台具备完整管理权限、可重启验证并能隔离任务的真实远程 Mac。可以先在 RUVCLOUD 的 Mac 租赁方案上使用非生产证书完成 match、归档和恢复演练,确认权限与回滚流程后,再决定是否承载正式发布任务;长期稳定重负载或必须连接特定物理设备的团队,则仍应评估自购 Mac 或专用本地节点。