雲端 Mac CI 工作區權限漂移診斷與治理

雲端 Mac CI 工作區權限漂移診斷與治理

同一個 Xcode 建置工作連續執行數天後,突然開始在寫入 DerivedData、更新相依套件快取或刪除舊產物時回報 Permission denied。重新簽出儲存庫或許能暫時恢復正常,但下一次平行工作又會再次發生。此時不要先懷疑 Xcode,也不要直接放寬目錄權限;更常見的根本原因是某個指令碼更換了執行使用者、繼承了不同的 umask,或在工作區留下額外的 ACL。

先保留失敗現場

權限問題最怕被「清理後重跑」抹去線索。先記錄工作的執行身分、主目錄、目前目錄與預設權限遮罩,再檢查第一個失敗路徑,不必立刻掃描整個磁碟。

printf 'user=%s
' "$(id -un)"
printf 'groups=%s
' "$(id -Gn)"
printf 'home=%s
' "$HOME"
printf 'cwd=%s
' "$PWD"
printf 'umask=%s
' "$(umask)"

target="${FAILED_PATH:?set FAILED_PATH first}"
stat -f 'owner=%Su group=%Sg mode=%Sp path=%N' "$target"
ls -led "$target"

stat 會回答「誰擁有它、基本模式為何」,ls -le 則會顯示 ACL。目錄模式看似允許寫入,不代表實際存取一定會成功:父目錄缺少執行權限、ACL 中存在限制項目,或檔案屬於另一個工作使用者,都可能阻止操作。

只需逐層檢查第一個失敗路徑及其父目錄。對整個工作區遞迴變更權限,不但會破壞現場,也可能讓金鑰、快取與建置產物暴露給無關程序。

區分三類權限漂移

擁有者發生變化

最常見的情況是某個步驟透過權限提升命令安裝相依套件或複製檔案,之後卻把產物留給其他使用者。可先列出不屬於目前工作使用者的內容:

workspace="${WORKSPACE:?set WORKSPACE first}"
find "$workspace" -x ! -user "$(id -un)" -print

如果輸出集中在單一工作目錄,應回頭檢查建立該目錄的指令碼,而不是用遞迴 chown 掩蓋問題來源。CI 工作區最好從建立之初就由工作使用者擁有;即使必須執行高權限操作,也不要將輸出寫回儲存庫、快取或 DerivedData。

ACL 超出預期

Finder 操作、移轉指令碼或複製工具都可能保留 ACL。如果 ls -le 的基本權限行下方出現編號項目,就應確認其來源。只有在確定該目錄是可重建的暫存工作區時,才能針對特定位置移除 ACL:

job_dir="${JOB_DIR:?set JOB_DIR first}"
chmod -RN "$job_dir"

不要對使用者主目錄或憑證目錄執行此命令。修復後再次執行 ls -led,確認 ACL 已消失,且基本模式仍符合需求。

umask 不一致

SSH 互動式工作階段、CI 常駐程式與獨立指令碼不一定會讀取相同的 Shell 設定。如果某個工作以 077 建立快取,後續工作即使屬於相同群組,也可能無法重複使用。與其依賴啟動檔案,不如在工作入口明確宣告:

umask 022
install -d -m 0755 "$JOB_DIR"
install -d -m 0755 "$JOB_DIR/DerivedData"
install -d -m 0755 "$JOB_DIR/Artifacts"

敏感資料應放在獨立目錄並採用更嚴格的模式,不能為了共用建置快取而全面放寬權限。

每個工作使用獨立工作區

當平行工作共用同一個 DerivedData 或封存目錄時,其中一個工作的清理動作,可能會碰上另一個工作正在寫入的檔案。建議使用儲存庫識別碼、提交識別碼與工作編號組成目錄,並將路徑明確傳給建置命令。

run_id="${CI_RUN_ID:?set CI_RUN_ID first}"
root="$HOME/ci-runs/$run_id"
src="$root/source"
derived="$root/DerivedData"
artifacts="$root/Artifacts"

umask 022
install -d -m 0755 "$src" "$derived" "$artifacts"

xcodebuild \
  -workspace App.xcworkspace \
  -scheme App \
  -configuration Release \
  -derivedDataPath "$derived" \
  archive \
  -archivePath "$artifacts/App.xcarchive"

共用快取與工作輸出應彼此分離:相依套件下載快取可以唯讀方式重複使用,DerivedData、封存檔與日誌則應按工作隔離。這樣不僅能減少權限互相污染,也能讓失敗現場在工作結束前保持完整。

路徑類型 建議歸屬 生命週期 是否允許平行寫入
原始碼簽出 單一工作 單次執行
DerivedData 單一工作 單次執行
封存檔與日誌 單一工作 驗收後清理
下載快取 固定工作使用者 跨工作 僅由快取流程更新

在建置前後設定驗收門檻

權限檢查應成為工作入口的一部分,而不是發生錯誤後才手動處理。建置前應確認目錄可寫入、擁有者正確,且沒有非預期的 ACL;建置後則檢查是否產生了由其他使用者擁有的檔案。下列檢查會在發現漂移時直接終止工作:

test -d "$JOB_DIR"
test -w "$JOB_DIR"

unexpected_owner="$(
  find "$JOB_DIR" -x ! -user "$(id -un)" -print -quit
)"

if [ -n "$unexpected_owner" ]; then
  printf 'unexpected owner: %s
' "$unexpected_owner" >&2
  exit 1
fi

if ls -led "$JOB_DIR" | tail -n +2 | grep -q '^[[:space:]]*[0-9]:'; then
  printf 'unexpected ACL on job directory
' >&2
  exit 1
fi

如果團隊需要共用快取,應另外定義快取寫入者、更新時機與原子替換方式,不要讓所有建置工作同時修改同一個目錄。工作完成後的清理,也只能刪除本次 run_id 對應的路徑,並且先驗證該路徑位於預期的根目錄之下。

從指令碼源頭修正問題

一次可靠的修正應能回答四個問題:哪個步驟建立了異常檔案、建立時使用哪個使用者、繼承了什麼 umask,以及為何寫入共用目錄。找到答案後,應將目錄建立方式、執行身分與輸出位置寫進指令碼,而不是依賴人工登入後的環境。

在 RunnerVM 的雲端 Mac 上執行 CI 時,同樣應在工作開始時輸出最精簡的環境快照,並在控制台確認目前可選的設定。機器重新啟動或工作移轉後,只要入口指令碼能重新建立目錄與權限基準,建置就不必依賴上一次執行所遺留的狀態。最終目標不是消除所有權限限制,而是讓每個檔案的建立者、讀寫範圍與清理責任都可以預測。

常見問題

遇到 Permission denied 時可以直接對整個工作區執行 chmod 777 嗎?

不建議。這會掩蓋擁有者與 ACL 問題,也讓其他程序能修改建置檔案。應先用 stat 與 ls -le 檢查失敗路徑,再只修復已確認的工作目錄。

為什麼同一支腳本透過 SSH 執行成功,在 CI 工作中卻失敗?

兩者可能使用不同的執行使用者、HOME、umask、PATH 與啟動環境。應在工作開頭記錄這些值,並於腳本內明確設定權限基準。

Runner M4

讓下一次建置在獨享物理 Mac 上執行

依任務週期選擇雲端 Mac 節點,並在控制台管理訂單、連線資訊與支援工單。

選擇方案並下單