MiniRent 工程指南

在雲端 Mac 建立 Swift 程式風格增量門禁

在雲端 Mac 建立 Swift 程式風格增量門禁

在雲端 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 需要整理日誌,應先保存原始結束狀態,輸出摘要後再依原值結束。

以雙層策略控制長期成本

增量門禁只能保證「這次變更沒有引入新問題」,無法證明舊程式碼全部符合目前規則。因此可以將檢查拆成兩層:

  1. 每個合併請求都執行差異檢查,目標是快速、穩定,並且能在本機完整重現。
  2. 排程工作執行全儲存庫檢查,用於發現基準線漂移、失效的排除項目,以及長期未處理的舊警告。
  3. 規則升級應獨立提交,不要與業務變更混在一起,以便審查格式化造成的大範圍變動。
  4. 產生目錄必須有明確來源,除了從工具設定中排除,也要在差異指令碼中過濾,避免產生工作的執行順序影響結果。

上線前再確認一次:目標分支參照是否存在、工具版本是否精確相符、重新命名的檔案是否已納入、刪除的檔案是否已排除、含空格的路徑是否能安全處理,以及沒有 Swift 變更時是否會正常結束。完成這些檢查後,程式風格門禁才能從「偶爾報錯的指令碼」轉變為可靠的提交邊界。

常見問題

使用增量檢查後還需要全儲存庫檢查嗎?

仍然需要。增量檢查負責合併請求的快速回饋,定期全量檢查則用來發現未修改舊檔案中的規則漂移與既有違規。

SwiftFormat 與 SwiftLint 應該啟用相同規則嗎?

不應重複管理。可自動修正的排版交給 SwiftFormat,風險模式與團隊規範交給 SwiftLint,重疊規則只保留一個擁有者。

獨享實體設備

使用雲端 Mac mini 執行下一項開發或建置任務

可從兩種 M4 設定、四種租用週期與五個可租用節點中選擇,實際可用狀態以控制台即時回傳為準。

選擇設定並租用