雲端 Mac CI 的 Git 物件庫損壞診斷與安全復原

雲端 Mac CI 的 Git 物件庫損壞診斷與安全復原

若流水線在 checkout 後突然出現 bad objectmissing blobpack checksum mismatch,重新執行一次有時會恢復,也可能導致更多工作同時失敗。這類問題通常不是業務程式碼出錯,而是工作區、物件快取或 Git 封裝檔已經不一致。正確的處理順序不是立即清除快取,而是先停止寫入並保存現場,再判斷損壞發生在單一儲存庫還是共用層。

先區分網路失敗與物件損壞

網路失敗多半發生在擷取階段,記錄通常會包含連線中斷、遠端提前關閉或傳輸未完成等訊息。物件損壞則常在簽出、合併、讀取歷史記錄或產生封存檔時出現,並附帶具體的物件雜湊。應先記錄失敗的命令、退出碼、提交編號、工作區路徑及快取代次,不要只保留流水線記錄的最後幾十行。

可以先執行唯讀檢查:

git status --porcelain=v2
git rev-parse --show-toplevel
git rev-parse HEAD
git fsck --full --strict --no-dangling

git fsck 回報 missing blob,表示某個可由參照到達的檔案物件不存在;missing tree 往往會阻止目錄簽出;invalid sha1 pointer 則表示參照指向無法讀取的物件。若只看到 dangling 物件,不應直接判定儲存庫已損壞,因為它們可能來自重定基底、壓縮或遭替換的提交。

診斷階段的目標是確認損壞範圍,而不是讓目前的命令暫時恢復成功。任何會重寫物件庫的操作,都應延後到現場保存完成後再執行。

凍結工作區並保存證據

先暫停所有會寫入同一目錄的 runner、排程擷取及快取更新工作。不要在原目錄中執行 git gcgit prune、重新封裝或遞迴刪除。這些操作可能改變物件配置,使最初的異常無法重現。

至少應保存以下內容:完整的工作記錄、.git/HEAD.git/config.git/packed-refs.git/refs.git/logs、目前狀態及尚未提交的差異。原始碼工作區可能包含未追蹤檔案,封裝前應先檢視清單,避免將憑證或大型建置產物一併封存。

incident="$HOME/git-incidents/$(date +%Y%m%d-%H%M%S)"
mkdir -p "$incident"
git status --porcelain=v2 > "$incident/status.txt"
git show-ref --head > "$incident/refs.txt"
git reflog show --all --date=iso > "$incident/reflog.txt"
git diff --binary > "$incident/worktree.patch"
git diff --cached --binary > "$incident/index.patch"
git fsck --full --strict > "$incident/fsck.txt" 2>&1 || true

若工作區可由 CI 隨時重建,最穩妥的方式是將整個目錄重新命名並隔離,而不是直接在原位置修補。隔離後的目錄應設為唯讀,並記錄其對應工作及快取來源。

定位鬆散物件、封裝檔與共用快取

Git 物件可能以鬆散檔案存在,也可能收錄在 .pack 中。若錯誤訊息提供了雜湊,可以先查詢物件類型與大小:

object="0123456789abcdef0123456789abcdef01234567"
git cat-file -t "$object"
git cat-file -s "$object"

若命令失敗,請檢查 .git/objects/${object:0:2}/${object:2} 是否存在。檔案存在但無法讀取,通常表示內容遭截斷、校驗不符,或底層複製作業未完整完成。不要從其他未確認的來源複製同名檔案並覆蓋現場。

對於封裝檔,應逐一驗證其索引:

for index in .git/objects/pack/*.idx; do
  git verify-pack -v "$index" >/dev/null || printf '%s\n' "$index"
done

若儲存庫設定了 alternates,還應檢查 .git/objects/info/alternates。當多個工作共用同一個可寫入的物件目錄時,一次中斷的寫入就可能影響所有參照該目錄的工作區。此時,只重新複製某個儲存庫並不能排除根本原因,必須停止發布該快取代次。

安全檢查損壞是否可重現

應先複製隔離目錄,再於副本中重現檢查,同時記錄檔案系統的剩餘空間、程序退出方式,以及相同時段內的工作數量。若每次都在同一物件失敗,應優先調查物件來源與快取;若失敗物件持續變化,則應檢查磁碟壓力、並行寫入及工作區清理邏輯。

使用乾淨複製復原,而非原地修補

對於沒有人工修改的 CI 工作區,安全的復原方式通常是建立全新目錄、完整擷取目標提交、執行校驗,再以原子方式替換舊目錄。不要讓新複製的儲存庫重複使用尚未確認完整性的物件快取。

root="$HOME/ci-workspaces"
next="$root/project.next"
active="$root/project"
failed="$root/project.failed"

rm -rf "$next"
git clone --no-local "$REPOSITORY_PATH" "$next"
git -C "$next" checkout --detach "$EXPECTED_COMMIT"
git -C "$next" fsck --full --strict
test "$(git -C "$next" rev-parse HEAD)" = "$EXPECTED_COMMIT"
mv "$active" "$failed"
mv "$next" "$active"

替換前還應執行專案的最低限度驗收,例如解析工程、列出建置進入點,或執行一組快速測試。若隔離的儲存庫中存在尚未推送的提交,不要假設重新複製即可找回。應依據已保存的參照與 reflog,在副本中嘗試讀取;若物件確實缺失,就只能從可信任的遠端、其他完整複製,或原始工作成果中重新復原。

將物件校驗納入快取發布流程

共用 Git 快取不應由所有工作同時寫入。更穩定的模式是「產生、校驗、發布」:工作先在暫存目錄中建立新快取,完成擷取後執行 git fsck,只有通過校驗,才將目錄重新命名為新的唯讀代次。正在執行的工作固定使用各自的代次,不會在執行途中跟隨更新。

日常檢查可以收斂為四點:發布快取前校驗物件;將工作區與共用基準分離;失敗時保存物件雜湊與快取代次;清理指令碼只刪除不再被參照的完整代次。如此一來,即使單次下載失敗或程序退出造成不完整寫入,影響也只會停留在尚未發布的暫存目錄中。

最後,應將復原演練編寫成可執行指令碼,並在不含業務資料的測試儲存庫中驗證。一次可靠的演練至少必須證明:損壞的快取不會繼續分發、目標提交可從乾淨來源重建、尚未提交的差異有獨立保存路徑,而且替換過程不會讓兩個工作同時寫入同一工作區。

常見問題

發現 Git 物件損壞後,可以直接刪除 .git/objects 裡的異常檔案嗎?

不建議。應先停止寫入、保存日誌與參照快照,再隔離整個儲存庫。直接刪除物件可能擴大參照缺失,也會破壞後續診斷條件。

CI 儲存庫損壞時,執行 git gc 可以修復嗎?

不可以把 git gc 當成修復命令。它會重寫並清理物件,使現場更難判斷。可拋棄的 CI 工作區通常應重新複製並完成校驗。

如何避免損壞的 Git 快取繼續污染新任務?

快取應採用唯讀基線、暫存副本與校驗後發布。新快取先在獨立目錄執行 git fsck,通過後再以目錄重新命名完成原子切換。

獨享實體節點

為持續整合選擇一台雲端 Mac

比較 SDKMac M4 與 SDKMac M4 Pro 的配置、地區和四種計費週期,然後建立訂單。

選擇租用方案