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。按天驗證映像與憑證鏈,比發表會當天再搶併發更從容。