症状: PR 能在 Linux 上通过,但 iOS framework、模拟器验证或 TestFlight 发布总在最后一步卡住。
最快解法: 不要把 Kotlin Multiplatform iOS CI 全部押在一种节点上;PR 验证与普通模拟器构建优先托管 macOS Agent,生产签名、私网依赖和固定 Xcode 环境优先使用专用远程 Mac,峰值明显的团队采用“固定生产基线+托管弹性容量”。
SECTION 01谁该看这篇?
如果你正在为 Kotlin Multiplatform 项目补齐 iOS 自动构建、模拟器测试和 TestFlight 发布,这篇文章适合你。
如果你负责签名凭证、内部代码仓库、私网制品库或 macOS 构建预算,也可以直接用下面的场景矩阵做初筛。
截至 2026 年 8 月 27 日,官方文档已经给出使用托管 macOS Agent 构建、签名并发布 iOS 应用的流程,也提供了基于 macOS Runner 构建 Kotlin Multiplatform 项目的示例;但 Apple 官方页面列出的 Xcode 27 仍是 beta 版本,不能把 beta 镜像当成长期生产基线。(kotlinlang.org)
SECTION 02先按场景分流:哪些任务该放在哪里?
你要先区分“验证共享代码”和“验收 Apple 产物”。Kotlin Multiplatform 的共享测试可以在非 macOS 节点执行,但 iosArm64 面向真实 iPhone 或 iPad 设备,iosSimulatorArm64 面向 Apple Silicon Mac 上的模拟器,两者产生的目标和验证结果并不相同。(kotlinlang.org)
| 工作负载场景 | 托管 macOS Agent | 专用远程 Mac | 推荐决策 |
|---|---|---|---|
| 共享 Kotlin 静态检查、Gradle 测试 | 通常不需要占用 macOS | 不建议专门承接 | 放到通用 Runner,降低 Mac 队列 |
| PR 中的 iOS framework 编译 | 适合短时、可重复任务 | 适合需要固定缓存或私有依赖的项目 | 默认托管,遇到网络边界再转专用 |
| iOS Simulator 验证 | 适合并行拉起、按任务释放 | 适合固定模拟器、长期缓存与诊断 | 普通验证托管,复杂回归专用 |
| 设备归档与 Release 构建 | 可以执行,但要确认 Xcode、签名与权限 | 更容易固定版本和工作区 | 关键应用优先专用 |
| TestFlight 自动发布 | 可行,但凭证暴露面更需治理 | 适合独立发布节点和审计 | 生产发布优先隔离节点 |
| 私网仓库、制品库、企业代理 | 取决于托管 Agent 是否支持网络边界 | 可按企业网络策略控制 | 不能接入时不要强行扩大权限 |
| 发布高峰、突发并发 | 适合弹性扩容 | 容量固定,需提前预留 | 混合方案更稳妥 |
这里的“托管”不是天然安全,“自建”也不是天然可控。托管 Runner 通常是任务级临时环境,适合不保留工作区状态的验证;自托管或专用远程 Mac 则可能长期保存缓存、凭证、日志和 SSH 访问路径,必须额外设计清理、隔离与恢复机制。官方安全文档也明确提醒,自托管 Runner 可能被工作流中的不受信任代码持久化影响。(docs.github.com)
SECTION 03PR 验证:不要让每个 Job 都抢 Mac
Kotlin Multiplatform 项目最容易出现的浪费,是把所有检查都绑定到 macOS。实际上,代码格式、共享模块编译、通用 Gradle 测试和部分依赖检查,可以先在 Linux 或其他通用节点完成;只有涉及 Xcode、Apple SDK、iOS framework 或模拟器的任务,才进入 macOS 阶段。
你可以把一次 PR 流水线拆成以下顺序:
-
第一步:执行通用检查。
运行 Kotlin 编译、静态分析、依赖锁定检查和共享模块单元测试。此阶段的结果只能证明共享代码和通用构建逻辑通过,不能证明 iOS 应用可以安装或发布。 -
第二步:生成 iOS 目标产物。
在 macOS 节点执行iosArm64与iosSimulatorArm64对应的 framework 或 XCFramework 构建。官方示例显示,XCFramework 可以把多个 Apple 目标聚合到同一个分发包中,但这不代表只构建一个目标就完成了设备兼容性验证。(kotlinlang.org) -
第三步:运行模拟器验证。
至少覆盖项目实际支持的模拟器架构与最低部署目标。模拟器构建成功,只能说明该模拟器目标上的链接和测试通过,不能替代真实设备归档、签名和 TestFlight 处理。 -
第四步:上传可追溯工件。
保存 framework、.app、测试报告和构建日志的摘要,不要把完整 Keychain、临时签名文件或包含凭证的环境变量写入工件。 -
第五步:按变更风险决定是否进入设备构建。
共享业务逻辑的小改动可以只跑模拟器 Job;涉及原生桥接、Xcode 工程、Entitlements、推送能力或签名配置的变更,应触发设备归档验收。
| CI 任务 | 能证明什么 | 不能证明什么 | 节点建议 |
|---|---|---|---|
commonTest 与共享模块编译 |
Kotlin 逻辑和通用依赖可编译 | iOS SDK、签名、设备安装 | 非 macOS 优先 |
iosSimulatorArm64 构建 |
Apple Silicon 模拟器目标可链接 | 真机架构、发布签名、TestFlight | 托管 Agent |
iosArm64 Release 构建 |
设备目标的归档输入可生成 | 凭证权限和商店处理结果 | 专用或受保护托管 Job |
| Xcode Simulator 测试 | 指定模拟器环境下的行为 | 真机传感器、推送、证书链 | 托管或专用,取决于缓存 |
| TestFlight 上传 | 构建已提交到 App Store Connect | 外部测试反馈和正式审核结果 | 独立生产发布节点 |
托管 Agent 的优势,是每次任务都可以从较干净的环境开始,并且更容易把多个独立 PR 分散到不同 Job。其缺点也很明确:缓存无法按你的方式长期保留,镜像中的 Xcode 版本可能变化,私网访问和固定出口也未必满足企业边界。
SECTION 04模拟器与 Xcode 27:固定版本比“能跑一次”更重要
Apple 目标编译的风险,通常不在第一次构建,而在版本变化之后。Xcode、macOS、iOS SDK、Kotlin/Native 编译器、Gradle 插件和第三方二进制依赖之间存在联动;如果你只记录“某次流水线成功”,却没有记录完整工具链,下一次升级就很难复现。
截至本文更新日,Apple 官方系统要求页面将 Xcode 27 beta 4 列为 beta,并要求 macOS Tahoe 26.4 或更高版本;同一页面也列出了 Xcode 26.6 等已发布版本。生产流水线不应仅因为托管镜像出现 xcode-27 标签,就直接把它设为唯一发布环境。(developer.apple.com)
建议你在节点标签中明确写出以下信息:
- Xcode 主版本与完整构建号;
- macOS 主版本;
- Kotlin Gradle plugin、Gradle 和 JDK 版本;
iosArm64、iosSimulatorArm64是否都已声明;- CocoaPods、Swift Package Manager 或内部二进制依赖的锁定状态;
- 模拟器运行时是否已预装;
- Derived Data 和 Gradle 缓存的生命周期。
托管 Agent 适合快速验证“当前代码能否在标准环境构建”。专用远程 Mac 更适合固定 Xcode、保留经过验证的依赖缓存,并在升级前复制出一套试点环境。注意,缓存保留会缩短准备时间,却会增加旧依赖残留和工作区污染的风险;专用节点必须配套清理策略,而不是无限累积缓存。
⚠️ 经验提醒: Xcode 27 在官方页面仍处于 beta 状态。你可以用它做兼容性试点,但生产发布至少要保留一条使用已确认版本的回退流水线,并把升级结果记录为可复核的构建证据。(developer.apple.com)
SECTION 05TestFlight 发布:真正要隔离的是信任边界
Kotlin Multiplatform 自动发布 TestFlight,不只是把最后一条命令从终端搬进 CI。发布 Job 同时接触代码、构建产物、Apple Distribution certificate、私钥、Keychain、App Store Connect API key 和商店上传权限,因此它的安全等级应高于普通 PR Job。
App Store Connect API 使用 JWT 进行授权,API key 可以按角色或用户范围生成;上传构建还需要符合 Apple 对构建工具版本和应用关联关系的要求。TestFlight 构建必须包含 provisioning profile 中的应用标识符,上传后还要经过 Apple 的处理流程才会出现在 App Store Connect。(developer.apple.com)
推荐的凭证流向如下:
受保护分支
↓
生产环境审批
↓
专用发布 Job
├─ 临时导入 Distribution certificate
├─ 临时创建或解锁 Keychain
├─ 使用最小权限 API key
├─ archive / export
└─ 上传后清理证书、私钥与临时目录
你还需要把权限拆开:
- PR Job: 只能读取普通依赖和测试配置,不能读取生产签名凭证。
- 模拟器 Job: 通常不需要 Distribution certificate,也不应获得 TestFlight 上传权限。
- Release archive Job: 只允许受保护分支触发,并绑定固定 Xcode 环境。
- TestFlight deploy Job: 使用独立 production environment,启用人工审批、并发限制和分支限制。
- 平台管理员: 负责轮换 API key、证书和节点访问账号,不直接参与每次普通构建。
如果你使用带有环境保护规则的 CI,生产环境可以在审批通过前阻止 Job 读取环境密钥;并发控制还可以避免同一应用同时进行多个生产发布。(docs.github.com)
专用远程 Mac 的优势,是可以把发布账号、Keychain、固定工作区和审计日志放在一台隔离节点上;但这只代表边界更容易实施,不代表默认安全。你仍然要验收账号隔离、工作区清理、无人值守重启、远程访问日志、网络出口和故障恢复。
SECTION 06私网依赖:节点能联网,不等于能进你的网络
企业 KMP 项目经常依赖内部 Maven 仓库、私有 CocoaPods、企业代理、内部 Git 仓库或只能从固定出口访问的制品服务。托管 Agent 如果无法满足数据驻留、IP 白名单、代理配置或私网路由要求,就不应该通过扩大 Token、SSH key 或 API key 权限来“临时打通”。
此时你需要先画出依赖清单:
- 源代码从哪里拉取,是否包含受限仓库;
- Kotlin、Gradle、CocoaPods 和 Swift Package 依赖从哪里下载;
- 构建期间是否访问内部 API、制品库或配置中心;
- 哪些请求必须经过企业代理;
- 构建日志是否可能打印内部地址、包名或凭证片段;
- 节点故障后,是否能够撤销访问并重新注册。
自托管 Runner 的基本要求包括:节点能够安装并运行 Runner 程序、可以与 CI 服务通信,并拥有满足工作流的硬件资源;如果没有在线且空闲的匹配节点,任务会持续排队,超过 24 小时仍未执行时会失败。(docs.github.com)
这意味着专用远程 Mac 的运维对象不只是“机器是否开机”,还包括 Runner 服务是否在线、标签是否正确、磁盘是否已满、网络证书是否过期、重启后是否自动恢复,以及工作流是否能在节点失联时回退到托管容量。
SECTION 07第一步:用四类负载规划混合容量
不要从“团队有多少开发者”直接推导 Mac 数量。更有效的方式,是分别记录以下四类负载:
- 日常基线: 普通 PR 的 iOS framework 构建、模拟器测试和少量归档;
- 发布高峰: 版本冻结、TestFlight 集中上传和回归测试;
- 节点故障: 专用 Mac 离线、Xcode 损坏或凭证轮换后的恢复任务;
- 升级试点: 新 Xcode、Kotlin 或第三方 SDK 的兼容性验证。
可以用变量建立容量模型,而不预填金额或节省比例:
日常固定容量 = 生产发布并发数 + 私网任务并发数 + 必要冗余
托管弹性容量 = 峰值并发 Job − 固定容量可承接的并发 Job
月度托管支出 = 托管计费单位 × 实际消耗量
专用节点总成本 = 租赁或采购成本
+ 运维工时
+ 监控与备份成本
+ 故障期间的发布中断损失
当托管 Agent 的任务主要是短时 PR 验证时,按任务消耗更容易控制闲置成本;当生产签名、私网依赖和固定 Xcode 长期占用时,专用远程 Mac 的可控性通常更有价值。最终不要只比较构建分钟数,还要把排队、凭证处理、故障恢复和发布中断一起纳入 TCO。
托管 macOS Runner 也存在具体限制。例如公开文档列出的标准 Apple Silicon macOS Runner 为 3 个 M1 CPU、7 GB 内存和 14 GB SSD,并且 xcode-27 标签仍属于 Public preview;这些规格只能作为对应托管环境的参考,不能推导出所有项目的构建性能。(docs.github.com)
SECTION 08什么时候该选托管、自建或混合?
你可以按下面的准入条件完成采购前评审:
适合优先托管
✅ PR 并发有明显波动;
✅ Job 可以从干净环境重复执行;
✅ 不依赖企业私网或固定出口;
✅ 不需要长期保留模拟器状态和 Derived Data;
✅ 生产凭证可以限制在独立的受保护环境中;
✅ 团队希望减少 macOS 节点维护工作。
适合专用远程 Mac
✅ 生产签名和 TestFlight 发布需要独立节点;
✅ 依赖内部代码仓库、制品库或企业代理;
✅ 必须锁定 Xcode、macOS 和模拟器运行时;
✅ 构建缓存经过验证,保留后能明显减少准备工作;
✅ 需要通过 VNC 或 SSH 进行故障排查;
✅ 发布中断的业务影响高于节点闲置成本。
适合混合部署
✅ 日常 PR 数量不稳定,但生产发布不能排队;
✅ 既有公共验证任务,也有受限代码和私网依赖;
✅ 团队需要新旧 Xcode 并行试点;
✅ 希望在专用节点故障时仍有托管回退路径;
✅ 能够把任务标签、凭证范围和网络权限明确拆开。
在实际落地时,你可以先使用 MACNOX 的远程 Mac 方案 建立一台隔离试点节点,把真实的 KMP 构建、Xcode 归档、远程重启和故障恢复记录下来,再决定是否扩大容量。涉及预算核算时,可同步查看 MACNOX 的套餐与计费页面,但不要用标价替代你的队列、并发和恢复数据。
SECTION 09当前方案与 Mac 方案:差别在可控性,不只在能否构建
如果你继续把生产流程压在普通共享托管 Agent 上,常见缺点是:Xcode 镜像变化需要重新验证;私网依赖和固定出口不一定可用;生产凭证必须进入更复杂的托管密钥边界;高峰时还可能与其他任务争抢并发容量。
如果改为一次性采购多台本地 Mac,问题则变成设备折旧、硬件交付、远程办公接入、故障备件、系统升级和闲置容量。对需要临时扩展 KMP 构建能力、验证 Xcode 兼容性或隔离 TestFlight 发布的团队,先租用 MACNOX 的专用远程 Mac 做真实流水线试点,通常比直接锁定长期采购更容易验证假设;但如果你的负载全年稳定且持续重载,或者必须连接物理 iPhone、专用 USB 设备,本地自购仍可能更合适。
SECTION 10常见问题
Kotlin Multiplatform 构建 iOS 应用一定要用 Mac 吗?
共享 Kotlin 代码的静态检查、部分单元测试和 Android 构建不一定需要 Mac;但只要任务调用 Xcode、构建 iOS framework、运行 iOS Simulator、生成设备归档或完成 Apple 签名,就必须放在 macOS 环境中。不要用 Linux 测试通过替代真实 iOS 产物验收。
Kotlin Multiplatform iOS CI 应该选托管还是自托管节点?
PR 验证和短时模拟器构建通常优先托管 macOS Agent,因为环境创建和并行扩容更简单。生产签名、私网制品库、固定 Xcode 版本以及需要长期缓存的任务,更适合隔离的专用远程 Mac。负载波动明显时,采用固定生产基线加托管弹性容量。
TestFlight 发布时,怎样控制证书和 API key 的暴露范围?
把 TestFlight 发布放入独立 production 环境,只允许受保护分支和经过审批的 Job 读取 App Store Connect API key、Distribution certificate 与 Keychain 密钥。PR Job 不应接触这些凭证;发布节点还应限制仓库范围、网络出口、日志内容,并在任务后清理临时文件。
团队应依据哪些指标决定 macOS 节点数量?
先按日常基线、发布高峰、节点故障和 Xcode 升级试点分别统计队列长度、并发 Job、占用时长与恢复时间。固定节点只承接不可中断或需要稳定环境的任务,峰值 PR 和模拟器验证交给托管容量;连续多个周期出现排队或恢复不足,再增加专用节点。
最后更新于 2026 年 8 月 27 日;版本、目标架构、Xcode 状态、托管 Runner 能力与签名流程已依据 Kotlin、Apple 及 CI 官方文档核实。