Phone 18 发布会的前一周,会议室里没人在聊摄像头焦段——大家盯着的是 Actions 列表里一串红灯。Xcode 27、iOS 27 SDK、GitHub 托管的 xcode-27 标签,再加上几台刚挂上的 Apple Silicon 自托管 Runner:四条线叠在同一周,任何一条漂了,App Store 截图与 TestFlight 外测都会一起卡死。我们把整条 iOS CI/CD 从「能绿」重跑到「能扛发布周」,把真实翻车点记在这里。
时间锚点也很硬:2026 年 9 月 10 日,GitHub 在 Changelog 写明 Xcode 27 runner 镜像已改跑 macOS 27,标签仍是 xcode-27 / xcode-27-xlarge,且只提供 arm64。前一天还能在 macOS 26 底子上「碰巧绿」的 job,镜像一切就可能因为系统版本、模拟器设备名或磁盘水位整片翻车。Golden Gate 正式版窗口与机型裁切,可先对照站内 macOS 27 Golden Gate 发布日期与支持机型;本文只写 CI 侧怎么踩、怎么收。
为什么发布前要「整条重跑」,而不是改一个 matrix
很多团队的肌肉记忆是:把 xcode-version 从 26 改成 27,再在 destination 里换成 iPhone 18。Phone 18 窗口前这样做几乎必炸。原因有三层。第一,SDK 与 Runner 镜像不是同步发货的——你本机已经装了 Xcode 27 GM,托管镜像可能还在 public preview,或刚从 macOS 26 底子切到 27。第二,模拟器设备表会变:iOS 27 runtime 里未必立刻出现你硬编码的机型名,硬写 name=iPhone 18 会直接 Unable to find a device matching the provided destination。第三,DerivedData、SwiftPM、CocoaPods 的 cache key 若仍绑着「Xcode 大版本之前」的路径,命中旧产物等于把昨天的编译垃圾喂给今天的编译器。
我们的做法是把「能编译」和「发布周能扛」拆开验收:前者看单条 PR job 绿;后者看并发、磁盘、证书与 Simulator 冷启动是否在同一高峰互相踩脚。托管侧先读 GitHub-hosted runners 说明,自托管侧则按标签把 PR 快反馈与 Archive 重任务拆池——这点和早先写过的 GitHub Actions 拖慢 iOS / Xcode CI 的新解法 同一套决策逻辑,只是这次触发器换成了 Phone 18。
坑一:还在用 macos-latest,或以为 xcode-27 标签「随便哪台 Mac 都行」
macos-latest 在 2026 年秋天仍然很适合「默认能跑」,但不适合作为 Phone 18 发布周的唯一验收面。它指向的是 GitHub 当前默认镜像策略,不等于「已经带齐 iOS 27 SDK + 你要的 Simulator」。要钉 Xcode 27,工作流里应显式写 runs-on: xcode-27(重任务再考虑 xcode-27-xlarge),而不是指望 maxim-lobanov/setup-xcode 在一台底子不对的镜像上硬切版本——底子缺 runtime 时,选中 Xcode.app 也救不了 simctl。
更隐蔽的一点:Changelog 强调该镜像仅 arm64。组织里若还留着 Intel 自托管节点、或有人把 runs-on: [self-hosted, macOS] 写成「所有 Mac 都能接 Xcode 27 job」,矩阵一扩就会出现「偶发绿、偶发红」——绿的是撞上 Apple Silicon,红的是 Intel 节点根本装不上 / 跑不动对应 toolchain。标签要写成你真正准备好的集合,例如 [self-hosted, macOS, ARM64, xcode27],并把未升级节点从该标签摘掉。
xcode-27、谁接 macos-26 兼容线,要写进 Runbook,并在 Actions 的 Runner 页每周对一次。
坑二:destination 写死 iPhone 18,镜像里还没有这台「电话」
营销口径里的 Phone 18,和 Simulator 列表里的设备名不是同一张表。GitHub 的 actions/runner-images 说明里,iOS 27.0 runtime 当前列出的是 iPhone 17 系列、iPhone Air、多款 iPad 等——并不保证发布会当天就出现名为 iPhone 18 的模拟器。把 YAML 写成 -destination 'platform=iOS Simulator,name=iPhone 18,OS=27.0',是发布前最常见的自伤。
可复现的做法是:job 开头用 xcrun simctl list devices available -j 解析 JSON,按「iOS 27 runtime + 名称以 iPhone 开头」排序取一台,把 UDID 写进 $GITHUB_OUTPUT,后续 xcodebuild test 只用 id=…。需要多机型矩阵时,用「可用列表 ∩ 你们白名单」做交集,缺了就 skip 并告警,而不是让整条 Release 红掉。UI 测试还要接受一个现实:新系统第一次冷启动 Simulator 可能多吃几分钟,timeout 仍按 Xcode 26 时代的 20 分钟砍,会把偶发变成必现。
| 写法 | 发布周风险 | 更稳的替代 |
|---|---|---|
name=iPhone 18,OS=27.0 | 设备名未入库即失败 | 按 UDID 动态选择 |
OS=27.0 写死小版本 | 镜像可能是 27.0.1 | 匹配 runtime 前缀或最新可用 |
只跑 macos-latest | SDK/模拟器不确定 | 显式 xcode-27 验收面 |
| Intel + Apple Silicon 混标签 | 同 job 偶发架构失败 | 标签拆池,27 线仅 ARM64 |
坑三:缓存 key 没跟着 Xcode 27 作废,绿的是「旧世界」
大版本升级里,最像「修好了」的假象来自缓存。SwiftPM 的 SourcePackages、CocoaPods 的 Pods、自研的 DerivedData tarball,若 key 只含 branch 名或 lockfile hash,不含 $(xcodebuild -version) / 镜像版本,Actions 会高高兴兴 restore 出一堆 Xcode 26 时代的产物。症状很飘:有时 link 报符号找不到,有时 Swift driver 直接崩,有时测试能编过但模块缓存校验失败。
我们把 cache key 强制加上三段:Xcode 短版本、runner image version(托管可从环境或文档钉死)、以及 Package.resolved / Podfile.lock。升级当天直接把旧 key 前缀改掉,宁可冷启动多 8–12 分钟,也不要在发布周排障「为什么同一 commit 本地过、CI 不过」。自托管 Apple Silicon 节点更要小心:磁盘上的 DerivedData 若跨作业共用,又没按 Xcode 版本分目录,会把预览版和 GM 的中间文件搅在一起。
坑四:Apple Silicon Runner 有了,磁盘和队列把你拖回原点
Xcode 27 + iOS 27 runtime + 多台 Simulator,是磁盘杀手。托管 xcode-27 镜像本身已经很重;自托管 Mac mini 若还留着 Xcode 26、两个 beta、以及三个 App 的 DerivedData,往往在 Archive 后半段才爆 No space left on device。发布周前我们给每台节点定了硬规矩:只保留一条「当前验收」Xcode、Simulator runtime 白名单化、DerivedData 按 job 清理、每晚 cron 报告 df -h 与 xcrun simctl delete unavailable。
队列问题不会因为换了 Silicon 就消失。全公司在发布会前把 matrix 从「一版 SDK」扩成「26 兼容 + 27 验收」,托管并发与自托管槽位会被同时打满。PR 快反馈和 TestFlight Archive 必须拆标签:前者追求分钟级,后者允许排队但不能堵死前者。谁在高峰抢槽,看的是标签设计,不是机器海报上的 M 系列型号。
官方工具链行为以 Apple Xcode 文档 为准;镜像里到底装了哪几个 Simulator,以 runner-images 仓库当周 Readme 为准——不要用去年的博客截图当配置源。
发布周 Runbook:我们实际怎么切
第一天,把生产 PR 线的验收 job 显式切到 xcode-27,保留一条 macos-26(或你们仍支持的上一代)做兼容哨兵,失败策略是「27 红则阻断合并,26 红则仅告警」。第二天,所有 test destination 改为动态 UDID,删掉 YAML 里的 Phone 营销名。第三天,作废缓存前缀,强制跑一轮冷构建,记下 wall time 作为发布周基线。第四天,自托管池按标签拆开:ci-pr 与 ci-release,并确认没有 Intel 节点误贴 27 标签。
值守期每天三件套:看 Changelog / runner-images 是否又推了镜像、看 Runner 磁盘水位、看 queued 与 run 的比值有没有回到「黄条长于绿条」。数字恶化时,先砍 matrix 并行度,再谈加机器——加机器却不分标签,只会让错误的 job 跑得更快。
xcode-27 排队已经长过编译、或你需要钉死某一版 GM 镜像做公证与上传时,按天租一台 Apple Silicon 云 Mac 做自托管,往往比在共享队列里赌运气便宜。专用节点适合 Release 与证书链;PR 海量并发仍可留在托管预览镜像上做第一道闸。
FAQ
必须马上把所有 job 迁到 xcode-27 吗?
不必。发布验收与商店构建要钉 27;旧系统兼容与热修可以暂时留在上一代镜像。关键是标签和分支保护规则写清楚,避免「随便一台绿了就合并」。
本机 Xcode 27 过了,为什么 CI 仍红?
优先查三件:Runner 是否真是 arm64 的 xcode-27 镜像、模拟器设备名是否存在、缓存是否命中了旧 DerivedData。本机有完整 runtime,托管镜像不一定有。
自托管和 GitHub 托管可以混在同一 workflow 吗?
可以,但要用不同 job 与不同标签。不要让同一个 runs-on 数组既匹配托管又匹配任意 self-hosted,否则调度结果不可复现。
Phone 18 实机测试能不能替代 Simulator CI?
不能替代。实机负责相机、蜂窝与性能体感;CI 负责每次 PR 的编译、单测与回归。发布周两者都要,只是不要把实机当合并闸门的唯一条件。
Phone 18 窗口 · Xcode 27 值守
需要一台不被共享队列绑架的 Apple Silicon 吗?
把 Release 与公证钉在专用云 Mac 上,PR 仍可走托管 xcode-27。按天验证镜像与证书链,比发布会当天再抢并发更从容。