在雲端 Mac 上對一個中型 Swift 儲存庫執行完整格式檢查時,常見的問題並不是工具本身速度太慢,而是每次合併請求都會重複掃描未修改的原始碼、產生目錄與外部相依套件。更棘手的是,開發者本機與 CI 使用不同版本,導致同一行程式碼在兩邊得到不同結果。解決方式不是停用規則,而是將工具版本、設定檔與差異計算一併納入儲存庫管理。
先釐清兩種工具的職責
SwiftFormat 適合處理結果明確且可自動修正的排版,例如縮排、換行與多餘空白;SwiftLint 則更適合檢查強制型別轉換、過長函式、命名限制等風險模式。若兩者同時管理同一條規則,開發者可能才剛完成格式化,隨後又遭靜態檢查拒絕。
建議先建立一份簡單的規則歸屬表:
| 檢查類型 | 負責工具 | 合併請求行為 |
|---|---|---|
| 縮排、空白、參數換行 | SwiftFormat | 僅檢查,不自動改寫 |
| 強制型別轉換與強制解包 | SwiftLint | 標記失敗並輸出檔案位置 |
| 產生的程式碼與外部相依套件 | 兩者皆排除 | 不納入差異檔案清單 |
| 歷史遺留警告 | SwiftLint 基準線 | 僅阻止新增問題 |
不要在 CI 中直接執行會改寫原始碼的命令。門禁只負責回報差異;開發者在本機修正後再提交,才能讓儲存庫狀態易於重現。
將版本與規則納入儲存庫
不要依賴雲端 Mac 上「目前安裝」的工具。應先選定團隊已驗證的版本,再將預期版本寫入指令碼。以下以 SwiftFormat 0.54.3 與 SwiftLint 0.55.1 為例;實際專案應替換為自行驗證的版本。
.swiftformat 可以維持精簡:
--swiftversion 5.10
--indent 4
--wraparguments before-first
--exclude .build,DerivedData,Vendor,Generated
.swiftlint.yml 則明確指定原始碼範圍與忽略目錄:
included:
- Sources
- Tests
excluded:
- .build
- DerivedData
- Vendor
- Generated
only_rules:
- force_cast
- force_try
- trailing_whitespace
- unused_import
規則檔必須與程式碼一併審查。新增規則時,先在獨立分支對整個儲存庫執行掃描,確認警告規模,再決定要一次修正,或是建立基準線。不要直接將數百條舊警告帶入合併請求門禁,否則團隊最終只會習慣忽略紅燈。
正確擷取合併請求中的 Swift 差異
直接使用 git diff HEAD^ 只適用於單次提交的分支。當合併請求包含多次提交或曾經過 rebase 時,這種寫法很容易遺漏檔案。更穩妥的方式是先計算目前提交與目標分支的共同祖先,再擷取新增、複製、修改與重新命名的檔案。
set -euo pipefail
expected_format="0.54.3"
expected_lint="0.55.1"
[[ "$(swiftformat --version)" == "$expected_format" ]]
[[ "$(swiftlint version)" == "$expected_lint" ]]
base_ref="${LINT_BASE_REF:-origin/main}"
merge_base="$(git merge-base HEAD "$base_ref")"
changed=("${(@0)$(git diff --name-only -z --diff-filter=ACMR "$merge_base" HEAD)}")
swift_files=()
for file in "${changed[@]}"; do
[[ "$file" == *.swift ]] || continue
[[ "$file" == .build/* ]] && continue
[[ "$file" == DerivedData/* ]] && continue
[[ "$file" == Vendor/* ]] && continue
[[ "$file" == Generated/* ]] && continue
swift_files+=("$file")
done
(( ${#swift_files[@]} > 0 )) || exit 0
swiftformat --lint "${swift_files[@]}"
swiftlint lint --strict --force-exclude "${swift_files[@]}"
這裡使用以空字元分隔的檔案清單,因此含有空格的路徑也不會被錯誤拆分。--diff-filter=ACMR 會排除已刪除的檔案,避免檢查工具收到不存在的路徑。目標分支名稱透過環境變數傳入,讓同一份指令碼既能在預設分支上執行,也能支援發布分支。
處理淺層複製
如果 git merge-base 找不到共同祖先,通常不是指令碼出錯,而是 CI 只擷取了深度很淺的提交歷史。應先增加 checkout 深度,確認目標分支參照存在,再執行指令碼。不要退回使用 HEAD^,否則門禁會在不同分支結構下產生不一致的結果。
讓失敗結果可定位且可重現
門禁失敗後,日誌至少應回答三個問題:使用了什麼工具版本、比較的是哪個基準提交,以及哪些檔案進入檢查。若版本不符,應立即停止,而不是繼續執行並產生大量格式差異。
在本機重現時,開發者只需擷取目標分支參照,並執行同一份指令碼:
git fetch origin main
LINT_BASE_REF=origin/main zsh Scripts/lint-changed-swift.zsh
常見錯誤包括將檢查命令接在管線後,再讀取錯誤的結束狀態;或是為了產生較美觀的日誌而使用 || true,導致真正的失敗被忽略。指令碼啟用 set -euo pipefail 後,任何工具傳回非零狀態都會終止工作。若 CI 需要整理日誌,應先保存原始結束狀態,輸出摘要後再依原值結束。
以雙層策略控制長期成本
增量門禁只能保證「這次變更沒有引入新問題」,無法證明舊程式碼全部符合目前規則。因此可以將檢查拆成兩層:
- 每個合併請求都執行差異檢查,目標是快速、穩定,並且能在本機完整重現。
- 排程工作執行全儲存庫檢查,用於發現基準線漂移、失效的排除項目,以及長期未處理的舊警告。
- 規則升級應獨立提交,不要與業務變更混在一起,以便審查格式化造成的大範圍變動。
- 產生目錄必須有明確來源,除了從工具設定中排除,也要在差異指令碼中過濾,避免產生工作的執行順序影響結果。
上線前再確認一次:目標分支參照是否存在、工具版本是否精確相符、重新命名的檔案是否已納入、刪除的檔案是否已排除、含空格的路徑是否能安全處理,以及沒有 Swift 變更時是否會正常結束。完成這些檢查後,程式風格門禁才能從「偶爾報錯的指令碼」轉變為可靠的提交邊界。
常見問題
使用增量檢查後還需要全儲存庫檢查嗎?
仍然需要。增量檢查負責合併請求的快速回饋,定期全量檢查則用來發現未修改舊檔案中的規則漂移與既有違規。
SwiftFormat 與 SwiftLint 應該啟用相同規則嗎?
不應重複管理。可自動修正的排版交給 SwiftFormat,風險模式與團隊規範交給 SwiftLint,重疊規則只保留一個擁有者。
使用雲端 Mac mini 執行下一項開發或建置任務
可從兩種 M4 設定、四種租用週期與五個可租用節點中選擇,實際可用狀態以控制台即時回傳為準。