GitHub 官方文档说明:如果没有在线且空闲、同时匹配标签和 Runner Group 的主机,Job 会持续排队;排队超过 24 小时仍未被接收时会失败。查看 self-hosted runner 的路由规则
这说明 GitHub Actions iOS 并发构建不应该按开发者人数计算 Mac 数量。我们本周建议先记录峰值并发 Job、每个 Job 的排队与执行时长、可接受等待时间,以及 Build、Test、Archive、签名上传之间是否发生争用。低频单人项目从单机开始;构建、测试和发布经常重叠时,再验证双机或按发版周期扩容。
这篇文章适合三类人:
- 单人维护一个 iOS App,已经配置
self-hosted runner,但不确定一台远程 Mac 是否够用。 - 需要让拉取请求测试与 TestFlight 发布并行运行的小团队。
- 发版需求有明显波峰,正在评估按周或按月租用 Mac,而不是长期购买闲置硬件的项目负责人。
先分清:Workflow、Job、Runner 和 Mac 不是同一个容量单位
GitHub Actions 的 Workflow 是流程定义,Job 是其中可以被调度的执行单元,Runner 是接收 Job 的执行环境,Mac 主机则是承载 Runner 的物理或虚拟设备。
容量估算应以 Job 为单位。一次 Workflow 可能包含:
- 普通提交的编译 Job;
- 拉取请求的单元测试或模拟器测试 Job;
- 定时运行的完整测试矩阵;
- 生产 Archive Job;
- 签名、上传和发布 Job。
Xcode 内部的编译并行,不等于一台 Runner 可以同时接受多个独立 Job。GitHub 会寻找匹配 runs-on 标签和 Runner Group 的在线空闲 Runner;找到后才分配任务。因此,我们在容量卡里先按“一台 Runner 同时承载一个 Job”估算,再单独观察 Xcode 内部并行对单个 Job 执行时间的影响。查看 Runner 选择与标签规则
还要把 App Store Connect 的后台处理排除在 Runner 占用之外。Archive 生成、签名和上传需要占用 Mac;上传后,构建还要经过 Apple 系统处理,才能在 App Store Connect 中显示。这个后台等待不应该直接算成第二台 Mac 的需求。查看 App Store Connect 构建上传说明
第一类:单人低频项目,单机通常优先于扩容
单人项目的关键不是“只有一个人”,而是任务是否在同一时间重叠。
如果普通提交验证、定时测试和正式 Archive 很少同时出现,单台 Mac 通常足够。我们建议把任务分成三个优先级:
- ✅ 高优先级:正式 Archive、签名、TestFlight 上传。
- ✅ 中优先级:合并前的必要测试。
- ⚠️ 低优先级:完整测试矩阵、夜间任务、非阻断式检查。
单机方案成立,需要同时满足几个条件:
- 正式发布可以避开完整测试矩阵;
- 普通提交不会持续堆积;
- 旧提交可以取消,不必每次都跑到结束;
- 依赖下载、脚本重复执行和模拟器初始化没有制造假性拥堵;
- Mac 断线或重启后,Runner 能自动恢复并重新接收任务。
GitHub Actions 支持通过 concurrency 控制工作流或 Job 的并发。对于同一分支上的连续提交,可以取消已经过时的运行,避免旧代码占据唯一 Runner。查看并发与取消规则
但不要把所有任务都放进同一个并发组。正式发布通常需要保留顺序,拉取请求检查则可以取消旧任务。否则,表面上队列下降了,实际可能只是把还没完成的验证任务取消掉。
第二类:单人高频项目,重点看任务重叠而不是平均利用率
常见症状是:单台 Runner 的平均空闲时间不少,但每天某些时段仍然持续排队。
这通常发生在以下组合同时出现时:
- 多个功能分支连续推送;
- 拉取请求触发模拟器测试;
- TestFlight 构建与上传固定在同一时间段;
- 定时测试与依赖更新任务重叠;
- 失败 Job 自动重跑,进一步占用 Runner。
这类项目不要直接从一台 Mac 跳到多台。先按顺序执行三项动作。
1.取消没有价值的旧 Job
对拉取请求检查设置合理的 concurrency。新提交已经替代旧提交时,旧 Job 通常没有继续执行的价值。
但正式 Archive、签名和发布不能简单套用同一套取消规则。正在上传或准备发布的 Job 被取消后,可能留下难以判断的中间状态。
2.拆分 Build、Test 和 Archive Workflow
Build 和 Test 可以服务代码反馈速度。Archive 和上传则服务发布可靠性。把它们放入不同 Workflow,才能分别观察各自排队时间。
Apple 建议通过 Xcode 的 Build Timing Summary 查看构建任务耗时,也可以在命令行使用 xcodebuild -showBuildTimingSummary 获取构建计时信息。查看 Xcode 构建计时文档
测试也应保留 .xcresults 和测试报告。报告能帮助我们区分真正的测试执行时间、安装时间、启动时间以及测试后处理时间。查看 Xcode 测试结果说明
3.仍然重叠时,再增加第二台 Mac
第二台 Mac 的主要价值是并行和故障回退,不是默认提升单个 Job 的速度。
例如:
- Runner A 负责拉取请求 Build 和 Test;
- Runner B 负责 Archive、签名与 TestFlight 上传;
- 两台主机都保留一致的 Xcode 27 工具链和依赖版本;
- 发布 Job 只能路由到生产 Runner Group。
如果只是单个 Archive 太慢,但没有其他 Job 等待,第二台 Mac 不一定解决问题。此时应先优化工程、依赖和测试矩阵。
第三类:小团队,按反馈时限而不是“永不排队”配置
多人提交时,完全消除排队通常不是合理目标。更实际的标准是:拉取请求在团队可接受的时间内得到结果,发布 Job 不被普通任务拖住。
我们建议把 Runner 分成两类:
- 验证 Runner:处理 Build、Lint、单元测试和模拟器测试。
- 发布 Runner:处理 Archive、签名、上传和生产发布。
GitHub 支持使用自定义标签和 Runner Group 路由任务。标签会累积匹配,Job 必须同时满足指定条件;Runner Group 还可以形成权限边界,限制哪些仓库能访问发布环境。查看标签配置说明
建议使用类似下面的逻辑,而不是让所有 Job 只写一个宽泛标签:
jobs:
test:
runs-on: [self-hosted, macOS, ios-test]
archive:
runs-on: [self-hosted, macOS, ios-release]
实际标签名称应使用脱敏后的项目规则。不要把真实仓库名、Bundle ID、Team ID、证书名称或服务器路径直接放进公开日志。
小团队是否需要双机,可以检查四件事:
- 普通测试排队时,发布 Job 是否仍能立即进入发布环境;
- 发布 Runner 维护时,是否还有可验证的回退路径;
- 签名凭据是否只存在于发布 Runner;
- 增加第二台 Mac 后,队列下降是否真实,而不是因为取消了更多任务。
GitHub Actions 的组织级指标可观察 Job 平均执行时间、平均排队时间和失败率。个人仓库如果没有组织级指标,可以从 Workflow 运行历史和 Job 日志手工记录这些字段。查看 Actions 性能指标说明
第四类:多 App 团队,采用分层容量而不是共享一个标签
多个 App 共用 Mac Runner 时,问题通常不只是并发数量,还包括安全边界和工具链差异。
我们建议至少拆成三层:
- 开发验证层:多个仓库可以共享,用于快速 Build 和基础 Test。
- 重型测试层:运行模拟器矩阵、UI 测试和定时任务。
- 生产发布层:只允许指定仓库访问,保存发布所需的签名配置。
这样做的好处是,容量缺口可以被定位。若只有重型测试层排队,就不必为整个组织增加发布 Mac。若生产发布层必须持续可用,则需要保留常驻容量,再在集中发版期间临时扩容。
发布 Job 还要考虑 Archive 的保存。Apple 说明,分发构建需要保留对应的 Xcode Archive,否则后续排查崩溃和符号化问题可能缺少必要文件。查看构建调试信息说明
因此,发布 Runner 不应只是“能跑完命令”的临时机器。它还需要稳定保存产物、日志、Archive 和上传结果。
独立 FAQ:从长尾问题直接做容量判断
一台 Mac Runner 能同时处理多个 iOS Job 吗?
容量估算时,不要把一台 Runner 当成多个并行槽位。一个 Runner 被分配 Job 后,就不再是空闲状态;Xcode 在单个 Job 内部使用多核编译,也不会自动让第二个独立 Job 获得同一 Runner。多个 Job 重叠时,应增加 Runner、拆分优先级或取消过时任务。
独立开发者做 iOS CI,一台远程 Mac 够不够?
低频提交、测试矩阵较小、Archive 可以错峰时,一台远程 Mac 通常足够。我们建议先观察真实队列,而不是根据“以后可能会增长”提前扩容。只有当发布经常阻塞测试,或断线维护会导致无法发版时,第二台 Mac 才有明确价值。
怎样根据 GitHub Actions 排队时间估算 Runner 数量?
记录每个 Job 的排队开始时间、开始执行时间、完成时间,并标记 Build、Test、Archive、上传和失败重跑。随后观察峰值时段同时等待的 Job 数量,以及团队能接受的最长等待时间。新增 Runner 后,如果排队仍集中在依赖准备或 App Store Connect 后台处理,继续加 Mac 的收益会有限。
iOS 测试和 Archive 应该使用不同的 Mac 吗?
单人项目不必一开始就拆分。可以先用标签、并发组和发布权限控制争用。测试和 Archive 经常在同一时间发生,或者签名凭据不能与普通测试环境混用时,再设置独立发布 Runner。拆分后必须验证证书、Provisioning Profile、Xcode 和缓存版本一致。
发版高峰时临时添置一台远程 Mac,是否比全年保留更划算?
如果日常没有持续队列,只有 TestFlight 或正式发布窗口出现峰值,按周或按月增加远程 Mac 更合适。扩容前要完成一次完整验收:Runner 注册、标签路由、依赖安装、证书访问、Archive、上传和断线恢复。若交付周期无法满足发版窗口,才需要提前保留常驻机器。
第五步:把数据填入容量决策卡
我们建议连续记录一段真实工作流历史。不要只看每日构建次数,因为每日构建次数无法说明任务是否同时出现。
至少记录以下字段:
- [ ] Workflow 名称与触发来源;
- [ ] Job 名称和任务类型;
- [ ] Job 进入队列的时间;
- [ ] Runner 开始接收任务的时间;
- [ ] Job 完成时间;
- [ ] Build、Test、Archive、签名、上传分别占用多久;
- [ ] 失败重跑是否占用额外 Runner 时间;
- [ ] 同一时段最高有多少个 Job 等待;
- [ ] 是否出现 Mac 断线、重启或工具链损坏;
- [ ] 任务是否被取消,取消原因是什么;
- [ ] 发布凭据是否与普通验证任务隔离。
然后按以下条件做结论:
- ✅ 继续单机:低频任务可错峰,队列偶发,发布不阻塞测试,断线后能自动恢复。
- ✅ 增加第二台 Mac:Build、Test 与 Archive 经常重叠,且不同优先级任务互相阻塞。
- ✅ 按发版周期临时扩容:平时队列稳定,只有 TestFlight 或正式发布窗口出现持续峰值。
- ⚠️ 先优化 Workflow:队列主要由依赖下载、重复脚本、无效测试矩阵或未取消旧提交造成。
- ⚠️ 拆分生产 Runner:签名凭据、Archive、发布权限或维护窗口需要独立隔离。
如果使用 workflow_job 事件采集数据,还可以区分 Job 的状态变化。GitHub 的相关事件用于追踪工作流 Job 活动,完成事件不论成功或失败都会产生记录。查看 workflow_job Webhook 文档
三种容量模式的停止条件
单机模式的停止条件是:发布开始阻塞紧急修复,或者维护一次 Mac 就会让整个 CI 停摆。
双机模式的停止条件是:第二台机器长期空闲,且真实队列并没有下降。此时应回退到单机,或者只在固定发版窗口租用额外环境。
弹性扩容模式的停止条件是:新增远程 Mac 的工具链交付不一致,导致证书、Xcode、缓存或依赖版本出现差异。没有一致的验收流程,机器数量越多,排查成本越高。
对于需要按周期增加环境的项目,可以先查看 MESHLAUNCH 的远程 Mac 租赁方案,再根据发版窗口选择实际租期。Mac 主机交付后,仍应使用项目自己的 Workflow 历史完成验收,而不是只确认 Runner 显示为在线。
当前方案与远程 Mac:差别在闲置成本和回退能力
如果当前方案是购买一台 Mac 专门做 CI,真实缺点通常有三个:低峰期硬件长期闲置;发版波峰到来时仍然只有一个执行环境;本地断电、网络故障或系统维护会同时影响开发和发布。
如果当前方案是把所有 Job 挤在一台已有 Mac 上,问题则是测试会阻塞 Archive,普通提交会占用签名环境,故障时也没有回退路径。对只在特定周期出现峰值的团队,按周或按月租用远程 Mac,往往更适合先验证双机并行和发布隔离,再决定是否长期购买硬件。需要了解具体 Mac mini M4 租赁周期时,可以进一步查看 远程 Mac 的周期与区域方案。
本周的执行顺序很明确:先收集 Job 队列与执行数据,再取消无效重跑、拆分发布 Workflow,最后根据峰值并发决定单机、双机或发版期临时扩容。不要按开发者人数直接购买 Mac;让真实队列决定下一台机器是否值得保留。