CI 這個詞對多數開發者並不陌生,而且它很少單獨出現,通常和 CD 一起被介紹,合稱 CI/CD。要理解兩者的分工,可以先把它們放回 DevOps 的整體流程來看:

圖片來源:CMU 17-636 DevOps, Lecture 1: Introduction
DevOps 把軟體從開發到上線、再回到開發的過程視為一個循環。CI 與 CD 分別負責這個循環中的不同區段:
兩者雖然常被並列,普及程度卻不相同。Brown 大學 CSCI 2952-F 的課程投影片中提到,CI 幾乎是所有軟體專案的標準配備,CD 則不然,例如航太、醫療等產業並不適合讓修改自動上線。原因在於,不是每個專案都需要部署,但只要專案會持續更新,每一次把修改合回共用版本,都是一次 integration。
在這篇文章裡,我們會把 CI 理解為一道專案層級的 quality gate(品質關卡)。remote 上的主分支是團隊共用的 source of truth,CI 就位在更新這個版本之前的最後一關:程式碼必須先通過檢查,才能被合併進去。至於要檢查什麼,前幾天介紹的 formatter、linter、type checker 與 test 都是常見的項目,實際的組合則由各個 repo 自行定義。
前面介紹的每一種工具,都能在開發者自己的電腦上執行。但只要檢查是在個人環境中進行,它就取決於個人的習慣與設定:有人會記得在提交前跑一次測試,有人不會;即使大家都有執行,本機上的工具版本也未必一致。隨著參與的人數與修改的頻率增加,「每個人都記得檢查」就不再是可靠的假設。
CI 把檢查從個人的習慣,轉變為專案層級的規則:
換句話說,CI 保證的是:任何進入主分支的程式碼,都經過了同一組檢查。
CI 解決了一致性的問題,但它有一個明顯的代價:回饋來得太晚。
若所有檢查都只在 remote 執行,每次都要 push、等 CI 跑完才知道結果,一個格式錯誤就得來回好幾趟,而這些問題本來在自己的電腦上幾秒內就能解決。
因此,實務上會把一部分檢查提前到本機,在修改離開開發者的電腦之前先攔下來,也就是 local quality gate。最常見的做法是 Git 的 pre-commit hook:Git 會在建立 commit 之前執行它,若檢查失敗,這次 commit 就會被中止。
需要注意的是,local quality gate 無法取代 CI。pre-commit hook 需要每個人自行安裝,也可以用 git commit --no-verify 直接跳過。它的定位是讓問題更早被發現,而確保問題不會進入主分支,仍然是 CI 的工作。

既然兩道關卡的定位不同,放在上面的檢查也應該有所區分:
| pre-commit hook(local) | CI(remote) | |
|---|---|---|
| 觸發時機 | 每次 commit | push、PR |
| 速度要求 | 數秒內 | 可以到數分鐘 |
| 檢查範圍 | 本次修改的檔案 | 整個 repo |
| 適合的檢查 | formatter、linter;type checker 與 test 只跑與本次修改相關、能快速完成的部分 | 四種檢查全部完整執行,必要時涵蓋多個版本/平台 |
| 能否繞過 | 可以(--no-verify) |
設為必要條件後不行 |
pre-commit hook 一旦執行太久,開發者很快就會習慣性地跳過它,所以只能跑部分檢查;而只檢查修改到的檔案,可能漏掉跨檔案的問題,完整的檢查仍要交給 CI。
簡單來說,local gate 負責快速回饋,remote gate 負責不可繞過。
各個 CI 平台的語法不同,但設定的內容大致相同:
| 平台 | 設定檔位置 |
|---|---|
| GitHub Actions | .github/workflows/*.yml |
| GitLab CI/CD | .gitlab-ci.yml |
設定檔和程式碼放在同一個 repo 裡,修改檢查規則,和修改程式碼一樣需要經過 review。
Git 的 hook 預設存放在 .git/hooks/,這個目錄不受版本控制。因此團隊通常會使用 hook 管理工具,把設定寫成檔案納入版控,每位開發者在本機啟用一次即可。設定的內容包括:
最常用的工具是 pre-commit,設定寫在 .pre-commit-config.yaml;Git 本身支援的所有 hook 類型,可以參考 Git 官方文件。
AI 讓程式碼產生的速度大幅提升,review 的負擔也跟著變重。有 CI 把關工具能檢查的部分,reviewer 就能專注在工具無法判斷的地方:需求是否被正確理解、設計是否合理。不過,CI 通過只代表修改符合 repo 所定義的檢查;檢查該如何定義,仍然需要由人負責。
對 coding agent 而言,local gate 比 remote gate 更關鍵。agent 的工作方式本身就是一個迴圈:修改程式、執行檢查、讀取結果、再修改。local 檢查的結果在幾秒內就能回到這個迴圈,此時相關的修改還在 agent 的 context 裡,可以當下修正;CI 則要等到 push 之後,錯誤才會以 log 的形式回來,agent 還得回頭推敲是哪一次修改造成的。
四種檢查在 local 都能提供回饋,但對 agent 的價值並不相同:
| 檢查 | 在 local 對 agent 的幫助 |
|---|---|
| formatter | 自動修正格式,agent 幾乎不需要處理 |
| linter | 指出有固定形狀的陷阱,修正方式通常很直接 |
| type checker | agent 常只讀了部分檔案,修改介面後容易漏掉其他呼叫端;型別檢查能直接指出哪裡的假設不一致 |
| test | 唯一檢查行為是否符合預期的工具;失敗訊息指出哪個 case、預期與實際輸出各是什麼,讓 agent 有明確的修正方向 |
formatter 與 linter 處理的是寫法,type checker 與 test 處理的則是程式對不對,這也正是 agent 最容易出錯、卻不容易自己察覺的地方。人會因為等待而跳過較慢的檢查,agent 不會;因此在 agent 的工作流程中,值得要求它在 commit 前就執行 type check 與相關的 test,而不是把這些錯誤留給 CI 才發現。
修改程式 → local 檢查(format、lint、type check、test)→ 依回饋修正 → push → CI 完整檢查 → 合併
local gate 讓 agent 在自己的迴圈裡把大部分問題修掉,CI 則確保最終進入主分支的程式碼,都通過了專案定義的完整檢查。