引言#

許多軟體專案在開發過程中,長時間處於「無法運作」的狀態。開發者提交變更、甚至執行單元測試,但沒有人嘗試在類生產環境中啟動並使用整個應用程式。使用長期分支(long-lived branches)或將驗收測試延後的專案,往往在開發末期安排漫長的整合階段,而這個階段的時程完全無法預測。

持續整合(Continuous Integration, CI) 要求每一次提交都必須建置整個應用程式並執行完整的自動化測試套件。如果建置或測試失敗,團隊必須立即停下手邊工作修復問題。目標是讓軟體在任何時刻都處於可運作狀態。

CI 代表一種典範轉移(paradigm shift):

  • 沒有 CI 時:軟體預設是壞的,直到有人證明它能運作
  • 有 CI 時:軟體預設是可運作的(前提是有足夠完善的自動化測試),每次變更後都能得到驗證

實踐 CI 的團隊能更快交付軟體、產生更少的 bug,因為問題在流程早期就被發現,修復成本更低。

實施持續整合#

開始前的三個先決條件#

  1. 版本控制(Version Control)

    • 專案中的所有東西都必須存入版本控制:程式碼、測試、資料庫腳本、建置與部署腳本
    • 不存在「專案太小不需要版本控制」的情況
  2. 自動化建置(Automated Build)

    • 必須能從命令列啟動建置流程
    • 不應僅依賴 IDE 的建置功能,原因包括:
      • CI 環境需要可稽核的自動化流程
      • 建置腳本應像程式碼一樣被測試與重構
      • 命令列建置更容易理解、維護與除錯,也便於與維運人員協作
  3. 團隊共識(Agreement of the Team)

    • CI 是一種實踐(practice),不是工具
    • 團隊需要承諾:頻繁提交小量增量變更到主線,並將修復建置失敗視為最高優先任務

基本的 CI 系統#

CI 不需要昂貴的工具,James Shore 曾撰文描述如何用一台閒置開發機、一隻橡皮雞和一個鈴鐺實現 CI(“Continuous Integration on a Dollar a Day”)。不過現代 CI 工具安裝簡單,開源選擇包括 Hudson、CruiseControl 家族等;商業選擇包括 Go(ThoughtWorks)、TeamCity(JetBrains)、Bamboo(Atlassian)等。

設定 CI 伺服器只需告訴它三件事:

  • 版本控制庫的位置
  • 要執行的建置與測試腳本
  • 如何通知團隊建置結果

第一次在 CI 工具上執行建置時,很可能會發現機器缺少各種軟體與設定。記錄下所有安裝步驟,並將系統依賴的軟體和設定納入版本控制,自動化新機器的配置流程。

每次提交的標準流程#

  1. 確認建置是否正在執行;若正在跑且失敗了,先與團隊合作修復
  2. 建置通過後,從版本控制更新本地開發環境
  3. 在本地執行建置腳本與測試,確認一切正常
  4. 本地建置通過後,提交程式碼到版本控制
  5. 等待 CI 工具執行建置
  6. 若失敗,立即停下手邊工作修復;若通過,進入下一個任務
flowchart TD
    A[確認建置狀態] --> B{建置正在失敗?}
    B -->|是| C[先修復再繼續]
    C --> A
    B -->|否| D[從主線更新本地程式碼]
    D --> E[本地執行建置與測試]
    E --> F{本地測試通過?}
    F -->|否| E
    F -->|是| G[提交到版本控制]
    G --> H[等待 CI 建置結果]
    H --> I{CI 建置通過?}
    I -->|是| J[✅ 繼續下一個任務]
    I -->|否| K[立即修復]
    K --> E

持續整合的先決實踐#

頻繁提交(Check In Regularly)#

  • 至少一天提交兩次到主幹(trunk / mainline)
  • 頻繁提交的好處:
    • 變更較小,較不容易破壞建置
    • 隨時有可回退的已知良好版本
    • 促進小步重構的紀律
    • 大量檔案修改較不易與他人衝突
    • 鼓勵探索性嘗試(不滿意就回退)

使用長期分支(long-lived branches)時無法真正實現持續整合,因為分支上的程式碼定義上就沒有與其他開發者整合。除非極為特殊的情況,作者不建議使用分支。

建立完善的自動化測試套件#

沒有自動化測試的建置通過只代表「能編譯」,並不代表應用程式真正能運作。CI 建置中應包含三類測試:

測試類型說明特性
單元測試(Unit Tests)測試小範圍程式的隔離行為不碰資料庫、檔案系統、網路;整套應在 10 分鐘內跑完
元件測試(Component Tests)測試多個元件的交互行為可能碰資料庫或檔案系統;耗時較長
驗收測試(Acceptance Tests)驗證業務需求與非功能需求在類生產環境執行;可能需數小時甚至超過一天

保持建置與測試流程簡短#

建置加測試時間過長會導致:

  • 開發者在提交前跳過完整建置與測試
  • 多次提交累積,無法判斷哪次提交破壞了建置
  • 開發者減少提交頻率

理想的提交階段(commit stage)建置時間:90 秒最佳、5 分鐘良好、10 分鐘是上限

分階段建置策略

  1. 提交階段(Commit Stage):編譯、執行單元測試、產生可部署的二進位檔 — 每次提交都執行
  2. 第二階段:取用第一階段的產出,執行驗收測試、整合測試、效能測試 — 提交階段通過後執行

若第二階段超過半小時,考慮在多處理器機器上平行執行,或建立建置網格(build grid)。也可在提交階段加入簡單的煙霧測試(smoke test)以快速回饋。

管理開發工作區#

開發者應該能在自己的本地機器上執行建置、自動化測試和部署,並使用與 CI 環境相同的自動化流程。需要管理好的項目包括:

  • 原始碼、測試資料、資料庫腳本、建置腳本、部署腳本(全部存入版本控制)
  • 第三方相依套件的正確版本(可使用 Maven、Ivy 等工具,或直接提交到版本控制)
  • 自動化測試能在開發機上執行(包含中介軟體設定、記憶體資料庫等)

好的應用程式架構的標誌之一:能夠在開發機器上輕鬆執行。

使用持續整合軟體#

基本運作#

CI 伺服器有兩個核心元件:

  1. 執行引擎:定期輪詢版本控制系統,偵測到變更時檢出專案並執行指定的建置命令
  2. 結果展示:透過 Web 介面顯示建置清單、測試報告,並提供產出物(二進位檔、安裝包)的下載

Figure 3.1: Screenshot of Hudson, by Kohsuke Kawaguchi

進階功能#

  • 建置狀態視覺化:紅綠熔岩燈、文字轉語音播報破壞建置者的名字、大螢幕顯示建置狀態與提交者頭像
  • 程式碼分析:測試覆蓋率、程式碼重複率、編碼標準合規度、循環複雜度(cyclomatic complexity)等
  • 建置網格(Build Grid):進階 CI 伺服器可分散工作到多台機器、管理元件間的建置相依性

能見度(Visibility) 是使用 CI 伺服器最重要的好處之一。建置失敗不代表品質有問題,反而代表問題被及早發現,避免流入生產環境。

CI 的前身#

CI 的演進歷程:

階段做法優缺點
夜間建置(Nightly Build)每晚自動編譯與整合隔天才發現錯誤,建置可能紅好幾天
夜間建置 + 自動測試加入基本煙霧測試提升了信心但回饋仍然緩慢
滾動建置(Rolling Build)建置完成後立刻開始下一輪比夜間建置好,但無法追溯到具體哪次提交造成問題
持續整合每次提交觸發建置完整的可追溯性
timeline
    title CI 演進歷程
    夜間建置 : 每晚編譯一次 : 發現問題已是隔天
    夜間建置+自動測試 : 加入自動化測試 : 仍有延遲
    滾動建置 : 每次提交觸發 : 缺乏系統性管理
    持續整合 : CI 伺服器管理 : 完整自動化流程

必要實踐(Essential Practices)#

CI 是實踐,不是工具。以下列出的實踐是 CI 能否成功的關鍵,團隊必須嚴格遵守。

不要在壞掉的建置上提交#

  • 這是 CI 的首要禁忌(cardinal sin)
  • 建置失敗時,負責的開發者應立即著手修復
  • 若違反此規則,團隊會習慣看到紅色建置,建置將長期處於失敗狀態

提交前在本地執行所有提交測試#

提交前應:

  1. 從版本控制更新本地程式碼
  2. 在本地執行建置與提交測試
  3. 通過後才提交

本地先跑測試的原因:

  • 其他人可能在你上次更新後已經提交了新變更,合併後可能導致測試失敗
  • 如果本地通過但 CI 失敗,通常是忘記將新檔案加入版本控制

許多現代 CI 伺服器提供 預測試提交(pretested commit / personal build / preflight build) 功能:CI 伺服器先用你的變更執行建置,通過後才自動提交。搭配分散式版本控制系統(DVCS)效果更佳。

等待提交測試通過後再繼續#

  • 提交後,開發者有責任監控建置進度
  • 在提交階段完成前,不要開始新任務、不要去吃午餐、不要開會
  • 通過後才能進入下一個任務;失敗則立即修復或回退變更

絕不在壞掉的建置下班#

週五下午 5:30 提交後建置失敗,你有三個選項:

  1. 留下來修好它
  2. 回退變更,下週再處理
  3. 拍拍屁股走人(不要選這個

如果留下壞掉的建置回家,週一回來後記憶模糊修復更困難。在分散式團隊中,這對其他時區的同事影響更嚴重。建議的做法是:提早且頻繁提交,留有足夠時間處理問題;或者至少回退變更,把修復留到隔天早上。

隨時準備回退到前一版本#

  • 破壞建置是正常的事,重要的是快速恢復
  • 建立團隊規則:嘗試修復 10 分鐘,若無法解決就回退(time-box fixing)
  • 如同飛行員著陸時隨時準備復飛(go around),提交時也應隨時準備回退

不要註解掉失敗的測試#

  • 這是開發者為了讓提交通過的常見捷徑,但絕對是錯誤的做法
  • 測試失敗時應該:
    • 修復程式碼(若發現了回歸問題)
    • 修改測試(若假設條件已改變)
    • 刪除測試(若被測功能已不存在)
  • 可追蹤被註解掉的測試數量,甚至設定門檻(如超過 2% 就讓建置失敗)

對你的變更導致的所有失敗負責#

  • 即使你寫的測試全部通過,但其他測試因你的變更而失敗,建置仍然是壞的,你有責任修復
  • 這意味著團隊中每個人都需要存取整個程式碼庫(shared code ownership)
mindmap
  root((持續整合成功要素))
    先決條件
      版本控制系統
      自動化建置流程
      團隊共識與紀律
    必要實踐
      不在壞建置上提交
      提交前本地先跑測試
      等待測試通過
      不帶壞建置下班
      隨時準備回退
      不註解失敗的測試
      對所有失敗負責
    建議實踐
      TDD
      架構違規檢查
      慢速測試門檻
      棘輪法

測試驅動開發(Test-Driven Development, TDD)#

  • 快速回饋(CI 的核心成果)只有在優秀的單元測試覆蓋率下才有可能
  • 作者認為達到優秀單元測試覆蓋率的唯一途徑就是 TDD
  • TDD 流程:先寫測試(作為可執行規格),再寫實作程式碼。測試同時作為回歸測試與文件

建議實踐(Suggested Practices)#

極限程式設計(XP)開發實踐#

CI 是 XP 十二個核心實踐之一,與其他 XP 實踐互補。除了 TDD 與共享程式碼所有權外,重構(Refactoring) 也是高效開發的基石。CI 和 TDD 讓開發者能自信地進行大範圍重構,同時確保不破壞現有行為。重構也促進了頻繁提交——每個小步增量修改後都可以提交。

讓建置因架構違規而失敗#

在提交階段加入檢查,防止開發者違反系統架構規則。例如在分散式系統中,檢查客戶端與伺服器端程式碼之間是否有不應存在的直接呼叫(應該走遠端呼叫)。此技巧適合用來守護重要的架構約束。

讓建置因慢速測試而失敗#

  • 設定個別測試的時間上限(例如 2 秒),超過就讓建置失敗
  • 這能促使開發者關注測試效能,測試快則提交更頻繁,提交頻繁則合併問題更少
  • 注意:此實踐更適合作為短期聚焦策略,不一定要永久啟用,以避免在系統負載異常時產生不穩定的測試

讓建置因警告與編碼風格違規而失敗#

  • 編譯器警告通常有其道理,可以設定建置在出現警告時失敗
  • 搭配程式碼品質工具:Simian(重複偵測)、JDepend/NDepend(設計品質指標)、CheckStyle/FxCop(編碼實踐檢查)、FindBugs(常見 bug 偵測)

棘輪法(Ratcheting):比較當次提交的警告 / TODO 數量與上次提交,若增加就讓建置失敗。這可以逐步且溫和地提升程式碼品質,確保每次提交至少減少一個問題。

分散式團隊(Distributed Teams)#

流程影響#

  • 同一時區:流程大致相同,但因為不在同一空間,提醒修復建置時較不自然;預測試提交功能更有用
  • 不同時區:影響被放大。若舊金山團隊破壞建置後下班,北京團隊隔天一上班就會受阻

分散式團隊中,VoIP、即時通訊工具至關重要。定期讓不同地點的人員互訪以建立信任。可使用視訊會議進行回顧會議、展示會、站立會議。每個團隊也可錄製螢幕錄影,介紹當天開發的功能。

集中式持續整合#

進階 CI 伺服器可以提供:

  • 集中管理的建置農場(build farm)與授權方案
  • 團隊自助式 CI 服務,無需自行取得硬體
  • 統一的環境配置,確保與生產環境一致
  • 跨專案的標準化指標與儀表板

虛擬化技術搭配集中式 CI 效果極佳,可按需建立一致的基準映像檔環境。

集中式 CI 必須讓團隊能快速自助取得新環境。若要求發多封信等好幾天才能拿到新的 CI 環境,團隊會繞過流程、不使用 CI。

技術議題#

  • 版本控制系統與建置基礎設施之間的網路頻寬至關重要
  • 若跨地理位置的連線速度慢,考慮使用分散式版本控制系統(如 Git、Mercurial),允許離線提交
  • 版本控制系統、CI 系統和測試環境必須讓每個開發地點都能平等存取,且每個地點都要有管理這些系統的知識與權限

替代方案(次優選擇)#

若無法解決頻寬問題,可考慮:

  • 本地 CI 伺服器與測試環境(需嚴格確保各地環境一致)
  • 本地版本控制庫 + 定期同步到全域主庫(合併衝突風險高)
  • 按元件切分庫與團隊(詳見第 13 章)

二進位檔原則上應只建置一次,再分發到各地。若必須本地建置,應自動產生雜湊值並比對,確保產出一致。

分散式版本控制系統(DVCS)#

DVCS(如 Git、Mercurial)的核心特性是每個庫都包含完整的專案歷史,沒有任何庫天生享有特權。它增加了一層間接性:變更先提交到本地庫,再推送到其他庫。

DVCS 與 CI 的關係#

DVCS 完全可以搭配傳統 CI 使用:指定一個庫為主庫(master),CI 伺服器監控該庫的變更,所有人將變更推送到此庫。這保留了 DVCS 的優勢(如頻繁本地提交、探索性重構),同時維持 CI 的運作。

GitHub 模型的挑戰#

GitHub 的 fork/pull request 模型打破了 CI「單一主線」的基本假設:

  • 每個使用者的變更存在不同的庫中,無法輕易判斷哪些變更能成功整合
  • 嘗試將所有 fork 自動合併通常會在合併階段就失敗

Figure 3.2: Integrating branches

折衷做法:為每個 fork 建立 CI 建置,每次變更時嘗試從主庫合併並執行建置。這不是真正的 CI,但能告訴 fork 維護者他們的分支是否能與主庫合併並產生可運作的軟體。

Martin Fowler 稱之為 「雜交式整合」(promiscuous integration):貢獻者不僅在 fork 與主庫之間拉取變更,也在 fork 之間互相拉取。

這些替代模型的適用條件#

  • 小型且經驗豐富的核心提交者團隊
  • 定期從 fork 拉取變更,避免大量難以合併的庫存累積
  • 核心開發者人數相對少,可能搭配較大但提交速度較慢的社群

這些條件適用於大多數開源專案和小型團隊,但幾乎不適用於中大型全職開發團隊。對於這類團隊,還是應該使用傳統的「單一主線 + CI」模式。

總結#

如果只能從本書中選擇一個實踐來導入,作者建議選擇持續整合

CI 帶來的關鍵效益:

效益說明
典範轉移軟體的預設狀態從「壞的」變成「可運作的」
緊密的回饋迴路問題在引入時就被發現,修復成本低
強制良好實踐促使團隊做好組態管理、建立自動化建置與測試流程
團隊紀律指標建置是否保持綠色是一個簡單而有力的指標
擴展基礎成熟的 CI 系統可進一步建構部署流水線、品質儀表板、一鍵部署等基礎設施