雲端 Mac 的 iOS 依賴供應鏈稽核實戰

雲端 Mac 的 iOS 依賴供應鏈稽核實戰

同一個提交昨天還能順利通過,今天卻解析到不同的間接相依套件,是雲端 Mac 建置中最難追查責任的一類問題。表面上的故障可能只是編譯錯誤,真正改變的卻可能是套件修訂、下載內容,或新增的指令碼階段。處理這類問題不能只靠「重新執行」,而是要讓每次建置都能回答三個問題:解析了哪些項目、內容是否改變,以及變更是否經過審查。

先定義需要保留的證據

相依套件稽核至少要涵蓋 SwiftPM 與 CocoaPods 的直接及間接相依套件。建議將證據分為三層:

層級 證據 解決的問題
宣告 Package.swiftPodfile、Xcode 專案參照 團隊預期引入什麼
解析 Package.resolvedPodfile.lock 實際鎖定到哪個版本或修訂
建置 物料清單、檔案雜湊、建置日誌 本次實際使用了什麼

Package.resolvedPodfile.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 -versionswift --versionruby --version 與相依套件管理工具的版本。這些資訊不一定要寫入物料清單,但必須隨建置日誌一併封存。否則,即使鎖定檔相同但解析行為不同,也很難判斷差異究竟來自工具鏈還是原始碼。

從鎖定檔產生物料清單

以下指令碼會走訪版本庫中的 Package.resolved,相容常見的頂層 pinsobject.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 儲存為建置產物。如果團隊決定將它提交到版本庫,就必須在更新相依套件的提交中同步更新,不能由發布工作暗中覆寫。

將差異檢查設為發布門禁

門禁不應單純禁止所有變更,而應要求每項變更都有合理說明。可採用以下執行順序:

  1. 檢查宣告檔與鎖定檔是否同時變更。
  2. 清空隔離的相依套件目錄並重新解析。
  3. 確認解析程序沒有重寫鎖定檔。
  4. 產生新的物料清單並與基準版本比較。
  5. 檢查新增相依套件的授權條款、維護來源與指令碼執行能力。
  6. 完成編譯與測試後,保存清單、鎖定檔雜湊及工具鏈版本。

可以先用標準差異工具建立自動化門禁:

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 建置之前還是之後執行?

應先完成乾淨解析與鎖定檔檢查,再開始完整建置,讓未提交的解析差異在執行專案腳本階段前被發現。

間接依賴的修訂突然改變時該怎麼處理?

先暫停發布,追查上游依賴鏈並審核變更與授權條款,再於獨立分支重新解析及測試,通過後一併提交鎖定檔與稽核紀錄。

ZoomMini 雲端 Mac

選擇獨享實體節點,滿足建置、測試與實驗需求

兩檔 M4 配置涵蓋新加坡、東京、首爾與香港節點。實際可用狀態以下單時回傳的結果為準。

選擇機型並訂購