持續整合(原始版本)
任何軟體開發流程中的一個重要部分是取得軟體的可靠建置。儘管它的重要性,但我們常常驚訝於這件事未被執行。在這裡,我們討論馬特在 Thoughtworks 的一個大型專案中實施的流程,這個流程在整個公司中越來越廣泛地使用。它強調一個完全自動化且可重現的建置,包括測試,每天執行多次。這允許每位開發人員每天整合,從而減少整合問題。
2000 年 9 月 10 日
本文現已被 更新版本 取代
軟體開發充斥著經常被討論但似乎很少執行的最佳實務。其中最基本且最有價值的一項,就是一個完全自動化的建置和測試流程,允許團隊每天多次建置和測試其軟體。每日建置的想法已經被廣泛討論。麥康奈爾 建議將其作為最佳實務,並且它長期以來一直被視為 Microsoft 開發方法的一個特點。然而,我們同意 XP 社群的說法,即每日建置是最低要求。一個允許您每天建置多次的完全自動化流程既可實現,而且非常值得付出努力。
我們使用術語持續整合,這是 XP(極限編程)實務之一。然而,我們承認這項實務已經存在很長一段時間,並且被許多從未考慮將 XP 納入其工作的人員所使用。我們一直在將 XP 作為軟體開發流程中的試金石,這影響了我們許多術語和實務。然而,您可以在不使用 XP 任何其他部分的情況下使用持續整合 - 事實上,我們認為它是任何稱職軟體開發活動的必要部分。
要讓自動化每日建置發揮作用,有幾個部分。
- 保留一個所有原始程式碼存放且任何人都可以從中取得目前來源(和先前版本)的單一位置
- 自動化建置流程,以便任何人都可以使用單一指令從來源建置系統
- 自動化測試,以便您可以使用單一指令在任何時候對系統執行一組良好的測試
- 確保每個人都可以取得目前可執行檔,而你確信這是目前為止最好的可執行檔。
所有這些都需要一定程度的紀律。我們發現將其導入專案需要耗費大量精力。我們也發現一旦安裝完畢,要持續維護並不需要太多精力。
持續整合的優點
持續整合最難表達的事情之一是,它對整個開發模式產生根本性的轉變,如果你從未在實踐這種模式的環境中工作過,那麼很難看出這種轉變。事實上,大多數人如果單獨工作就會看到這種氛圍,因為那時他們只會與自己整合。對許多人來說,團隊開發只會帶來某些屬於領域一部分的問題。持續整合減少了這些問題,作為換取,需要一定程度的紀律。
持續整合的根本好處是,它消除了人們花時間追蹤錯誤的時段,在這些時段中,一個人的工作會踩到另一個人的工作,而兩個人都沒有意識到發生了什麼事。這些錯誤很難找到,因為問題不在於一個人的領域,而在於兩項工作的互動。時間會加劇這個問題。整合錯誤通常會在第一次顯現之前幾週或幾個月就插入。因此,它們需要花費很多時間才能找到。
透過持續整合,絕大多數此類錯誤會在它們被引入的同一天顯現出來。此外,至少一半的互動發生在哪裡會立即顯而易見。這大大縮小了搜尋錯誤的範圍。如果你找不到錯誤,你可以避免將有問題的程式碼放入產品中,因此最糟的情況是你沒有新增同時新增錯誤的功能。(當然,你可能比討厭錯誤更想要這個功能,但至少這樣做是一個明智的選擇。)
現在,無法保證你可以找到所有整合錯誤。此技術依賴於測試,而我們都知道,測試無法證明沒有錯誤。關鍵在於,持續整合捕捉到的錯誤足以抵銷成本。
所有這些的最終結果是,透過減少追蹤整合錯誤所花費的時間,提高了生產力。雖然我們不知道有沒有人對此進行過任何接近科學研究的東西,但軼事證據相當有力。持續整合可以大幅減少在整合地獄中所花費的時間,事實上,它可以讓地獄變成一場非事件。
越頻繁越好
持續整合的核心有一個基本的反直覺效果。它表示頻繁整合比罕見整合來得更好。對於那些有在做的人來說,這很明顯;但對於那些沒有做的人來說,這看起來像是與直接經驗相矛盾。
如果你只偶爾整合,例如低於每天一次,那麼整合就是一項痛苦的練習,需要花費大量時間和精力。事實上,它夠尷尬,以至於你最不想做的事情就是更頻繁地做它。我們常聽到的評論是「在這麼大的專案中,你無法進行每日建置。」
然而,有些專案做到了。我們每天在一個由五十人團隊處理的約二十萬行程式碼代碼庫上建置幾十次。Microsoft 對擁有數千萬行程式碼的專案進行每日建置。
之所以能做到這一點,是因為整合的努力與整合之間的時間成正比。儘管我們不知道有任何針對此的測量,這表示每週整合一次並不會花費比每天整合一次多五倍的時間,而是多達二十五倍的時間。因此,如果你的整合很痛苦,你不應該將此視為無法更頻繁地整合的跡象。如果做得好,更頻繁的整合應該是輕鬆的,而且你最終花在執行整合上的時間會少很多。
這方面的關鍵是自動化。大多數整合都可以而且應該自動完成。取得來源、編譯、連結和重要的測試都可以自動完成。最後,你應該會得到一個簡單的指示,說明建置是否成功:是或否。如果是,你忽略它,如果不是,你應該能夠輕鬆地復原對組態的最後一次變更,並確定這次建置會成功。不應該需要思考才能取得一個可用的建置。
透過像這樣的自動化流程,你可以隨意建置。唯一的限制是執行建置所需的時間。
什麼是成功的建置?
一個重要的決定因素是,什麼構成了成功的建置。這似乎很明顯,但令人驚訝的是,這可能會變得混淆不清。馬丁曾經審查過一個專案。他詢問該專案是否執行每日建置,並得到肯定的答覆。幸運的是,Ron Jeffries 在場進一步探討。他問了「對於建置錯誤,你們會怎麼做?」得到的回答是「我們會寄電子郵件給相關人員」。事實上,該專案幾個月來都沒有成功建置。那不是每日建置,那是每日建置嘗試。
對於我們所謂的成功建置,我們採取相當積極的態度。
- 所有最新來源都已從組態管理系統中簽出
- 每個檔案都從頭開始編譯
- 產生的物件檔案(在我們的案例中是 Java 類別)會連結並部署以執行(放入 jar 檔中)。
- 系統會啟動,並針對系統執行一組測試(在我們的案例中,約有 150 個測試類別)。
- 如果所有這些步驟都執行完畢且沒有錯誤或人為介入,而且每個測試都通過,那麼我們就有一個成功的建置
大多數人認為編譯和連結就是建置。至少我們認為建置應包括啟動應用程式並針對它執行一些簡單的測試(McConnnell 使用了「冒煙測試」一詞:打開它,看看是否冒煙)。執行更全面的測試組會大幅提升持續整合的價值,所以我們也偏好這麼做。
單一來源點
為了輕鬆整合,任何開發人員都需要能夠輕鬆取得一組完整的最新來源。沒有什麼比必須四處詢問不同的開發人員以取得最新位元,然後必須複製它們、找出放置位置更糟的事,而這一切都是在開始建置之前。
標準很簡單。任何人都應該能夠帶來一台乾淨的機器,將它連線到網路,並透過單一指令下載建置開發中系統所需的所有來源檔案。
顯而易見(我們希望如此)的解決方案是使用組態管理(原始碼控制)系統作為所有程式碼的來源。組態管理系統通常設計為透過網路使用,並具備讓人員能夠輕鬆取得來源的工具。此外,它們還包含版本管理,因此您可以輕鬆找到各種檔案的先前版本。成本不應成為問題,因為 CVS 是一個出色的開源組態管理工具。
要讓這項工作順利進行,所有原始檔都應保留在組態管理系統中。所有通常比人們想像的還多。它還包括建置腳本、屬性檔、資料庫架構 DDL、安裝腳本以及在乾淨的機器上建置所需的其他任何內容。我們常常看到受控的程式碼,但找不到其他一些重要的檔案。
請務必確保所有內容都位於組態管理系統中的單一原始樹中。有時人們會在組態管理系統中為不同的元件使用不同的專案。這樣做會造成的問題是,人們必須記住哪些元件版本與哪些其他元件版本搭配使用。在某些情況下,您必須將來源分開,但這些情況比您想像的還要罕見。您可以從單一來源樹建置多個元件,此類問題應由建置腳本處理,而非儲存結構。
自動化建置腳本
如果您撰寫一個包含十幾個檔案的小程式,則建置應用程式可能只是對編譯器執行單一指令的問題:javac *.java
。較大的專案需要更多。在這些情況下,您在許多目錄中都有檔案。您需要確保產生的物件程式碼位於適當的位置。除了編譯之外,還可能有連結步驟。您有從其他檔案產生的程式碼,在您編譯之前需要產生這些程式碼。測試需要自動執行。
大型建置通常需要時間,如果您只做了一些小變更,您不希望執行所有這些步驟。因此,一個好的建置工具會分析在此過程中需要變更的內容。執行此操作的常見方法是檢查原始檔和物件檔的日期,並僅在原始檔日期較新的情況下進行編譯。然後,相依性會變得棘手:如果一個物件檔變更,依賴它的物件檔也可能需要重新建置。編譯器可能會處理這類事情,也可能不會。
根據您的需要,您可能需要建置不同種類的東西。您可以建置有或沒有測試程式碼的系統,或建置不同組的測試。有些元件可以獨立建置。建置腳本應允許您為不同的情況建置替代目標。
一旦您通過一個簡單的命令列,腳本通常會處理負載。這些可能是 shell 腳本,或使用更精密的腳本語言,例如 perl 或 python。但很快地,使用為這類事情設計的環境是有道理的,例如 Unix 中的 make 工具。
在我們的 Java 開發中,我們很快發現需要一個更嚴謹的解決方案。事實上,Matt 花了很多時間開發一個名為 Jinx 的建置工具,該工具是為企業 Java 工作而設計的。然而,最近我們已轉換到開源建置工具 Ant。Ant 的設計與 Jinx 非常類似,讓我們得以編譯 Java 檔案並將它們封裝到 Jar 中。它也讓我們可以輕鬆撰寫自己的 Ant 擴充功能,以便在建置中執行其他任務。
我們許多人使用 IDE,而且大多數 IDE 都有一些建置管理程序。然而,這些檔案總是 IDE 的專有檔案,而且常常很脆弱。此外,它們需要 IDE 才能運作。IDE 使用者設定自己的專案檔案,並將它們用於個別開發。然而,我們依賴 Ant 進行主要建置,而且主要建置是在伺服器上使用 Ant 執行的。
自測程式碼
光是讓程式編譯是不夠的。雖然強類型語言中的編譯器可以找出許多問題,但即使編譯成功,仍有太多錯誤會被放過。為了協助追蹤這些錯誤,我們非常重視自動化測試規範,這是 XP 提倡的另一種做法。
XP 將測試分為兩類:單元測試和驗收(也稱為功能)測試。單元測試是由開發人員撰寫的,通常會測試個別類別或一小群類別。驗收測試通常是由客戶或外部測試小組(在開發人員的協助下)撰寫的,而且會測試整個系統的端對端。我們使用這兩種測試,而且盡可能自動化這兩種測試。
作為建置的一部分,我們會執行一組我們稱為「BVT」(建置驗證測試)的測試。BVT 中的所有測試都必須通過,我們才能建置成功。所有 XP 風格的單元測試都在 BVT 中。由於本文是關於建置程序,因此我們將在此主要討論 BVT,請記住,除了 BVT 中的測試之外,還有一條第二測試線,因此不要僅憑 BVT 來判斷整個測試和 QA 工作。事實上,我們的 QA 小組永遠不會看到程式碼,除非它已通過 BVT,因為他們只處理正常運作的建置。
基本原則是,開發人員在撰寫程式碼時也會為該程式碼撰寫測試。當他們完成一項任務時,他們不僅會檢查生產程式碼,還會檢查該程式碼的測試。緊密遵循 XP 的人會使用測試優先的程式設計風格:在測試失敗之前,你不應該撰寫任何程式碼。因此,如果你想為系統新增一個新功能,你首先要撰寫一個只有在該功能存在時才會運作的測試,然後讓該測試運作。
我們使用 Java 撰寫測試,這與我們開發時使用的語言相同。這使得撰寫測試與撰寫程式碼一樣。我們使用JUnit作為組織和撰寫測試的架構。JUnit 是一個簡單的架構,可讓你快速撰寫測試,將它們組織成套件,並以互動或批次模式執行套件。(JUnit 是xUnit系列的 Java 成員 - 幾乎每種語言都有其版本。)
開發人員通常在撰寫軟體時,會在每次編譯時執行單元測試的某個子集。這實際上會加快開發工作,因為測試有助於找出你正在處理的程式碼中的任何邏輯錯誤。然後,你可以查看自上次執行測試以來的變更,而不是進行除錯。這些變更應該很小,因此更容易找出錯誤。
並非所有人都嚴格按照 XP 測試優先風格工作,但關鍵好處來自於同時撰寫測試。除了讓個別任務進行得更快之外,它還會建立 BVT,使其更可能找出錯誤。由於 BVT 每天執行多次,這表示 BVT 偵測到的任何問題都更容易找出,原因相同:我們可以查看少量的已變更程式碼,以找出錯誤。透過查看已變更程式碼進行除錯通常比透過逐步執行程式碼進行除錯更有效。
當然,你不能依賴測試來找出所有問題。正如經常被說到的:測試無法證明沒有錯誤。然而,完美並非你獲得良好 BVT 回報的唯一重點。不完美的測試,經常執行,比從未撰寫過的完美測試好得多。
一個相關的問題是開發人員在自己程式碼上撰寫測試的問題。人們常說,人們不應該測試自己的程式碼,因為很容易忽視自己工作中的錯誤。雖然這是真的,但自測程序需要快速將測試轉換到程式碼庫中。快速轉換的價值大於獨立測試人員的價值。因此,對於 BVT,我們依賴開發人員撰寫的測試,但有獨立的驗收測試是獨立撰寫的。
自我測試的另一個重要部分是透過回饋來提升測試品質,這是 XP 的一個關鍵價值。這裡的回饋是以 BVT 中逃脫的錯誤形式呈現。此處的規則是,在 BVT 中有失敗的單元測試之前,你不准修復錯誤。這樣一來,每次你修復一個錯誤時,你同時也會新增一個測試來確保它不會再次溜過你。此外,這個測試應該引導你想到其他需要撰寫的測試,以強化 BVT。
主建置
建置自動化對於個別開發人員來說很有意義,但它真正發揮作用的地方是在為整個團隊製作主建置。我們發現,擁有一個主建置程序能讓團隊團結在一起,並讓早期發現整合問題變得更容易。
第一步是選擇一台機器來執行主建置。我們使用 Trebuchet(我們玩了很多世紀帝國),這是一台四處理器伺服器,幾乎專門用於建置程序。(在建置花費很長時間的早期,這種馬力至關重要。)
建置程序是在一個始終執行的 Java 類別中。如果沒有正在進行的建置,建置程序會停留在 while 迴圈中,每隔幾分鐘檢查一次儲存庫。如果自上次建置以來沒有人檢查過任何程式碼,它會繼續等待。如果儲存庫中有新程式碼,則它會開始建置。
建置的第一個階段是從儲存庫中進行完整簽出。Starteam 提供了一個相當不錯的 Java API,因此可以輕鬆地連接到儲存庫。建置守護程式會在當前時間前五分鐘查看儲存庫,並查看在這最後五分鐘內是否有任何人簽入。如果是這樣,它會認為在五分鐘前簽出程式碼是安全的(這可以防止在有人簽入時簽出,而不會鎖定儲存庫。)
守護程式會簽出到 Trebuchet 上的目錄。一旦全部簽出,守護程式就會呼叫目錄中的 ant 腳本。然後 Ant 接手進行完整建置。我們從所有來源進行完整建置。ant 腳本會編譯並將產生的類別檔案分成六個 jar,以部署到 EJB 伺服器中。
一旦 Ant 完成編譯和部署,建置守護程式就會使用新的 jar 啟動 EJB 伺服器並執行 BVT 測試套件。測試執行,如果全部通過,則我們有一個成功的建置。然後,建置守護程式返回 Starteam,並使用建置編號標籤已簽出的來源。然後,它會查看在建置時是否有任何人簽入,如果是,它會開始另一個建置。如果不是,守護程式會返回其 while 迴圈並等待下一次簽入。
在建置結束時,建置守護程式會向所有使用該建置簽入新程式碼的開發人員發送電子郵件。電子郵件會摘要該建置的狀態。在收到電子郵件之前離開建置後簽入程式碼被認為是不好的行為。
守護程式會將所有步驟記錄到 XML 記錄檔中。Trebuchet 上執行一個 servlet,讓任何人都能透過檢查記錄來查看建置狀態。

圖 1:建置 servlet
畫面會顯示目前是否有建置正在執行,如果有,則會顯示開始時間。左側會顯示所有建置的歷程記錄,無論是否成功。按一下建置會顯示該建置的詳細資料:是否已編譯、測試狀態、有哪些變更等。
我們發現許多開發人員會定期查看這個網頁。這能讓他們了解專案的進度,以及在人員簽入時有哪些變更。我們可能會在這個網頁上放置其他專案新聞,但我們不希望它失去相關性。
讓任何開發人員都能在自己的電腦上模擬主建置非常重要。這樣一來,如果發生整合錯誤,開發人員就能在自己的電腦上調查和除錯問題,而不會影響主建置程序。此外,開發人員可以在簽入前在本地執行建置,以降低主建置失敗的機率。
這裡有一個合理的問題,主建置應該是乾淨建置,也就是只從來源建置,還是增量建置。增量建置的速度可能快很多,但也會增加問題潛入的風險,因為有些東西沒有編譯。它也有無法重建建置的風險。我們的建置相當快(約 15 分鐘,約 200KLOC),所以我們很樂意每次都執行乾淨建置。然而,有些商店喜歡大部分時間執行增量建置,但會定期執行乾淨建置(至少每天一次),以防出現那些奇怪的錯誤。
簽入
使用自動化建置表示開發人員在開發軟體時會遵循某種節奏。這個節奏最重要的部分是他們會定期整合。我們曾遇過每天建置一次的組織,但人員不會頻繁簽入。如果開發人員只在幾週內簽入一次,那麼每天建置對你來說沒有多大幫助。我們遵循一個一般原則,即每個開發人員每天大約簽入一次程式碼。
在開始新任務之前,開發人員應先與組態管理系統同步。這表示他們本機的來源副本是最新狀態。在過期的來源上撰寫程式碼只會導致問題和混淆。
然後,開發人員會更新需要變更的檔案來執行任務。開發人員可以在完成任務時或在任務進行中整合,但所有測試都必須順利執行才能整合。
整合的第一部分是將開發人員的工作副本與儲存庫重新同步。儲存庫上變更的任何檔案都會複製到工作目錄,而且組態管理系統會警告開發人員任何衝突。然後,開發人員需要建置到同步的工作集,並在這些檔案上成功執行 BVT。
現在,開發人員可以將新檔案提交到儲存庫。完成後,開發人員需要等待主建置。如果主建置成功,則簽入成功。如果不是,開發人員可以修正問題,並在問題簡單明瞭時提交修正。如果問題較為複雜,則開發人員需要還原變更、重新同步其工作目錄,並在再次提交前讓事情在本地正常運作。
有些簽入流程會強制執行簽入流程的序列化。在這種情況下,有一個建置權杖,只有一個開發人員可以取得。開發人員會取得建置權杖、重新同步工作副本、提交變更,並釋放權杖。這會阻止多位開發人員在建置之間更新儲存庫。我們發現,在沒有建置權杖的情況下,我們很少遇到問題,所以我們不使用權杖。通常,多位人員會提交到同一個主建置,但很少會導致建置失敗:而且通常很容易修正。
我們也讓開發人員自行判斷在簽入前應有多小心。由開發人員決定他們認為發生整合錯誤的可能性有多高。如果她認為可能性很高,則會在簽入前先執行本地建置,如果她認為整合錯誤的可能性很低,則只會簽入。如果她錯了,她會在主建置執行後發現,然後她必須還原變更並找出錯誤所在。你可以容忍錯誤,前提是它們很容易找到且容易移除。
總結
開發有紀律且自動化的建置流程對於受控專案至關重要。許多軟體大師都這麼說,但我們發現這在業界中仍然很罕見。
重點是要自動化所有事情,並且頻繁執行程序,以便快速找出整合錯誤。因此,當需要變更時,每個人都更願意變更,因為他們知道如果造成整合錯誤,很容易就能找出並修正。一旦獲得這些好處,你會發現它們會讓你死命堅持,不願放棄。
重大修訂
2000 年 9 月 10 日:原始出版