接線目標:跑通一條可驗收的 iOS 建置鏈路
在動手裝 Runner 之前,我們先定義「接線完成」的驗收標準,避免裝完 Runner 卻發現 Archive 卡在簽名環節。本次實錄的終點是:push 到指定分支 → GitHub Actions 在專屬 M4 節點上觸發 → checkout 程式碼 → 執行 xcodebuild archive 併成功產出 .xcarchive。TestFlight 上傳不在本文範圍,但 Archive 一旦穩定,後續 fastlane 或 altool 只是附加步驟。
測試倉庫是一箇中等規模的 SwiftUI 應用:約 60 個 Swift 原始檔、CocoaPods 管理三方依賴、Release 配置走 Manual 簽名。基準環境為 JexMac 新加坡節點上的 Mac mini M4(16 GB 統一記憶體、256 GB NVMe),Xcode 16.2 透過 xcodes 安裝並鎖定。
對照組是同一倉庫在 runs-on: macos-14 託管 Runner 上的表現:UTC 13:00–18:00 時段(亞太下午)job 排隊中位數 11 分鐘,最長一次等待 19 分鐘;且 macos-14 預裝 Xcode 版本與本地開發機不一致,曾觸發 Swift 6 語法相容問題。自託管的價值在這裡非常直觀——等待時間從分鐘級降到秒級,Xcode 版本完全由你寫死在節點上。
託管 macOS Runner 的三類隱性成本
很多團隊最初選 GitHub 託管 Runner 是因為「零運維」,但在 iOS 場景裡,隱性成本往往比帳單更棘手。
第一類是時間成本。GitHub 的 macOS 池容量有限,免費帳戶每月 2000 分鐘額度按 macOS 10 倍權重摺算,實際可用約 200 分鐘。一個每天觸發 8 次、單次建置 6 分鐘的專案,月消耗約 1440 加權分鐘——接近免費額度上限,稍微加個 nightly 建置就會超額。
第二類是環境漂移。macos-latest 標籤會隨 GitHub 基礎設施升級而切換底層 Xcode,歷史上多次出現「昨天綠、今天紅」的情況。臨時 workaround 是在 workflow 里加 sudo xcode-select 步驟,但每多一個版本切換就多 1–2 分鐘開銷,且無法保證與本地開發機完全一致。
第三類是除錯成本。託管 Runner 每次 job 結束後環境被銷燬,無法 SSH 進去復現問題。鑰匙串彈窗、描述檔案過期這類需要「登上去看一眼」的故障,在託管環境裡只能反覆 push 試日誌——我們曾為一個 User interaction is not allowed 錯誤來回 push 了 7 次才定位到 partition list 缺失。
本文所有命令與耗時資料均在 JexMac 獨享 Mac mini M4 實體節點上執行,節點位於新加坡資料中心,測試視窗為 2026 年 7 月下旬。硬體規格:Apple M4 · 10 核 CPU · 16 GB 統一記憶體 · 1 Gbps 獨享頻寬。
節點交付後:SSH 首連與環境基線
JexMac 控制台在付款確認後通常 1–5 分鐘內推送 SSH 憑證。拿到公網 IP 後,建議先用 Ed25519 金鑰接入並關閉密碼登入,再開始裝 Runner——Runner 程序會以當前 macOS 使用者身份執行,SSH 安全基線要先打好。
-
01
寫入 SSH 公鑰
ssh-copy-id -i ~/.ssh/id_ed25519.pub jexmac@<節點IP>驗證免密登入成功後,編輯
/etc/ssh/sshd_config設定PasswordAuthentication no,重啟 sshd。 -
02
安裝 Homebrew 與 xcodes
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"brew install xcodesorg/made/xcodesxcodes install 16.2 --experimental-fast-pass安裝並鎖定 Xcode 16.2(版本號按專案需求調整)。 -
03
驗證建置鏈
xcodebuild -version應輸出Xcode 16.2及 Build 號。xcodebuild -showsdks | grep iphoneos確認 iOS SDK 可用。 -
04
安裝 CocoaPods(若專案使用)
sudo gem install cocoapods -n /usr/local/bin提前在節點上跑通一次
pod install,確認 Specs 倉庫快取就緒,避免 CI 首次建置卡在 repo update。
基線配置全部完成後,節點上應存在一個專用目錄(本文用 ~/ci-runner)存放 Runner 與建置產物,與日常開發檔案隔離。我們在該目錄下預留 DerivedData 子目錄,後續 workflow 裡顯式指定 -derivedDataPath,防止多專案併發時路徑衝突。
GitHub 側:Runner 令牌與標籤路由設計
進入目標倉庫 → Settings → Actions → Runners → New self-hosted runner,平台選 macOS ARM64。頁面會生成一次性註冊令牌(有效期 1 小時)和下載連結。
標籤設計直接影響 workflow 能否路由到正確機器。我們的命名約定:
mac:所有 macOS 自託管節點的通用標籤m4:標識 Apple Silicon M4 晶片,便於區分舊 Intel 節點xcode-16-2:鎖定 Xcode 小版本,升級時改標籤而非改 workflow 邏輯sg:資料中心區域(新加坡),多區域部署時可做就近路由
註冊命令中的 --labels 引數一次性寫入上述標籤。workflow 裡用陣列形式匹配:
jobs:
ios-archive:
runs-on: [self-hosted, mac, m4, xcode-16-2]
concurrency:
group: ios-build-${{ github.ref }}
cancel-in-progress: true
concurrency 組確保同一分支不會並行跑兩個 Archive——在 16 GB 記憶體的 M4 節點上,雙專案併發 Clean Build 雖然能跑,但 DerivedData 爭用會讓單次耗時波動 30% 以上。如果團隊有多條產品線,建議按產品線拆標籤(如 product-a、product-b)並配多臺節點,而不是在一臺機器上硬併發。
自託管 Runner 在公開倉庫中可被 fork PR 觸發,惡意 workflow 將在你的 Mac 上執行任意程式碼。務必僅在私有倉庫或 Organization 級別啟用,且 Runner 程序執行在非管理員帳戶下。生產環境建議配合 GitHub Environment 保護規則,限制 secrets 僅在指定分支可用。
在 M4 節點安裝 Runner 並配置 launchd 常駐
以下步驟在 SSH 會話中執行,Runner 版本號以 GitHub 註冊頁面顯示為準(本文示例為 v2.321.0)。
-
01
下載並解壓
mkdir -p ~/ci-runner/actions-runner && cd ~/ci-runner/actions-runnercurl -o actions-runner-osx-arm64-2.321.0.tar.gz -L \ https://github.com/actions/runner/releases/download/v2.321.0/actions-runner-osx-arm64-2.321.0.tar.gztar xzf ./actions-runner-osx-arm64-2.321.0.tar.gz -
02
互動式註冊
./config.sh --url https://github.com/YOUR_ORG/YOUR_REPO \ --token YOUR_ONE_TIME_TOKEN \ --name jexmac-m4-sg-01 \ --labels mac,m4,xcode-16-2,sg \ --unattended--unattended跳過互動確認,適合腳本化部署。Work folder 保持預設_work即可。 -
03
安裝 launchd 服務
./svc.sh install./svc.sh start驗證:
./svc.sh status應顯示active (running)。GitHub 倉庫 Runners 頁面出現綠色 Online 狀態即註冊成功。
launchd 會在系統重啟後自動拉起 Runner,但有一個細節:Runner 以安裝時的 macOS 使用者身份執行,該使用者必須已登入過一次(或啟用自動登入),否則 launchd 可能無法訪問 Keychain。我們在節點上建立了專用帳戶 ci-bot,首次透過 SSH 登入完成鑰匙串初始化後再裝 svc.sh,後續重啟無需人工干預。
Runner 日誌位於 ~/ci-runner/actions-runner/_diag/,每次 job 結束後會生成 Worker_*.log。排查「job 分配到了節點但沒有步驟輸出」類問題時,先看這裡的 RunnerListener 日誌,比 GitHub UI 上的 Annotations 資訊更完整。
最小可用 Workflow:從 checkout 到 Archive
註冊完成後,在倉庫建立 .github/workflows/ios-archive.yml。下面是我們驗證透過的最小版本,不含 TestFlight 上傳,專注 Archive 產出:
name: iOS Archive
on:
push:
branches: [main, release/*]
pull_request:
branches: [main]
jobs:
archive:
runs-on: [self-hosted, mac, m4, xcode-16-2]
timeout-minutes: 30
steps:
- uses: actions/checkout@v4
- name: Select Xcode
run: xcodes select 16.2
- name: Install pods
run: pod install --deployment
working-directory: ios
- name: Build archive
run: |
xcodebuild archive \
-workspace ios/MyApp.xcworkspace \
-scheme MyApp \
-sdk iphoneos \
-configuration Release \
-archivePath ./build/MyApp.xcarchive \
-derivedDataPath ~/ci-runner/DerivedData \
CODE_SIGN_STYLE=Manual \
CODE_SIGN_IDENTITY="Apple Distribution: Your Team (TEAMID)" \
PROVISIONING_PROFILE_SPECIFIER="MyApp AppStore"
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
- name: Upload xcarchive artifact
uses: actions/upload-artifact@v4
with:
name: MyApp-xcarchive
path: ./build/MyApp.xcarchive
retention-days: 7
幾個刻意保留的設計選擇:
pod install --deployment 使用 Podfile.lock 鎖定版本,避免 CI 與本地依賴不一致。-derivedDataPath 指向節點上的固定目錄,多次建置可複用編譯快取,增量建置從 4 分鐘降到約 1 分 40 秒。upload-artifact 把 xcarchive 上傳到 GitHub,方便在沒 SSH 權限的同事那裡下載驗證——artifact 保留 7 天,足夠做一次 QA 抽檢。
無介面環境:鑰匙串與 Distribution 證書匯入
Archive 步驟能否成功,取決於 CI 環境能否在無圖形介面的情況下訪問 Distribution 私鑰。託管 Runner 預配置了系統鑰匙串,但自託管節點需要你自己管理——這也是很多團隊卡在「Runner 裝好了但 Archive 報簽名錯誤」的原因。
推薦做法:每次 job 建立臨時鑰匙串,匯入 p12 後立即用於簽名,job 結束自動銷燬,避免私鑰長期駐留磁碟。
# 在 "Build archive" 步驟之前加入
- name: Import signing certificate
run: |
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
echo "${{ secrets.CERTIFICATE_P12_BASE64 }}" | base64 --decode > cert.p12
security import cert.p12 \
-k build.keychain \
-P "${{ secrets.P12_PASSWORD }}" \
-T /usr/bin/codesign \
-T /usr/bin/xcodebuild
security set-key-partition-list \
-S apple-tool:,apple: \
-s -k "$KEYCHAIN_PASSWORD" build.keychain
rm -f cert.p12
env:
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
GitHub Repository Secrets 中需預先配置三個變數:KEYCHAIN_PASSWORD(臨時鑰匙串密碼,隨機字串即可)、CERTIFICATE_P12_BASE64(Distribution 證書的 Base64 編碼)、P12_PASSWORD(匯出 p12 時設定的密碼)。
| 報錯關鍵詞 | 常見根因 | 處理辦法 |
|---|---|---|
User interaction is not allowed |
私鑰 partition list 未設定,codesign 試圖彈 UI | 補跑 set-key-partition-list 命令(見上方腳本) |
errSecItemNotFound |
證書匯入了錯誤的鑰匙串,或 default-keychain 未切換 | 確認 security default-keychain -s build.keychain 在 import 之前執行 |
| could not find signing certificate | CODE_SIGN_IDENTITY 字串與鑰匙串中 Identity 不完全匹配 |
執行 security find-identity -v -p codesigning build.keychain 複製完整名稱 |
| Provisioning profile doesn't match | 描述檔案過期或 Bundle ID / Capability 不一致 | 在 Apple Developer 後臺重新生成,下載後透過 secrets 或 match 同步 |
匯入成功後,可以用一條命令在 job 日誌裡快速驗證:
security find-identity -v -p codesigning build.keychain | grep Distribution
看到 1 valid identities found 再進入 Archive 步驟,能省掉大量「簽名失敗但不知道卡在哪」的排查時間。
排錯實錄:三個讓我們多 push 了一輪的問題
即使按上述步驟操作,首次接線仍可能遇到以下情況——都是我們真實踩過的坑,附上日誌特徵與解法。
標籤不匹配:job 永遠排隊不執行
現象:GitHub Actions UI 顯示 job 狀態 Queued,Runners 頁面節點卻是 Online。根因是 workflow 裡 runs-on 的標籤與註冊時不完全一致——比如 workflow 寫了 xcode-16.2(點號),註冊時用了 xcode-16-2(連字元)。GitHub 標籤匹配是精確字串比較,差一個字元就不會路由。
解法:在倉庫 Runners 頁面點選節點名稱,查看實際標籤列表,複製貼上到 YAML 裡,不要手打。
launchd 重啟後 Runner Offline
現象:節點 reboot 或 JexMac 側維護重啟後,Runner 顯示 Offline,需手動 SSH 執行 ./svc.sh start。根因是 launchd plist 裡的 UserName 與 SSH 登入使用者不一致,或該使用者從未完成首次圖形/session 登入。
解法:確認 ./svc.sh install 與 ./svc.sh start 在同一使用者下執行;重啟後檢查 ./svc.sh status。若仍失敗,查看 /Library/Logs/GitHubActionsRunner/ 下的 stderr 日誌。
DerivedData 權限衝突
現象:第二次建置報 Unable to write to DerivedData 或某些 .o 檔案 permission denied。根因是第一次 job 以 root 或不同使用者生成了 DerivedData 目錄,後續 job 無寫權限。
解法:統一 DerivedData 路徑並在 workflow 開頭加清理步驟:rm -rf ~/ci-runner/DerivedData && mkdir -p ~/ci-runner/DerivedData。或者在每次 Archive 前只清理對應專案的 hash 子目錄,保留可複用的編譯快取。
沒有常駐 Mac 時:按建置節奏租用專屬節點
走到這裡,你可能已經意識到:GitHub Actions 自託管 Runner 的技術門檻並不高,真正的瓶頸是有沒有一臺 24 小時線上、Xcode 版本可控的 macOS 實體機。本地 MacBook 不適合做 CI 主機——8 GB 記憶體機型跑 Archive 時風扇拉滿還頻繁 swap;公司採購 Mac mini 則要面對固定資產審批、機房託管和證書輪換時的現場維護。
我們的做法是:在迭代衝刺期(例如發版前兩週)租用 JexMac 獨享 Mac mini M4 節點充當 Runner 主機,上線穩定後按周續租或釋放。標準配置 16 GB 統一記憶體、256 GB NVMe、1 Gbps 獨享頻寬,按天 $21.5 起,付款後 1–5 分鐘交付 SSH / VNC 接入,無合同鎖定。五處節點——新加坡、日本(東京)、韓國(首爾)、中國香港、美國東部——可按團隊地理位置選擇,降低 git fetch 與 CocoaPods Specs 同步延遲。
與 GitHub 託管 macOS Runner 對比:高峰排隊 10–20 分鐘 vs 自託管 25 秒內開工;macOS 分鐘按 10 倍權重計費 vs 固定日租成本可預測。與自購硬體對比:零 upfront 投入,版本升級時換標籤或重灌 Xcode 即可,不必等採購週期。
同一臺節點還可以兼做遠端開發桌面——瀏覽器 VNC 連上去除錯 UI、Instruments 抓效能,CI 空閒視窗不浪費。對於兩三人的小團隊,「一臺 M4 實體機 = Runner + 遠端 Mac 開發環境」往往比分開買託管 Runner 分鐘和本地升級記憶體更划算。
把 Runner 接到你的 M4 節點上
文中全部命令已在 JexMac 獨享 Mac mini M4 實體節點驗證。開通 → SSH 接入 → 按步驟註冊 Runner,最快一小時內跑通 Archive。按天起租,衝刺結束可釋放,無年約捆綁。