為什麼自託管 Runner 比 GitHub 託管 Runner 更適合 iOS 建置?
GitHub Actions 提供的 macOS 託管 Runner 理論上免去了硬體維護,但實際生產中會碰到幾個難以回避的問題。
首先是等待時間。GitHub 的 macOS Runner 池容量遠小於 Linux,高峰時段(UTC 12:00–20:00,對應亞太工作時間)從 runs-on: macos-latest 觸發到 Runner 就緒,排隊時長中位數在 8–15 分鐘,偶爾超過 20 分鐘。對於一個 Clean Build 本身只需 6 分鐘的中型 SwiftUI 專案來說,等待時間已經超過實際編譯時間。
其次是Xcode 版本不可控。macos-latest 標籤會隨 GitHub 基礎設施升級靜默切換 Xcode 版本,歷史上曾出現 Xcode 15 → 16 的無通知升級導致專案建置失敗,需要手動追加 xcode-select 步驟並等待大版本相容。
第三是免費額度消耗快。macOS Runner 的計費權重是 Linux 的 10 倍(GitHub 官方檔案),免費帳戶每月 2000 分鐘額度在 macOS 上折算僅 200 分鐘,稍微密集一點的迭代週期就會超額計費。
相比之下,自託管 Runner 接在你自己的 Mac 上可以徹底規避以上三點:Runner 永遠線上、Xcode 版本由你維護、執行時間不計費。唯一的門檻是你需要有一臺持續執行的 macOS 機器——這正是本文要解決的問題。
準備工作:開通雲端 Mac 節點與基礎環境
在任何 iOS CI/CD 接線之前,你需要一臺能 SSH 進去、24 小時線上的 macOS 機器。本文以 VPSRox 獨享 Mac mini M4 節點為例(也適用於物理 Mac mini / Mac Studio),以下步驟在 VPSRox 控制台交付後 5 分鐘內均可完成。
SSH 連線與 Xcode Command Line Tools
-
01
設定 SSH 金鑰認證
在控制台複製公網 IP,本機執行:
ssh-copy-id -i ~/.ssh/id_ed25519.pub user@<節點IP>完成後停用密碼登入(
PasswordAuthentication no),防止暴力破解。 -
02
安裝 Xcode Command Line Tools
xcode-select --install如需完整 Xcode GUI(用於 Archive 簽署),從 developer.apple.com/downloads 下載
.xip檔案,解壓後移動到/Applications/並切換路徑:sudo xcode-select -s /Applications/Xcode.app/Contents/Developer -
03
安裝 Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"安裝完成後根據提示將
/opt/homebrew/bin加入 PATH,寫入~/.zprofile。 -
04
驗證 Xcode 建置鏈
xcodebuild -version確認輸出 Xcode 版本號與 Build Version,後續 YAML 中可用
XCODE_VERSION環境變數鎖定版本。
透過 brew install xcodesorg/made/xcodes 安裝 xcodes,可以快速切換 Xcode 版本,無需手動修改 xcode-select 路徑。在 CI 工作流中用 xcodes select 16.2 精確鎖定版本,杜絕版本漂移。
安裝與註冊 GitHub Actions 自託管 Runner
GitHub 提供的自託管 Runner 安裝包會在 Mac 上執行一個輪詢守護程式,當有 workflow 觸發時拉取 job 到本機執行,整個過程無需 Mac 向外開放埠。
在 GitHub 儲存庫取得註冊權杖
進入目標儲存庫 → Settings → Actions → Runners → New self-hosted runner,選擇 macOS 平台,複製頁面給出的下載命令與註冊權杖(Token 有效期 1 小時)。
-
01
下載並解壓 Runner
mkdir actions-runner && cd actions-runnercurl -o actions-runner-osx-arm64-2.319.1.tar.gz -L https://github.com/actions/runner/releases/download/v2.319.1/actions-runner-osx-arm64-2.319.1.tar.gztar xzf ./actions-runner-osx-arm64-2.319.1.tar.gz注意選擇
osx-arm64套件(Apple Silicon),版本號以 GitHub 頁面顯示為準。 -
02
註冊 Runner
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO --token <TOKEN> --name mac-m4-vpsrox --labels mac,xcode16,ios--labels自訂標籤稍後在 YAML 的runs-on中使用,可以精確路由到這台機器。 -
03
安裝為 launchd 服務(開機自啟)
./svc.sh install./svc.sh startlaunchd 會在使用者登入後自動拉起 Runner 程序。查看狀態:
./svc.sh status。
在 Workflow YAML 中指向自託管 Runner
註冊完成後,在 .github/workflows/ios-ci.yml 裡把 runs-on 替換為你的自訂標籤:
jobs:
build:
runs-on: [self-hosted, mac, xcode16]
steps:
- uses: actions/checkout@v4
- name: Show Xcode version
run: xcodebuild -version
自託管 Runner 在公開儲存庫中可能被 fork PR 觸發,惡意程式碼將在你的 Mac 上執行。建議:①僅在私有儲存庫或 Organization 範圍啟用;②在 Runner 的系統帳戶(非管理員)下執行,嚴格限制權限;③定期審查 ~/.bash_history 和 ~/actions-runner/_diag/ 日誌。
設定 Xcode 建置環境與相依性管理
一次乾淨的 iOS 建置通常需要解決三件事:依賴拉取、編譯快取、派生資料路徑隔離。
CocoaPods vs Swift Package Manager
如果專案使用 CocoaPods,在 Runner 機器上安裝一次即可:
sudo gem install cocoapods
在 CI 步驟中:
- name: Install CocoaPods
run: pod install --repo-update
working-directory: ./ios
使用 Swift Package Manager 時,Xcode 會在首次建置時自動解析依賴。為加快後續 CI 速度,可以在 job 中快取 SPM 解析結果:
- uses: actions/cache@v4
with:
path: ~/Library/Developer/Xcode/DerivedData
key: ${{ runner.os }}-spm-${{ hashFiles('**/Package.resolved') }}
衍生資料路徑隔離
同一臺 Runner 上如果有多個專案併發建置,預設的 DerivedData 路徑會相互干擾。推薦在 xcodebuild 命令中顯式指定派生資料路徑:
xcodebuild \
-project MyApp.xcodeproj \
-scheme MyApp \
-sdk iphoneos \
-configuration Release \
-derivedDataPath ./DerivedData \
clean build
並行建置參數調校
Mac mini M4 有 10 核 CPU(4 效能核 + 6 節能核),Xcode 預設並行數通常為核心數。對於大型專案可以適當調低以避免記憶體壓力:
defaults write com.apple.dt.Xcode IDEBuildOperationMaxNumberOfConcurrentCompileTasks 6
實測一個 80 個 Swift 檔案的 SwiftUI 專案,在 16 GB RAM 的 M4 節點上 Clean Build 耗時約 4 分 12 秒,無任何 swap 壓力。
程式碼簽署與鑰匙圈:Archive 打包全流程
程式碼簽署是 iOS CI/CD 流程中最容易出錯的環節。在無圖形介面的 CI 環境裡,預設的 macOS Keychain 行為會與正常開發環境不同,需要顯式管理鑰匙圈。
兩種主流簽署方案對比
| 方案 | 適用場景 | 優點 | 缺點 |
|---|---|---|---|
| 手動匯入 p12 憑證 | 小團隊單機 CI | 設定簡單、無需外部依賴 | 證書輪換需手動重新匯入 |
| fastlane match | 團隊協作 / 多台 CI 節點 | 憑證統一加密儲存、自動輪替 | 需要 Git 儲存庫或 S3 儲存後端 |
手動 p12 方案:建立暫時鑰匙圈
在 CI 環境裡最穩妥的做法是每次 job 建立一個臨時鑰匙圈,建置完成後刪除,避免殘留憑據:
# Create a temporary keychain
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
security set-keychain-settings -t 3600 -u build.keychain
# Import signing certificate
security import "$CERTIFICATE_P12_PATH" \
-k build.keychain \
-P "$P12_PASSWORD" \
-T /usr/bin/codesign \
-T /usr/bin/xcodebuild
# Allow xcodebuild to access the key without UI prompt
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
其中 KEYCHAIN_PASSWORD、P12_PASSWORD 等敏感變數應儲存在 GitHub Repository Secrets 中,透過 ${{ secrets.KEYCHAIN_PASSWORD }} 引用,絕對不能硬編碼在 YAML 檔案裡。
執行 Archive 並匯出 IPA
完整的 Archive → Export 流程分兩步:先用 xcodebuild archive 產生 .xcarchive,再用 -exportArchive 匯出 IPA。
# Step 1: Archive
xcodebuild archive \
-workspace MyApp.xcworkspace \
-scheme MyApp \
-sdk iphoneos \
-archivePath ./build/MyApp.xcarchive \
CODE_SIGN_IDENTITY="Apple Distribution: Your Name (XXXXXXXX)" \
DEVELOPMENT_TEAM="XXXXXXXX"
# Step 2: Export IPA
xcodebuild -exportArchive \
-archivePath ./build/MyApp.xcarchive \
-exportOptionsPlist ExportOptions.plist \
-exportPath ./build/
ExportOptions.plist 是 Export 步驟的關鍵設定檔案,需要包含 method(app-store / ad-hoc)、teamID 和 signingStyle 欄位。可以先在 Xcode GUI 裡手動 Export 一次,從產生的臨時目錄裡找到該檔案作為模板。
串接 fastlane 自動上傳 TestFlight
IPA 打包出來後,最後一步是把它推送到 App Store Connect 的 TestFlight 通道。fastlane 的 pilot(也叫 upload_to_testflight)是目前最穩定的方案,支援 App Store Connect API Key 認證,無需 Apple ID 密碼。
安裝 fastlane
推薦使用 Bundler 管理 fastlane 版本,避免全域性版本衝突:
# In project root, create Gemfile
source "https://rubygems.org"
gem "fastlane"
然後執行 bundle install,之後所有 fastlane 命令透過 bundle exec fastlane ... 呼叫。
設定 App Store Connect API Key
在 App Store Connect → Users and Access → Integrations → App Store Connect API 裡建立 API Key,下載 .p8 檔案,記下 Key ID 和 Issuer ID。
在 GitHub Secrets 中設定以下變數:
ASC_API_KEY_ID:API Key 的 IDASC_API_ISSUER_ID:Issuer IDASC_API_KEY_CONTENT:.p8檔案的 Base64 編碼內容
Fastfile 設定範例
lane :beta do
api_key = app_store_connect_api_key(
key_id: ENV["ASC_API_KEY_ID"],
issuer_id: ENV["ASC_API_ISSUER_ID"],
key_content: Base64.decode64(ENV["ASC_API_KEY_CONTENT"])
)
upload_to_testflight(
api_key: api_key,
ipa: "./build/MyApp.ipa",
skip_waiting_for_build_processing: true
)
end
在 GitHub Actions YAML 中呼叫:
- name: Upload to TestFlight
env:
ASC_API_KEY_ID: ${{ secrets.ASC_API_KEY_ID }}
ASC_API_ISSUER_ID: ${{ secrets.ASC_API_ISSUER_ID }}
ASC_API_KEY_CONTENT: ${{ secrets.ASC_API_KEY_CONTENT }}
run: bundle exec fastlane beta
skip_waiting_for_build_processing: true 讓 fastlane 在上傳完成後立即返回,不等待 App Store Connect 處理 IPA(通常需要 10–30 分鐘)。如果你的後續步驟不依賴處理結果(如通知 Slack 建置號),這個選項可以顯著縮短 CI 總耗時。
鑰匙圈常見錯誤集中排查
程式碼簽署的錯誤訊息往往語焉不詳,以下是在 CI 無介面環境中最頻繁出現的幾類錯誤及其根因與解法。
| 錯誤訊息 | 根因 | 解法 |
|---|---|---|
errSecItemNotFound |
憑證未匯入目前使用中的鑰匙圈,或暫時鑰匙圈已被 GC | 確認 security default-keychain -s build.keychain 已執行;在 archive 步驟前加 security unlock-keychain |
| could not find signing certificate for "Apple Distribution" | 憑證 Identity 名稱與 Xcode 工程 CODE_SIGN_IDENTITY 不符 |
執行 security find-identity -v -p codesigning build.keychain 核對完整 Identity 字串並複製貼上 |
| User interaction is not allowed | 鑰匙圈被鎖定,codesign 需要彈出 UI 授權對話框(無法在無頭環境彈出) | 加 security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" build.keychain,使 codesign 無需 UI 即可訪問私鑰 |
| Provisioning profile doesn't include the entitlement | 描述檔與工程 Entitlements 不符(如 Push Notifications、App Groups) | 在 Apple Developer 後台重新產生描述檔,確認已勾選全部 Capability,重新下載安裝 |
| No signing certificate "iOS Distribution" found | 描述檔與憑證類型不符(Distribution vs Development) | 在 ExportOptions.plist 中確認 method 欄位(app-store 需用 Distribution 證書) |
除錯技巧:在建置日誌裡看完整簽署指令
在 xcodebuild 後加 | xcpretty -r json-compilation-database,或者直接去掉 xcpretty 管道檢視原始日誌,搜尋 CodeSign 關鍵字可以看到實際執行的簽署命令及引數,比 Xcode GUI 的錯誤面板資訊量大得多。
另一個常見的除錯指令是:
codesign -dv --verbose=4 ./build/MyApp.xcarchive/Products/Applications/MyApp.app
直接查看 .app 內嵌的簽署資訊,確認憑證鏈是否完整。
Jenkins Agent 對照:遷移與取捨分析
如果你的團隊已經有 Jenkins 基礎設施,或者因為公司安全策略不能把程式碼推到 GitHub,Jenkins on Mac 是常見的替代方案。兩者的核心差異體現在幾個維度上。
| 維度 | GitHub Actions 自託管 Runner | Jenkins Agent (macOS) |
|---|---|---|
| 設定門檻 | 低,5 分鐘內註冊完成 | 中,需要先搭 Jenkins master(通常在 Linux/Docker) |
| 與程式碼儲存庫整合 | GitHub 原生,PR 檢查狀態自動回寫 | 需要 GitHub Branch Source 外掛設定 |
| 多平台混合 | 同一 workflow 可混用 macOS / Linux / Windows | 需要 Label 路由,設定相對繁瑣 |
| 流水線視覺化 | GitHub Actions 原生 UI,直觀 | Blue Ocean 外掛效果好,但需自行維護 |
| 機密管理 | GitHub Secrets + OIDC | Jenkins Credentials,可對接 Vault |
| 離線/內網建置 | 不支援(需能存取 GitHub) | 支援,適合強合規場景 |
| 硬體需求 | 需要一台線上 macOS 機器(Runner 主機) | 需要一台線上 macOS 機器(Jenkins Agent 主機) |
從遷移角度來看,如果原先是 Jenkins 流水線,把 sh 'xcodebuild ...' 步驟遷移到 GitHub Actions YAML 的 run: 欄位幾乎是一比一對應的。最大的差異在於觸發條件語法與併發控制:Jenkins 的 when { branch ... } 對應 GitHub Actions 的 on: push: branches:;Jenkins 的 lockable-resources 外掛功能對應 GitHub Actions 的 concurrency: 組語法。
兩套方案的共同前提是:都需要一臺常駐線上、固定 macOS 版本的物理 Mac 作為 Agent/Runner 主機。這臺機器是整條流水線穩定性的基礎——它發熱過載或版本漂移,CI 就會不可預期地失敗。
很多中小團隊在這裡遇到的實際困境不是方案選擇,而是:沒有一臺能長期跑 CI 的物理 Mac。本機辦公用的 MacBook 記憶體往往只有 8 GB,大專案編譯時散熱降頻明顯,在持續 CI 負載下體驗很差;而新採購一臺 Mac mini 的成本加上場地、電力與維護開支,前期投入也不低。
雲端獨享 Mac:沒有專屬裸機時的固定建置方案
針對上面這個困境,按需租賃獨享雲端 Mac 是一個值得認真評估的選項——既不需要前期硬體投入,又能獲得一台專屬的、固定環境的 macOS 建置節點。
以本文使用的 VPSRox Mac mini M4 節點為例:16 GB 統一記憶體 + 256 GB NVMe SSD 的標準設定,1 Gbps 獨享頻寬,單節點按天起租 $21.8,付款後 1–5 分鐘自動交付,無合同鎖定。
從 CI/CD 的角度,它相當於你租賃了一台永遠線上、獨享算力的 Mac mini,具體能做的事情與本機 Mac沒有差別:
- 安裝任意版本 Xcode(透過 xcodes 工具管理),完全鎖定建置環境
- 註冊為 GitHub Actions 自託管 Runner 或 Jenkins Agent,本文所有步驟均可直接套用
- 利用 M4 的 10 核 CPU 和 38 TOPS Neural Engine,對含 Core ML 模型編譯的專案有更好的表現
- 透過 Thunderbolt 5 並聯擴充,將多臺 Mac mini 組成私有建置叢集(適合大型團隊)
與 GitHub 託管 Runner 相比:按天租賃的成本在高頻建置場景下(如每天觸發超過 30 次建置)往往低於 macOS Runner 超額計費;與自購 Mac mini 相比:無需前期採購預算、可按專案階段調整租期(衝刺期加機、上線後減機)。
全球五個節點(新加坡、日本東京、韓國首爾、中國香港、美國東部)可以選擇距離團隊最近的位置,降低 git fetch 和依賴下載的延遲,進一步縮短整體建置時長。
雲端 Mac 節點並不只用來跑 CI。你可以同時把它作為遠端開發桌面(透過瀏覽器 VNC 或 SSH),在沒有本機 Mac 的情況下進行 Xcode 除錯、Instruments 分析、模擬器測試——這讓單臺節點的利用率大幅提升,攤薄了按日計費的單位成本。
把這套 CI/CD 流水線跑起來
文中所有步驟在 VPSRox 獨享 Mac mini M4 節點上均已驗證可用。開通節點 → SSH 進去 → 按本文步驟操作,從零到 TestFlight 自動上傳,最快當天完成。按天起租,不需要籤合同。