短周期「突发」构建最怕两件事:排队把窗口挤没、缓存把环境悄悄弄脏。把 GitLab Runner 注册到云 Mac 上并选用 shell 执行器,可以直接复用你在机器上已经验证过的 Xcode、Ruby/CocoaPods 与签名钥匙串,省去 Docker on Mac 或 SSH 远程脚本的额外一层抽象;代价是同一用户主目录下的全局状态要靠自己用标签与缓存键管住。下文按我们线上常用的参数写,可直接改仓库里的 .gitlab-ci.yml 试用。
Shell Executor:何时值得接云 Mac
当 Job 主要是 xcodebuild、Archive、notary 或依赖本机钥匙串的脚本时,shell 比容器执行器更省事:路径、权限与 GUI 会话(若需要)都和人工登录排障一致。建议为 Runner 单独建系统用户或至少独立 Home,避免与人工桌面共用 DerivedData;并发大于 1 时务必评估磁盘 IO 与 codesign 锁竞争,否则 wall time 会被尾延迟拖长。
gitlab-runner 固定 concurrent、为 shell executor 设置 environment 注入 CI_DERIVED_DATA_PATH、FASTLANE_SKIP_UPDATE_CHECK 等,让 YAML 只描述阶段,环境差异集中在 Runner 配置,突发扩容时只换 Runner 不改仓库。
缓存键:让「快」不牺牲「准」
GitLab 的 cache:key:files 建议至少纳入 Gemfile.lock、Podfile.lock、Package.resolved 中与编译相关的锁文件;Xcode 大版本升级时额外拼一段 MACOS_CI_IMAGE_ID 变量,避免旧 DerivedData 混进新工具链。policy 用 pull-push 适合主干高频分支,只对发布分支或 nightly 打开全量 push,可减轻对象存储写放大。
cache: key: files: - Gemfile.lock - Podfile.lock - YourApp.xcworkspace/xcshareddata/swiftpm/Package.resolved prefix: "${CI_COMMIT_REF_SLUG}-xcode${XCODE_VERSION}" paths: - .cocoapods/ - ~/Library/Developer/Xcode/DerivedData/ policy: pull-push
标签策略:burst、steady 与「只跑云 Mac」
用 tags 把 Runner 分成 mac-burst(弹性云节点,允许抢占与较短超时)与 mac-steady(合同内固定配额,跑签名与上架)。默认流水线只打 burst;需要钥匙串解锁或人工值守的步骤单独 job 并打 steady,避免突发任务挤占关键路径。与「第二条 macOS 流水线还是拆 Linux Job」的取舍可对照:2026年短周期冲刺:加开第二条macOS CI流水线还是把Job拆到Linux代理?排队成本与密钥隔离决策矩阵与FAQ。
| 标签组合 | 典型 Job | 超时与重试建议 |
|---|---|---|
mac-burst + shell |
单测、静态分析、无签名 Archive | timeout: 25m,retry: 1 仅网络类 |
mac-steady + shell |
notary、上传 TestFlight | 长超时 + 禁止与 burst 共用并发槽 |
mac-m4(可选机型维度的标签) |
大工程链接峰值 | 与资源池买/租策略联动,参见 2026年企业远程Mac Runner资源池买还是租:M4与M4 Pro、六地域延迟和并发标签运维决策矩阵 |
和 GitHub Actions 混用:决策矩阵(简版)
双平台常见动机是:开源侧仍在 GitHub,私有制品与内网依赖在 GitLab。原则是「谁离代码与密钥最近,谁跑重 Job」。若 GitHub 已有成熟的 macOS workflow,可把轻量检查留在 Actions,把需要内网缓存或 GitLab Registry 的步骤迁到自托管 Runner;反之亦然。注意两边都开缓存时,用不同 bucket 前缀与 key,避免同名 key 互相覆盖。
| 场景 | 优先放 GitLab(云 Mac shell) | 优先放 GitHub Actions |
|---|---|---|
| 仓库主托管在 GitLab | 是,减少镜像与 submodule 双拉 | 仅保留社区贡献 PR 的快速检查 |
| 依赖 GitHub Packages / OIDC | 用 id_tokens 或 PAT 镜像到内网后再跑重活 |
原生集成更简单 |
| 突发 PR 风暴 | 加 burst Runner 与队列上限 | 弹性 larger runner 计费与配额单独评估 |
tags 是否为空,并关闭未打标签 Runner 的「Run untagged」。Q:缓存命中但编译仍全量? 多半是 Xcode 小版本变了却没进 key,把 xcodebuild -version 写入 artifact 供对照。Q:与 Actions 双跑重复? 用 rules:changes 或路径过滤限定触发面,并把「权威 green」只绑在一侧,避免双红双绿扯皮。
ios_burst: stage: build tags: [mac-burst, shell] rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' variables: GIT_STRATEGY: fetch FASTLANE_OPT_OUT_USAGE: "1" script: - xcodebuild -version - bundle exec fastlane ci_unit
在云端 Mac mini 上,这一切更顺畅
自托管 GitLab Runner 要稳定,底层机器比 YAML 技巧更关键:Apple Silicon 统一内存让 Xcode 链接与 Swift 编译峰值更平滑,macOS 上 Homebrew、钥匙串与 codesign 链路开箱即用,省去跨系统模拟带来的排障时间。云端 Mac mini M4 待机功耗仅约 4W,适合作为长期在线的 shell Runner;Gatekeeper 与 SIP 叠加,恶意软件面远小于把同等负载摊在杂牌 Windows 组装机上。
从总拥有成本看,小团队不必先买整机:按项目弹性开 burst 节点、用标签把 steady 任务隔离,就能把短周期构建的排队与证书风险一起压住——这与本文的缓存键、标签策略是同一套思路在不同维度的延伸。
如果你正在把 GitLab 与 GitHub 的混合流水线落到可值守、可扩缩的硬件上,VPSSpark 云端 Mac mini M4 是目前性价比很高的起点——立即了解套餐方案,让 Runner 注册完就能专心写流水线,而不是和宿主机较劲。