fastlane match 远程 Mac 签名:2026 CI 配置指南

fastlane match 远程 Mac 签名:2026 CI 配置指南

本地归档成功,迁移到远程 Mac 后却出现“找不到签名身份”“Provisioning profile 不匹配”或构建卡在签名阶段。

获胜者:CI 只读同步 + 临时或隔离钥匙串 + 显式 Target 映射。 只有在重启、连续构建、并发隔离、证书轮换和失败恢复都通过后,远程 Mac 签名才算真正可用。

这篇文章适合以下读者:

  • 维护 iOS 自动打包流程、但远程节点频繁签名失败的 DevOps 工程师;
  • 需要让多个 App、Extension 或 Apple Team 共用构建节点的发布工程师;
  • 准备把本地 fastlane 流程迁移到长期在线远程 Mac 的独立开发者。

本地成功与远程失败:问题通常不在编译命令

本地签名能通过,只能证明当前用户会话、当前钥匙串和当前 Xcode 缓存是完整的。它不能证明远程 Mac 在无人登录、重启后或第二个任务启动时仍然拥有相同条件。

一次可复现的 Apple 代码签名,至少由 4 类资产共同决定:

  1. 签名证书:证明构建使用了正确的 Team 和证书类型;
  2. 私钥:证书本身不等于私钥,只有私钥可访问时才能完成签名;
  3. provisioning profile:需要匹配 Bundle ID、证书和所需能力;
  4. Xcode 构建设置:决定每个 Target 实际采用哪一个签名身份和 profile。

官方说明中,provisioning profile 会关联 Bundle ID、签名证书以及设备或分发条件;App Store 分发 profile 还包含单个分发证书。(developer.apple.com)

迁移时最容易漏掉的是私钥和钥匙串访问控制。把 .cer.mobileprovision 文件复制到远程 Mac,并不能替代私钥导入。另一方面,远程 Runner 常常以后台进程运行,无法像交互式桌面会话那样自动解锁登录钥匙串。

建议先把日志分成 4 类,不要把所有输出混在一起:

security find-identity -v -p codesigning
security list-keychains
xcodebuild -showBuildSettings -scheme "Release"
bundle exec fastlane match appstore --readonly --verbose

可安全记录的内容包括证书名称、Team 标识、profile UUID、Bundle ID 和失败阶段。密码、MATCH_PASSWORD、Apple 登录令牌、私钥文件内容以及签名仓库凭据必须使用占位符,不能写入构建日志。

⚠️ security find-identity 能列出证书,并不代表对应私钥一定可用。还需要在实际归档任务中验证签名和导出结果。

单应用发布:让同步、签名和构建各司其职

单应用 App Store 构建适合先建立最小可用链路。远程节点应使用独立构建账户或受控访问凭据,签名仓库只给 CI 读取权限,证书生成和轮换放在发布管理流程中完成。

fastlane match 负责同步签名证书与 profile;setup_ci 负责准备 CI 使用的钥匙串和环境;build_app 负责归档与导出。三者不能互相替代。

fastlane 官方文档明确建议 CI 使用 readonly,避免构建任务自行创建或修改证书和 profile。setup_ci 会创建临时钥匙串,并将 match 切换到只读行为。(docs.fastlane.tools)

一个可控的 Fastfile 结构如下:

platform :ios do
  lane :release do
    setup_ci if ENV["CI"]

    match(
      type: "appstore",
      app_identifier: "com.example.app",
      readonly: true
    )

    build_app(
      scheme: "Release",
      export_method: "app-store"
    )
  end
end

实际项目中的应用标识、scheme 和导出方式必须替换为真实值。不要把密码直接写入 Fastfile,而是通过 CI Secret 注入:

export MATCH_PASSWORD="__INJECTED_SECRET__"
bundle exec fastlane release

验收不能只看 lane 返回成功。至少保留以下证据:

security find-identity -v -p codesigning

xcodebuild -exportArchive \
  -archivePath "build/App.xcarchive" \
  -exportOptionsPlist "ExportOptions.plist" \
  -exportPath "build/export"

codesign -dvvv "build/export/Payload/App.app"

需要核对的结果包括:

  • 有效签名身份数量和名称符合预期;
  • profile 的 Bundle ID 与目标 App 一致;
  • 归档文件存在,导出过程没有重新选择错误 profile;
  • codesign 输出的 Team、Identifier 和 entitlements 符合发布环境;
  • 导出的 IPA 可以进入后续上传或测试步骤。

远程节点上 profile 的存放路径还不能想当然。fastlane 当前文档指出,Xcode 16 后默认路径发生变化:新路径为 ~/Library/Developer/Xcode/UserData/Provisioning Profiles,较早版本使用 ~/Library/MobileDevice/Provisioning Profiles。如果节点安装多个 Xcode,应先完成 Xcode 选择,再执行 match。(docs.fastlane.tools)

多 Target 对比:隐式选择省事,显式映射更适合发布 CI

主 App、Widget、Notification Service 或 Share Extension 通常拥有不同 Bundle ID。它们可能共用证书,但不应默认共用同一个 provisioning profile。

配置方式 适用场景 优点 远程 CI 风险 建议
自动签名 本地开发、频繁变更能力 配置速度快 可能受本机缓存和账号状态影响 不作为发布 CI 唯一依据
单一 Bundle ID 只有主 App 链路简单 扩展加入后需要重新设计 适合最小验证
显式多 Target 映射 App、Widget、Extension 并存 可审计、可复现 配置维护成本更高 发布 CI 首选
多 Team 分支隔离 多个 Apple Team 共用节点 减少资产串用 凭据和钥匙串边界复杂 必须配合隔离策略

fastlane 支持为多个 Bundle ID 同步 profile,也支持在同一签名存储中按 Team 使用不同分支。(docs.fastlane.tools)

推荐在 Fastfile 中直接表达 Target 映射:

lane :sync_signing do
  setup_ci if ENV["CI"]

  match(
    type: "appstore",
    app_identifier: [
      "com.example.app",
      "com.example.app.widget",
      "com.example.app.notification"
    ],
    readonly: true
  )
end

Xcode 工程中则应检查每个 Target 的:

  • PRODUCT_BUNDLE_IDENTIFIER
  • CODE_SIGN_STYLE
  • DEVELOPMENT_TEAM
  • CODE_SIGN_IDENTITY
  • PROVISIONING_PROFILE_SPECIFIER
  • Release 与 Debug 是否误用了同一套配置。

不要只查看主 App 的签名结果。归档后应检查扩展是否真的嵌入:

find "build/App.xcarchive/Products/Applications/App.app" \
  -name "*.appex" -print

codesign -dvvv \
  "build/App.xcarchive/Products/Applications/App.app/PlugIns/AppWidget.appex"

如果主 App 成功、扩展失败,通常说明 profile 映射不完整,而不是远程 Mac 的 CPU 或网络问题。Apple 的文档也明确说明,包含受限 entitlements 的不同 Bundle 需要对应的 profile;macOS 应用和扩展尤其不能把嵌入 profile 当成一个整体处理。(developer.apple.com)

多团队与长期 Runner:钥匙串隔离比“清空全部资产”更稳

同一台远程 Mac 服务多个 App 或 Apple Team 时,最危险的方案是让所有任务共享默认登录钥匙串。旧证书可能被自动选中,某个 Team 的 profile 也可能被另一个 Bundle ID 误用。

更稳妥的边界是:

  • 每个 Team 使用独立的签名仓库分支或存储路径;
  • 每个发布环境使用独立的 CI Secret;
  • 每次任务显式设置钥匙串搜索列表;
  • 构建结束后删除临时钥匙串和导出产物;
  • 禁止普通构建任务执行证书创建、撤销或重置。

setup_ci 当前文档中的默认钥匙串名称是 fastlane_tmp_keychain,默认超时为 3600 秒,也可以通过 keychain_nametimeout 调整。若设置 timeout: 0,则表示不设置超时;长期 Runner 是否采用这一设置,必须结合节点访问控制和清理机制判断。(docs.fastlane.tools)

对于长期在线节点,建议把清理动作放在成功和失败路径都能执行的位置:

set -e

KEYCHAIN_NAME="fastlane_tmp_keychain"
PROFILE_DIR="$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"

bundle exec fastlane release

security delete-keychain "$KEYCHAIN_NAME-db" || true
rm -rf "$PROFILE_DIR"/*.mobileprovision
rm -rf "$PROFILE_DIR"/*.provisionprofile
rm -rf build/export

上面的路径必须根据节点实际 Xcode 版本确认。不要未经核验就删除用户目录下全部钥匙串或所有 profile。破坏性清理可能影响其他 App、开发设备和正在运行的任务。

长期 Runner 至少要做 3 类验收:

  1. 重启后首个任务:验证无人手动登录时,钥匙串仍能创建、解锁并访问私钥;
  2. 连续任务:连续运行两个不同 App,确认第二个任务没有继承第一个任务的 profile;
  3. 失败任务清理:人为制造签名失败,确认临时钥匙串、profile 和导出文件不会残留。

如果使用自托管 Runner,还应将“是否允许并发”作为明确设计项。共享同一钥匙串的并发任务可能互相修改搜索列表、DerivedData 或临时 profile。自托管 Runner 文档强调,任务会被分配给在线且空闲、并匹配标签和组的 Runner;因此发布节点最好使用专用标签,并避免让不兼容的工作流共用节点。(docs.github.com)

FAQ:远程 Mac 签名失败时,先查哪一层

远程 Mac 上的 fastlane match,钥匙串应该怎样准备?

优先使用 setup_ci 创建临时钥匙串,并让 matchreadonly 模式导入已有证书和描述文件。不要直接依赖登录钥匙串;还要验证钥匙串已解锁、私钥可访问,并在任务结束后删除或清理临时资产。

本地 fastlane match 正常,到了 CI 为什么会失效?

常见原因不是编译命令,而是 CI 没有私钥、钥匙串未解锁、描述文件与 Bundle ID 不匹配,或后台进程没有访问权限。应分别检查签名身份、描述文件、构建设置和归档产物,不能只看 lane 是否返回成功。

发布流水线同步签名资产时,是否应锁定只读策略?

发布 CI 通常应使用 readonly 模式。它只同步已经存在的证书和描述文件,不在构建过程中创建或撤销签名资产。证书轮换和 profile 更新应由受控流程完成,再让 CI 拉取新版本。

App、Widget 和其他扩展怎样分别绑定 profile?

每个 Target 都应使用明确的 Bundle ID 与 profile 映射。主 App、Widget 和 Notification Service 等扩展不能依赖隐式选择;应在 Matchfile、Fastfile 或 Xcode Build Settings 中保存可审计的对应关系。

长期在线的 Mac Runner 怎样避免签名资产残留?

长期 Runner 应在每次任务后清理临时钥匙串、描述文件、DerivedData 和导出产物,并检查默认钥匙串与搜索列表。完成清理后,还要用重启后的首个任务、连续任务和失败任务重复验证。

证书到期、撤销与节点重建:恢复顺序不能反过来

证书或 profile 出现异常时,先不要执行 match nuke。该命令会撤销证书和 profile,影响范围可能超出当前构建节点。fastlane 官方文档特别提醒,清理不同环境的签名资产会影响 Ad Hoc 或 Enterprise 分发,不能当作通用排障按钮。(docs.fastlane.tools)

推荐按以下顺序恢复:

第一步:确认资产状态。

检查 Apple Developer 账户中的证书是否有效,profile 是否过期,App ID 能力是否发生变化。Apple 官方说明,证书撤销、能力变更或 profile 过期后,通常需要重新生成 profile。(developer.apple.com)

第二步:记录影响范围。

确认受影响的是 development、ad hoc、App Store 还是 macOS Developer ID。不要因为一个测试 profile 失败,就重置整个 Team 的分发证书。

第三步:在受控环境更新签名仓库。

由具备相应权限的发布负责人执行资产更新。CI 继续保持 readonly,直到新的证书和 profile 已提交、加密存储并完成审核。

第四步:重建远程节点。

清理旧钥匙串、旧 profile、DerivedData 和缓存后,再执行同步。节点重建时不要复用未知来源的 .p12.cer 或 profile 文件。

第五步:验证旧凭据失效。

确认旧证书不会继续被 security find-identity 选中,旧 profile 不会被 Xcode 自动缓存重新使用。然后完成一次全新归档、导出和产物签名检查。

第六步:保留回滚记录。

记录变更前后的证书名称、profile UUID、Team、Bundle ID、执行人和恢复结果。若新资产出现问题,应能够回到已知可用的签名仓库版本,而不是再次执行全量撤销。

⚠️ match nuke 只能在已经评估发布渠道、备份签名资产并确认团队影响后使用。它不是“远程 CI 签名失败”的第一步。

远程 Mac 与本地方案:发布稳定性取决于可复现边界

本地 Mac 的优点是交互式调试快,但长期运行 CI 时,登录状态、个人钥匙串、缓存 profile 和人工操作会形成隐性依赖。临时虚拟 macOS 环境则可能遇到硬件能力、系统兼容性和签名合规边界;云端 Linux 又无法直接替代真实 macOS 上的 Xcode 工具链。

如果当前方案依赖一台个人 Mac,常见缺点是:节点不一定持续在线、多人共用时资产边界模糊、重启后钥匙串状态不可预测。对于一次迁移、阶段性发布或需要独立重建测试的团队,具备完整 root 权限的远程 Mac 更容易把钥匙串、profile、Xcode 版本和 Runner 生命周期纳入同一套验收流程。

完成本文的签名设计后,可以先参考 SFTPMAC 的远程 Mac 方案,再结合 Mac mini 租赁价格说明 判断按周期使用是否比购买实体设备更合适。长期稳定重负载、必须连接本地物理设备或需要完全掌控硬件的团队,仍应评估自购 Mac;但对于阶段性 CI、节点重建和发布迁移,租赁一台真实 Mac 往往比继续修补不可复现的本地签名环境更直接。