VPSSpark 部落格
← 返回開發日記

Phone 18 發布前,我們重新跑了一遍 iOS CI/CD:Xcode 27、iOS 27、GitHub Actions 與 Apple Silicon Runner 實際踩坑紀錄

機房手記 · 2026.09.11 · 約 12 分鐘閱讀

常見搜尋:Xcode 27 · iOS 27 · xcode-27 · Apple Silicon · Phone 18 CI

坐在沙發上使用銀白色筆記型電腦工作的開發場景
先釘死 Runner 映像與可用模擬器,再談行銷機型名稱。

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 側怎麼踩、怎麼收。

9.10
GitHub 宣布 xcode-27 切到 macOS 27
arm64
託管 xcode-27 僅 Apple Silicon
4 坑
標籤 · 模擬器 · 快取 · 磁碟/排隊

為什麼發布前要「整條重跑」,而不是改一個 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。

Phone 18 前 iOS CI 四條踩坑線:標籤、模擬器、快取、磁碟與排隊
四條線要按這個順序排:先釘死 runs-on 與映像,再動態選模擬器,再作廢舊快取,最後才談自託管池與磁碟水位。

坑一:還在用 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],並把未升級節點從該標籤摘掉。

標籤漂移比程式碼漂移更危險
發布週最怕的不是某次 compile error,而是 Runner 標籤含義變了、文件沒改。誰接 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-latestSDK/模擬器不確定明確 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 的中間檔案攪在一起。

驗收口徑
「快取命中率上升」不是升級成功指標;「冷快取也能穩定綠」才是。發布前至少留一次強制 miss 的 nightly。

坑四: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 跑得更快。

什麼時候上雲端 Mac/專用 Silicon Runner
託管 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。按天驗證映像與憑證鏈,比發表會當天再搶併發更從容。

查看雲端 Mac 方案 →

限時特惠

需要不被共享佇列綁架的 Apple Silicon 嗎?

Xcode 27 · iOS 27 · 專用雲 Mac · 按天/按月

返回首頁
限時優惠 點擊查看套餐