同一套 iOS 工程在開發機上可能可以正常複製,移到雲端 Mac CI 後卻卡在子模組:主儲存庫已完成檢出,私有元件卻回報權限錯誤;巢狀相依項目停留在舊提交;或淺層複製找不到指定物件。問題通常不在 Xcode,而是檢出流程沒有明確區分「主儲存庫目前所在的分支」與「主儲存庫記錄的子模組提交」。
先確認可重現範圍
Git Submodule 在主儲存庫中保存的是路徑、遠端位址,以及一個精確提交。可重現建置的目標,是讓相同的主儲存庫提交永遠取得相同的子模組物件,而不是每次都追蹤子模組分支的最新狀態。
先在開發環境記錄基準:
git submodule status --recursive
git config --file .gitmodules --get-regexp 'submodule\..*\.path'
git config --file .gitmodules --get-regexp 'submodule\..*\.url'
狀態列開頭的 - 表示尚未初始化,+ 表示工作區中的提交與主儲存庫記錄不一致,U 則表示存在合併衝突。進入 CI 後,這三種狀態都不應視為正常結果。
建置工作不要執行
git submodule update --remote。這個命令會跟隨設定的分支移動,導致同一個主儲存庫提交在不同時間取得不同的相依項目。
相對位址適合同一程式碼代管邊界內的儲存庫遷移,但解析時會以主儲存庫的遠端位址為基準。如果工作暫時改寫了 origin,應在初始化時執行 git submodule sync --recursive,並輸出經過遮罩處理的實際位址,以核對解析結果。
建立統一的檢出入口
不要混用 clone --recurse-submodules、手動進入目錄拉取,以及建置指令碼中的補救命令。建議讓 CI 先取得主儲存庫,再透過單一入口處理所有層級:
set -euo pipefail
git submodule sync --recursive
git submodule update --init --recursive --jobs 4
expected_file="$(mktemp)"
actual_file="$(mktemp)"
trap 'rm -f "$expected_file" "$actual_file"' EXIT
git ls-tree -r HEAD |
awk '$2 == "commit" { print $3, $4 }' |
sort > "$expected_file"
git submodule status --recursive |
sed -E 's/^[ +\-U]([0-9a-f]+) ([^ ]+).*/\1 \2/' |
sort > "$actual_file"
diff -u "$expected_file" "$actual_file"
這裡的 --jobs 4 只用來限制平行網路請求數量,並不是固定的效能結論。不同節點的網路狀況、遠端限流規則與子模組數量各異,應先從較低的平行數開始,再依失敗率調整。
指令碼也應在建置前執行 git diff --submodule=log --exit-code。如果某個安裝步驟暗中修改了子模組工作區,工作應立即失敗,而不是帶著未知的原始碼繼續封存。
謹慎使用淺層複製
淺層複製可以減少傳輸量,卻也是「遠端明明存在該提交,CI 卻找不到」的常見原因。主儲存庫記錄的子模組物件可能早於目前的淺層邊界,也可能只能透過已刪除分支的歷史記錄存取。
| 情境 | 建議策略 | 失敗時的處理方式 |
|---|---|---|
| 子模組提交接近分支頂端 | 使用有限深度檢出 | 增加深度後重試 |
| 歷史跨度不固定 | 完整檢出子模組 | 不依賴固定深度進行猜測 |
| 多層巢狀子模組 | 逐層驗證提交 | 輸出失敗路徑與物件雜湊 |
| 遠端物件已無法存取 | 修正儲存庫參照 | 不以切換分支掩蓋問題 |
如果必須保留淺層複製,可以先嘗試:
git submodule update --init --recursive --depth 50
失敗後不要直接改成 --remote。應進入發生錯誤的路徑,執行 git fetch --deepen=100,再以 git cat-file -e <commit>^{commit} 驗證。若物件仍不存在,代表需要檢查遠端物件的保留狀況或主儲存庫記錄,而不是繼續增加隨機重試。
隔離私有子模組憑證
不同子模組可能分屬不同的權限邊界。最穩妥的做法,是為每項工作建立暫時性的 Git 設定,只在處理程序生命週期內傳入,且不修改節點的全域設定。
git_config="$(mktemp)"
chmod 600 "$git_config"
trap 'rm -f "$git_config"' EXIT
git config --file "$git_config" credential.helper ""
git config --file "$git_config" protocol.version 2
GIT_CONFIG_GLOBAL="$git_config" \
git submodule update --init --recursive
驗證值應由 CI 的受保護變數或短期檔案提供,不可寫入 .gitmodules、遠端位址、命令參數或建置日誌。工作結束後,至少應檢查:
git config --global --list
git remote -v
git submodule foreach --recursive 'git remote -v'
檢查輸出前必須先遮罩敏感資訊。如果憑證曾經被拼接到 URL 中,只刪除暫存檔案仍不夠;還必須還原遠端位址,並檢查 Shell 歷史記錄、診斷套件及快取目錄。
將失敗資訊轉換成可定位的證據
子模組失敗時,不能只保留一句「結束代碼 128」。建議記錄主儲存庫提交、失敗路徑、預期物件、實際物件、解析後的主機名稱、檢出深度與失敗階段,但不要記錄使用者名稱、權杖或完整的驗證位址。
可將疑難排解順序固定為:
- 使用
git ls-tree HEAD <path>確認主儲存庫預期的物件。 - 使用
.gitmodules與本機 Git 設定確認最終位址。 - 使用
git cat-file -e判斷物件是否已存在於本機。 - 使用唯讀驗證確認遠端存取權限。
- 檢查淺層複製邊界與巢狀子模組狀態。
- 清理工作區後,再次執行同一套檢出指令碼。
最後,將 git submodule status --recursive 作為建置中繼資料封存。它的資料量很小,卻能明確回答這次建置究竟使用了哪些相依提交。SDKMac 節點上的工作也應遵循同一原則:先固定原始碼輸入,再啟動 Xcode 或其他工具鏈,讓檢出故障與編譯故障維持清楚的界線。
常見問題
Submodule 已鎖定提交,為什麼 CI 仍然無法檢出?
主儲存庫只記錄子模組提交,並不保證遠端仍可提供該物件。需要檢查位址、憑證權限、複製深度與遠端物件是否存在。
CI 應該使用 git submodule update --remote 嗎?
可重現建置不應使用。這個參數會追蹤設定的分支,使相同主儲存庫提交在不同時間取得不同依賴。
如何避免子模組憑證留在雲端 Mac 節點?
將憑證限制在工作專屬的暫存設定或檔案,使用 trap 在結束時刪除,再檢查全域 Git 設定、遠端位址與日誌沒有敏感資訊。
為持續整合選擇一台雲端 Mac
比較 SDKMac M4 與 SDKMac M4 Pro 的配置、地區和四種計費週期,然後建立訂單。