雲端 Mac CI 的乾淨取出驗證:找出未提交檔案與本機依賴

CI/CD 實踐 ·約 7 分鐘閱讀

雲端 Mac CI 的乾淨取出驗證:找出未提交檔案與本機依賴

同一個 Xcode 專案在長期使用的開發目錄中可以正常編譯,複製到新的 VPSGit 雲端 Mac 工作區後,卻出現檔案不存在、找不到指令碼或設定值為空等錯誤。這時不應先搬移舊快取。真正需要驗證的是:一次建置能否只依賴指定的提交、明確宣告的工具鏈,以及由流水線注入的參數完成。

乾淨取出需要隔離哪些狀態

刪除 DerivedData 只能排除一類快取,無法證明專案沒有本機依賴。長期使用的工作區通常還疊加了未提交檔案、使用者目錄設定、全域安裝工具、Shell 初始化指令碼,以及指向儲存庫外部的絕對路徑。

至少需要隔離以下五個層面:

乾淨取出驗收的目的,不是讓建置「再多清理一次」,而是證明任何未納入版本庫、依賴清單或流水線參數的狀態,都不會被悄悄使用。

首次排查時應保留一組對照:原工作區建置成功,隔離工作區建置失敗。兩者必須使用相同的提交、Scheme、Configuration 與 Destination,這樣失敗差異才具有診斷價值。

建立一次性隔離工作區

以下指令碼會從現有儲存庫建立不使用本機硬連結的複製,取出指定提交,並將使用者目錄、暫存目錄與建置快取全部放入一次性目錄。專案名稱與 Scheme 透過參數傳入,避免將環境差異寫死在指令碼中。

#!/bin/bash
set -euo pipefail

SOURCE_REPO="${1:?source repository required}"
REVISION="${2:?revision required}"
PROJECT="${3:?project path required}"
SCHEME="${4:?scheme required}"

ROOT="$(mktemp -d "${TMPDIR:-/tmp}/clean-checkout.XXXXXX")"
trap 'rm -rf "$ROOT"' EXIT

git clone --no-local --no-checkout "$SOURCE_REPO" "$ROOT/repo"
git -C "$ROOT/repo" checkout --detach "$REVISION"
git -C "$ROOT/repo" submodule update --init --recursive

mkdir -p "$ROOT/home" "$ROOT/tmp" "$ROOT/DerivedData"
export HOME="$ROOT/home"
export TMPDIR="$ROOT/tmp"
export DEVELOPER_DIR="/Applications/Xcode.app/Contents/Developer"

cd "$ROOT/repo"
xcodebuild \
  -project "$PROJECT" \
  -scheme "$SCHEME" \
  -configuration Debug \
  -derivedDataPath "$ROOT/DerivedData" \
  CODE_SIGNING_ALLOWED=NO \
  build | tee "$ROOT/build.log"

CODE_SIGNING_ALLOWED=NO 只適合驗證不需要簽署的編譯階段。如果目標必須完成封存或簽署,應由 CI 安全地注入所需材料,不要為了讓指令碼通過而引用個人使用者目錄中的檔案。

讓缺少的依賴儘早暴露

完成複製後,先執行 git status --porcelain,結果應為空。接著檢查子模組狀態與大型檔案物件是否完整。如果專案包含程式碼產生步驟,應明確區分兩類產物:產生器的原始碼與版本必須鎖定;產生結果則依團隊約定提交,或在建置期間以穩定且可重現的方式產生。

不要把 ~/bin、/usr/local/bin 或某位成員的桌面目錄加入補救路徑。輔助工具應放入儲存庫的工具目錄、受版本約束的套件管理清單,或由節點初始化流程安裝固定版本。

從失敗記錄定位隱藏輸入

隔離建置失敗後,應先找出第一個真正的錯誤,而不是只看記錄末尾的摘要。常見訊號可依輸入類型分類:

失敗訊號 常見原因 修正方向
No such file or directory 未提交檔案或絕對路徑 將檔案納入儲存庫,改用相對於儲存庫的路徑
command not found 依賴互動式 Shell 或全域工具 宣告工具版本並明確設定 PATH
設定值為空 參數只存在於本機設定 由 CI 注入,並在缺少時立即失敗
模組在舊目錄中可見 共用快取掩蓋了缺少的依賴 修正依賴宣告,不要複製舊快取
指令碼權限錯誤 可執行位元未納入提交 修正檔案模式並重新提交

可使用以下命令檢查目前提交是否真的包含目標檔案及其權限:

git ls-tree -r HEAD -- path/to/file
git status --short
git diff --summary HEAD

如果錯誤路徑指向 /Users/某個名字/,通常表示專案設定、指令碼或產生檔案記錄了絕對路徑。應回到產生該路徑的來源設定進行修正,而不是在新節點上建立同名目錄。

收緊環境而不製造假失敗

直接使用 env -i 可以快速找出環境依賴,但也可能移除建置所需的 PATH、地區設定與暫存目錄。更穩妥的做法是先記錄目前建置實際讀取的變數,再建立白名單。

建議保留 HOME、TMPDIR、PATH、DEVELOPER_DIR、LANG,以及流水線明確注入的業務參數。每個自訂變數都應加入必填檢查,例如:

: "${API_BASE_URL:?API_BASE_URL must be provided by CI}"
: "${BUILD_FLAVOR:?BUILD_FLAVOR must be provided by CI}"

如此一來,缺少設定時會在指令碼入口立即失敗,不必等到編譯後段才出現語意含糊的錯誤。敏感值只應檢查是否存在,不要輸出到記錄中。建置指令碼也不應讀取互動式 Shell 設定,因為非互動式任務可能根本不會載入這些檔案。

將驗收接入日常流水線

完整的獨立複製會增加磁碟用量與取出時間,因此不必阻擋每一次小型提交。可以拆成兩級:一般任務使用重複利用的工作區提供快速回饋;乾淨取出任務則在合併主要分支、依賴或 Xcode 發生變更,以及發布前執行。

驗收通過必須同時符合以下條件:

  1. 指定提交可以在獨立複製中完整取出。
  2. 工作樹在建置前後都沒有非預期的修改。
  3. 建置不會讀取原工作區或個人使用者目錄。
  4. 所有輔助工具都有可追蹤的版本來源。
  5. 清空 DerivedData 後仍可取得預期產物。
  6. 失敗記錄與環境摘要足以用於重現,但不包含敏感值。

當乾淨取出失敗、一般建置卻成功時,應將其視為依賴宣告缺陷。先修正輸入邊界,再討論快取還原。持續保留這道驗收,能讓新節點接手、平行擴充與工具鏈升級,不再是「換機器碰運氣」,而是可重複執行的工程流程。

常見問題

為什麼不能只執行 git clean 再重新建置?

git clean 只能處理目前工作區中的未追蹤檔案,無法隔離使用者目錄設定、全域工具、共享快取、Keychain 與儲存庫外的絕對路徑依賴。

乾淨取出建置失敗後應先檢查什麼?

先比較失敗命令的輸入路徑與環境變數,再檢查子模組、大型檔案、產生程式碼、設定檔及腳本工具是否已在儲存庫或鎖定清單中宣告。

這項驗證需要每次提交都執行嗎?

不一定。日常流程可保留較短的冒煙檢查,完整隔離驗證則安排在合併主要分支、更新工具鏈或依賴,以及正式交付之前。

獨享實體節點

為下一次建置或推論任務設定雲端 Mac

從三種 Apple Silicon 設定、六個節點與固定租期中選擇,實際可用狀態以控制台即時回傳為準。

設定雲端 Mac