同一個提交昨天還能順利通過,今天卻解析到不同的間接相依套件,是雲端 Mac 建置中最難追查責任的一類問題。表面上的故障可能只是編譯錯誤,真正改變的卻可能是套件修訂、下載內容,或新增的指令碼階段。處理這類問題不能只靠「重新執行」,而是要讓每次建置都能回答三個問題:解析了哪些項目、內容是否改變,以及變更是否經過審查。
先定義需要保留的證據
相依套件稽核至少要涵蓋 SwiftPM 與 CocoaPods 的直接及間接相依套件。建議將證據分為三層:
| 層級 | 證據 | 解決的問題 |
|---|---|---|
| 宣告 | Package.swift、Podfile、Xcode 專案參照 |
團隊預期引入什麼 |
| 解析 | Package.resolved、Podfile.lock |
實際鎖定到哪個版本或修訂 |
| 建置 | 物料清單、檔案雜湊、建置日誌 | 本次實際使用了什麼 |
Package.resolved 與 Podfile.lock 必須納入版本庫。不要在 CI 中忽略它們,也不要讓流水線自動更新後繼續發布。
鎖定檔並不代表安全結論。它只是穩定的輸入;只有審查、雜湊與差異記錄,才能說明這份輸入為何可以進入建置流程。
ZoomMini 的獨享節點可以持續保留工作目錄,但稽核仍應以版本庫內的檔案為準,不能將某台機器快取中的現況當成唯一證據。
固定解析目錄並執行乾淨解析
先將 SwiftPM 下載目錄固定在工作區中,避免不同 runner 使用無法比較的全域快取。對包含 workspace 的專案執行:
set -euo pipefail
ROOT="$(pwd)"
SPM_DIR="$ROOT/.audit/spm"
DERIVED_DIR="$ROOT/.audit/DerivedData"
rm -rf "$SPM_DIR" "$DERIVED_DIR"
mkdir -p "$SPM_DIR" "$DERIVED_DIR"
xcodebuild \
-resolvePackageDependencies \
-workspace App.xcworkspace \
-scheme App \
-clonedSourcePackagesDirPath "$SPM_DIR"
xcodebuild \
-workspace App.xcworkspace \
-scheme App \
-configuration Release \
-derivedDataPath "$DERIVED_DIR" \
-clonedSourcePackagesDirPath "$SPM_DIR" \
build
如果專案只有 .xcodeproj,請將 -workspace 改為 -project。解析完成後,立即檢查版本庫是否遭到修改:
git diff --exit-code -- '**/Package.resolved' Podfile.lock
這個命令失敗時應停止建置。常見原因包括開發者更新相依套件後漏交鎖定檔、不同工具版本重寫檔案格式,或相依條件允許解析到新的間接版本。
記錄工具鏈邊界
稽核記錄還應包含 xcodebuild -version、swift --version、ruby --version 與相依套件管理工具的版本。這些資訊不一定要寫入物料清單,但必須隨建置日誌一併封存。否則,即使鎖定檔相同但解析行為不同,也很難判斷差異究竟來自工具鏈還是原始碼。
從鎖定檔產生物料清單
以下指令碼會走訪版本庫中的 Package.resolved,相容常見的頂層 pins 與 object.pins 結構,並記錄版本、分支、修訂及鎖定檔雜湊:
import glob
import hashlib
import json
import os
items = []
for path in glob.glob("**/Package.resolved", recursive=True):
if "/.audit/" in f"/{path}":
continue
with open(path, "rb") as handle:
raw = handle.read()
data = json.loads(raw)
pins = data.get("pins") or data.get("object", {}).get("pins", [])
packages = []
for pin in pins:
state = pin.get("state", {})
packages.append({
"identity": pin.get("identity") or pin.get("package"),
"location": pin.get("location") or pin.get("repositoryURL"),
"version": state.get("version"),
"branch": state.get("branch"),
"revision": state.get("revision")
})
items.append({
"file": path,
"sha256": hashlib.sha256(raw).hexdigest(),
"packages": sorted(packages, key=lambda item: item["identity"] or "")
})
for path in glob.glob("**/Podfile.lock", recursive=True):
with open(path, "rb") as handle:
raw = handle.read()
items.append({
"file": path,
"sha256": hashlib.sha256(raw).hexdigest()
})
os.makedirs(".audit", exist_ok=True)
with open(".audit/dependency-manifest.json", "w") as handle:
json.dump({"artifacts": items}, handle, indent=2, sort_keys=True)
執行後,將 dependency-manifest.json 儲存為建置產物。如果團隊決定將它提交到版本庫,就必須在更新相依套件的提交中同步更新,不能由發布工作暗中覆寫。
將差異檢查設為發布門禁
門禁不應單純禁止所有變更,而應要求每項變更都有合理說明。可採用以下執行順序:
- 檢查宣告檔與鎖定檔是否同時變更。
- 清空隔離的相依套件目錄並重新解析。
- 確認解析程序沒有重寫鎖定檔。
- 產生新的物料清單並與基準版本比較。
- 檢查新增相依套件的授權條款、維護來源與指令碼執行能力。
- 完成編譯與測試後,保存清單、鎖定檔雜湊及工具鏈版本。
可以先用標準差異工具建立自動化門禁:
python3 tools/dependency_manifest.py
diff -u audit-baseline/dependency-manifest.json .audit/dependency-manifest.json
允許更新時,不要直接關閉門禁。應在獨立分支中提交新的基準版本,並要求審查者重點檢查新增的版本庫位址、從版本切換到分支、修訂雜湊變更,以及沒有版本號的相依套件。
特別檢查指令碼階段
相依套件可能透過建置階段執行 shell 指令碼。審查 Xcode 專案與產生的相依套件專案時,應確認指令碼是否存取網路、讀取工作區以外的憑證、修改原始碼目錄,或將環境變數寫入日誌。CI 帳戶只應取得完成簽出、解析與建置所需的最低權限。
異常處理與交付檢查項目
如果發現鎖定檔未變,但物料清單雜湊發生變化,應先停止發布,並保留工作區、解析日誌與下載目錄。接著確認檔案是否遭到重新產生、相依套件是否使用可變分支、內部映像內容是否被替換,以及解析工具版本是否改變。不要先清除快取,否則會失去最有價值的現場資訊。
日常交付前可固定檢查以下項目:
- 宣告檔、鎖定檔與物料清單屬於同一個提交。
- SwiftPM 相依套件都有明確版本或不可變修訂。
- CocoaPods 解析結果與
Podfile.lock一致。 - 新增的相依套件已完成授權條款與指令碼階段審查。
- 乾淨解析不會產生 Git 差異。
- 建置產物已關聯清單雜湊與工具鏈版本。
- CI 未向相依套件指令碼暴露無關的權杖或私鑰。
相依套件供應鏈稽核的目的,不是製作一份更長的清單,而是建立可重複驗證的判斷機制:輸入不變時,結果可以驗證;輸入改變時,發布流程會停止;審查者也能準確看出變更發生在哪裡。
常見問題
已提交 Package.resolved 與 Podfile.lock,還需要依賴物料清單嗎?
需要。鎖定檔固定解析結果,物料清單則統一整理依賴名稱、版本、修訂、來源欄位與檔案雜湊,方便跨建置比較及保存證據。
依賴門禁應該在完整 Xcode 建置之前還是之後執行?
應先完成乾淨解析與鎖定檔檢查,再開始完整建置,讓未提交的解析差異在執行專案腳本階段前被發現。
間接依賴的修訂突然改變時該怎麼處理?
先暫停發布,追查上游依賴鏈並審核變更與授權條款,再於獨立分支重新解析及測試,通過後一併提交鎖定檔與稽核紀錄。
選擇獨享實體節點,滿足建置、測試與實驗需求
兩檔 M4 配置涵蓋新加坡、東京、首爾與香港節點。實際可用狀態以下單時回傳的結果為準。