工程實務

雲端 Mac CI 的 iOS 開源授權清單門禁

雲端 Mac CI 的 iOS 開源授權清單門禁

團隊在升級相依套件後順利完成封存,卻直到發佈前才發現新元件未納入開源授權清單。問題通常不在編譯,而是相依套件的來源分散:SwiftPM 有自己的鎖定檔,CocoaPods 使用另一套目錄,儲存庫中還可能直接複製原始碼或加入二進位套件。將檢查放到雲端 Mac CI,可以讓同一份專案在固定工作區內產生清單、比對政策並保存證據,而不是由發佈負責人在最後一刻臨時翻查目錄。

先定義門禁必須回答的問題

一套可執行的門禁至少要回答四件事:本次建置實際解析了哪些元件、使用哪個版本或提交、授權全文位於何處,以及該條件是否已獲核准。不要只搜尋檔名就直接放行,因為存在 LICENSE 不代表內容沒有變更,也不代表該元件確實已連結至目前的目標。

建議將結果拆分為機器可讀清單與人工審查報告。機器清單使用 JSON,記錄元件名稱、來源、鎖定版本、授權類型、文字雜湊與發現路徑;人工報告使用 Markdown,列出新增、刪除及變更項目。兩者都應保留為管線產出物。

門禁檢查的是相依套件事實是否符合既定政策。發現未知項目時,應停止發佈並要求複核,而不是根據授權名稱自動猜測處理方式。

從建置結果盤點實際相依套件

先讓 Xcode 完成相依套件解析,再讀取工作區實際使用的目錄。如此可避免掃描到開發人員機器上的舊快取。每項工作都應使用獨立的 DerivedData,並在工作結束後清理。

set -euo pipefail

ROOT="$(pwd)"
OUT="$ROOT/build/license-audit"
DD="$ROOT/build/DerivedData"
mkdir -p "$OUT"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -resolvePackageDependencies \
  -derivedDataPath "$DD"

find "$DD/SourcePackages/checkouts" \
  -maxdepth 3 \
  -type f \
  \( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
  -print0 | sort -z > "$OUT/swiftpm-license-files.bin"

find "$ROOT/Pods" \
  -maxdepth 4 \
  -type f \
  \( -iname 'license*' -o -iname 'copying*' -o -iname 'notice*' \) \
  -print0 | sort -z > "$OUT/pods-license-files.bin"

二進位相依套件不能只看副檔名。還應同時登記下載網址對應的內部元件識別碼、總和檢查碼、版本,以及隨套件提供的聲明。對於複製到儲存庫內的原始碼,則應維護一份 compliance/manual-components.json,明確標示目錄擁有者,避免每次掃描都依賴模糊比對。

鎖定檔也必須一併納入

將 Package.resolved、Podfile.lock 與人工元件清單複製到報告目錄。門禁必須驗證清單中的每個元件都能在某個鎖定來源中找到;反向也要確認鎖定檔中的直接相依套件沒有從報告消失。若專案包含多個工作區,應逐一解析,不能只檢查主應用程式。

正規化授權文字並計算指紋

授權文字經常只在線尾字元或行尾空白上有所差異。可先統一為 UTF-8、LF 換行,並移除行尾空白,再計算 SHA-256。不要刪除正文段落、著作權行或年份,否則真正的變更可能會被掩蓋。

python3 - <<'PY'
from pathlib import Path
import hashlib
import json

root = Path("build/license-audit/licenses")
rows = []
for path in sorted(root.rglob("*")):
    if not path.is_file():
        continue
    text = path.read_text(encoding="utf-8", errors="strict")
    normalized = "\n".join(line.rstrip() for line in text.splitlines()).strip() + "\n"
    digest = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
    rows.append({"path": str(path), "sha256": digest})
Path("build/license-audit/license-hashes.json").write_text(
    json.dumps(rows, ensure_ascii=False, indent=2) + "\n",
    encoding="utf-8"
)
PY

授權類型識別與文字指紋必須分開保存。類型用於比對政策,指紋則用於發現上游未明示的修改。無法可靠識別時應標記為 UNKNOWN,不得因檔名看起來熟悉就自動歸類。

使用政策檔控制變更

政策檔應納入版本控制,並透過明確的程式碼審查流程修改。可以依授權識別碼設定 allow、review 和 deny,同時為特定元件記錄已核准的文字雜湊。建議依下列方式判定檢查結果:

情況 CI 結果 後續動作
已知元件,版本與文字未變 通過 保存報告
新版本,授權指紋未變 複核 確認元件變更
新元件或未知授權 阻擋 補充來源與核准紀錄
現有元件的授權文字變更 阻擋 比對全文差異
鎖定檔有元件但報告缺漏 阻擋 修正發現規則

允許清單不應只記錄寬泛的授權類型。元件可能附帶額外的 NOTICE、雙重授權或獨立的資源授權,因此報告必須保留所有相關檔案。對於二進位套件,還要驗證目前下載項目的總和檢查碼與核准紀錄一致。

將報告接入合併與發佈流程

日常合併請求可以執行相依套件解析、差異檢查與報告產生;發佈工作則進一步核對封存內容,確認最終產出物對應同一份鎖定檔。建議將基準放在 compliance/license-baseline.json,CI 只比對結構化欄位,不比較終端機的彩色輸出。

門禁失敗時,日誌應直接列出元件、舊值、新值與證據路徑,避免只回傳「授權檢查失敗」。報告目錄至少應包含鎖定檔副本、正規化文字、雜湊清單、政策版本與差異摘要。ArmCLI 上的並行工作應各自使用獨立工作目錄,避免某項工作更新相依套件後,污染另一項工作的掃描結果。

上線前檢查項目

發佈前應確認相依套件解析未使用未登記的本機路徑;所有直接與遞移相依套件都有版本來源;未知授權數量為零;人工元件清單中的目錄仍然存在;報告與封存使用相同提交;CI 產出物不含存取權杖、私有下載網址或未遮罩的環境變數。

這套流程的價值不只是產生一頁聲明,而是將相依套件變更轉化為可審查的工程事件。只要鎖定檔、授權指紋與政策基準保持同步,團隊就能在相依套件進入發佈分支時發現問題,而不是等封存完成後才重新追查元件來源。

常見問題

只檢查 Package.resolved 就足夠嗎?

不夠。它能確認 SwiftPM 固定版本,但無法證明授權全文已找到,也不涵蓋 CocoaPods、直接複製的原始碼與二進位相依套件。

哪些變更應該阻擋發佈建置?

新相依套件缺少授權檔、授權條件不在允許清單、掃描結果與鎖定檔不一致,或既有授權全文變更時,都應先阻擋並人工複核。

ArmCLI 雲端 Mac

為下一次建置準備獨享實體 Mac

依任務選擇機型、機房與租用期間。節點全年正常運作,實際可用狀態以控制台即時回傳為準。