雲端 Mac CI 掛載 SMB 共享儲存:避免建置污染與半成品交付

雲端 Mac CI 掛載 SMB 共享儲存:避免建置污染與半成品交付

CI 節點完成歸檔後,需要把產物交給測試、發佈或製品保留作業。最直覺的做法,是直接將 SMB 共享目錄設為工作區,讓多部雲端 Mac 讀寫同一份工程。這種方式在小型儲存庫中或許暫時運作正常,但只要遇到網路抖動、檔案鎖定延遲或掛載失效,Xcode 的中間檔案就可能損毀,甚至把結果寫入與掛載點同名的本機空目錄。

更穩妥的界線是:SMB 只負責接收輸入快照與最終產物,原始碼展開、相依套件解析、DerivedData 和暫存檔案全部留在本機磁碟。

先劃分三類目錄

每個作業至少需要三個用途互不混用的路徑:

  • 唯讀輸入區:保存原始碼快照或固定版本的相依套件。
  • 本機工作區:承載簽出後的原始碼、DerivedData、測試結果和暫存檔案。
  • 遠端交付區:只保存已完成驗證的歸檔、日誌摘要與清單。

不要讓兩個並行作業共用 DerivedData。即使它們建置的是同一個提交,索引、模組快取和建置資料庫仍可能同時被改寫。建議把作業識別碼加入路徑,例如 ~/ci-work/$RUN_ID,並在作業結束後依保留政策清理。

網路共享應視為交付界線,而不是本機磁碟的透明替代品。建置成功與上傳成功也必須是兩個彼此獨立的狀態。

由常駐程序管理 SMB 掛載

無人值守作業不應在命令列參數中拼接使用者名稱和密碼。較合適的方式,是由受控的常駐程序預先掛載 SMB,並從獨立的鑰匙圈或受限設定中讀取憑證;一般建置作業只取得目標目錄所需的最低寫入權限。

作業開始時不能只檢查目錄是否存在。SMB 中斷連線後,掛載點仍可能退化為一般目錄。至少要檢查檔案系統類型、可寫入性,以及一次小型檔案的往返讀寫:

set -euo pipefail

SHARE_ROOT="/Volumes/ci-artifacts"
PROBE="$SHARE_ROOT/.probe-${RUN_ID}"

fs_type="$(stat -f '%T' "$SHARE_ROOT")"
test "$fs_type" = "smbfs"
test -w "$SHARE_ROOT"

printf '%s\n' "$RUN_ID" > "$PROBE"
test "$(cat "$PROBE")" = "$RUN_ID"
rm -f "$PROBE"

任何一步失敗都應終止作業。不要自動建立掛載點後繼續執行,否則一次共享連線中斷,就可能變成「建置成功,但產物只留在本機」。

將建置活動限制在本機磁碟

先把固定提交同步到本機暫存目錄,再執行相依套件解析與建置。作業目錄、DerivedData 和結果套件都應包含唯一的作業識別碼,避免並行作業彼此覆寫。

LOCAL_ROOT="$(mktemp -d "$TMPDIR/vmcache-ci.XXXXXX")"
trap 'rm -rf "$LOCAL_ROOT"' EXIT

rsync -a --delete "$SOURCE_SNAPSHOT/" "$LOCAL_ROOT/repo/"

xcodebuild \
  -workspace "$LOCAL_ROOT/repo/App.xcworkspace" \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$LOCAL_ROOT/DerivedData" \
  -resultBundlePath "$LOCAL_ROOT/TestResults.xcresult" \
  build

這裡的 SOURCE_SNAPSHOT 應對應不可變的提交,而不是仍會被其他作業持續修改的作用中目錄。若輸入來自共享磁碟區,可以先核對提交編號或清單,再開始同步。

不要快取整個 DerivedData

在不同作業之間複製完整的 DerivedData 往往得不償失:快取體積龐大,內部路徑和建置設定也可能改變。更容易維護的做法,是只快取明確可重複使用的相依套件下載目錄,並將 Xcode 版本、架構和鎖定檔摘要寫入快取鍵。快取鍵不相符時就重新產生,不要進行模糊比對後重複使用。

使用暫存名稱完成原子交付

直接複製成正式檔名會讓半成品暴露給下游作業。下游作業可能在檔案出現後立即讀取,但此時資料仍在傳輸。正確順序是:在本機封裝、在本機計算摘要、以暫存名稱複製到共享磁碟區、在共享磁碟區完成驗證,最後再於同一目錄中改名。

ARTIFACT="$LOCAL_ROOT/App-release.zip"
ditto -c -k --norsrc "$LOCAL_ROOT/DerivedData/Build/Products/Release" "$ARTIFACT"

REMOTE_DIR="$SHARE_ROOT/releases/$GIT_COMMIT"
REMOTE_TMP="$REMOTE_DIR/.App-release.zip.${RUN_ID}.partial"
REMOTE_FINAL="$REMOTE_DIR/App-release.zip"

mkdir -p "$REMOTE_DIR"
cp "$ARTIFACT" "$REMOTE_TMP"

local_hash="$(shasum -a 256 "$ARTIFACT" | awk '{print $1}')"
remote_hash="$(shasum -a 256 "$REMOTE_TMP" | awk '{print $1}')"
test "$local_hash" = "$remote_hash"

mv "$REMOTE_TMP" "$REMOTE_FINAL"
printf '%s  %s\n' "$remote_hash" "App-release.zip" \
  > "$REMOTE_DIR/SHA256SUMS.${RUN_ID}"

原子改名的前提,是暫存檔案與正式檔案位於同一個共享磁碟區,最好也位於同一目錄。不要先寫入本機,再跨磁碟區執行 mv,因為跨磁碟區操作會退化為複製後刪除。

讓失敗可判斷也可清理

上傳失敗後,應在受控的時間範圍內保留本機建置結果,以便重新傳輸,但不能把作業標記為已完整交付。建議分別記錄 build_completedartifact_verifiedpublish_completed,下游只接受最後一個狀態。

還要定期清理超過保留期限的 .partial 檔案。清理作業必須確認檔名格式和修改時間,只處理暫存後綴,不掃描或刪除正式產物。若共享磁碟區變成唯讀、校驗結果不一致或連線反覆中斷,應停止發佈佇列,優先保全本機結果和掛載診斷資訊。

在 VMCache 的雲端 Mac 上實作這套流程時,關鍵不在某一條掛載命令,而在於界線清楚:網路磁碟區負責交換,本機磁碟負責建置,正式檔名只代表已完成驗證的產物。如此一來,即使 SMB 短暫異常,建置現場與交付狀態也不會混在一起。

常見問題

可以把 DerivedData 直接放在 SMB 共享目錄嗎?

不建議。DerivedData 會產生大量小檔案、鎖定與中繼資料更新,容易受網路延遲影響。應為每個工作建立獨立的本機目錄。

如何避免下游工作讀到尚未複製完成的產物?

先以暫存名稱寫入共享磁碟,再產生校驗和與完成清單。確認內容無誤後,在同一目錄內改成正式名稱,下游只接受正式名稱與配對清單。

掛載路徑存在時是否代表 SMB 一定可用?

不代表。工作開始前仍要檢查檔案系統類型、寫入權限及小檔案往返讀取。任何檢查失敗都應立即停止,避免誤寫到同名的本機空目錄。

獨享實體節點

為建置、測試與 MLX 推論選擇雲端 Mac

依實際任務選擇 M4、記憶體、儲存空間、節點與租用期限。每份租用方案皆對應獨享實體機,並非虛擬機。

選擇租用方案