持續整合

持續整合是一種軟體開發實務,其中團隊的每位成員都會將其變更與同事的變更合併到程式碼庫中,至少每天一次。每次整合都會透過自動化建置(包括測試)進行驗證,以盡快偵測整合錯誤。團隊發現這種方法可以降低交付延誤的風險、減少整合的精力,並啟用有助於快速增強新功能的健康程式碼庫的實務。

2024 年 1 月 18 日



我對第一次看到大型軟體專案的情景記憶猶新。當時我在一家大型英國電子公司擔任暑期實習生。我的經理是品質保證 (QA) 小組的一員,他帶我參觀一個場地,我們走進一個巨大、令人沮喪、沒有窗戶的倉庫,裡面有許多人在隔間裡工作。我得知這些程式設計師已經為這個軟體編寫程式碼好幾年了,儘管他們已經完成編寫程式,但他們各自的單元現在正整合在一起,而且他們已經整合了好幾個月。我的導遊告訴我,沒有人真正知道整合需要花多少時間。從中我了解到軟體專案的常見故事:整合多位開發人員的工作是一個漫長且不可預測的過程。

多年來,我從未聽說過一個團隊陷入如此漫長的整合,但这並不意味著整合是一個輕鬆的過程。一位開發人員可能已經在一個新功能上工作了好幾天,定期將一個共同主分支的變更拉入她的功能分支。就在她準備推送她的變更之前,一個重大的變更登陸在主分支上,它改變了她正在互動的一些代碼。她必須從完成她的功能轉變為找出如何將她的工作與這個變更整合,雖然對她的同事來說更好,但對她來說卻不太好。希望變更的複雜性將在合併原始碼中,而不是一個只有在她執行應用程式時才會顯示的隱蔽錯誤,迫使她調試不熟悉的代碼。

至少在那個場景中,她可以在提交她的拉取請求之前找出問題。在等待某人審查變更時,拉取請求可能會充滿問題。審查可能需要時間,迫使她從她的下一個功能中切換上下文。在那段期間一個困難的整合可能會非常令人不安,進一步拖延審查過程。這甚至可能不是故事的結局,因為整合測試通常只在拉取請求合併後才執行。

隨著時間的推移,這個團隊可能會了解到對核心代碼進行重大變更會導致這種問題,因此停止這樣做。但這樣做,通過防止定期重構,最終允許在整個代碼庫中滋生垃圾。遇到垃圾代碼庫的人會想知道它是如何變成這種狀態的,而且答案通常在於一個摩擦力如此之大的整合過程中,它讓人們不敢移除那些垃圾。

但这不必如此。Thoughtworks 的大多數同事以及世界各地許多其他同事所做的項目都將整合視為非事件。任何個別開發人員的工作距離共享項目狀態只有幾個小時,並且可以在幾分鐘內重新整合到該狀態。任何整合錯誤都可以快速找到並快速修復。

這種對比並非昂貴且複雜的工具的結果。它的精髓在於團隊中的每個人都頻繁地整合,至少每天一次,針對受控的原始碼儲存庫。這種做法稱為「持續整合」(或在某些圈子中稱為「基於主幹的開發」)。

在本文中,我將解釋什麼是持續整合以及如何做好它。我寫這篇文章有兩個原因。首先,總是有新人進入這個行業,我想向他們展示如何避免那個令人沮喪的倉庫。但其次,這個主題需要明確,因為持續整合是一個被誤解很多的概念。有很多人說他們正在進行持續整合,但一旦他們描述了他們的工作流程,很明顯他們缺少重要的部分。對持續整合的清晰理解有助於我們溝通,這樣我們在描述我們的工作方式時就知道會發生什麼。它也幫助人們意識到還有更多事情可以做來改善他們的體驗。

我原本在 2001 年撰寫這篇文章,並於 2006 年更新。自此以來,軟體開發團隊的普遍預期已大幅改變。我在 1980 年代看到的數個月整合已成遙遠的記憶,版本控制和建置指令碼等技術已變得普遍。我在 2023 年再次改寫這篇文章,以更適切地說明當時的開發團隊,並以二十年的經驗確認持續整合的價值。

利用持續整合建構功能

對我來說,說明持續整合是什麼以及其運作方式最簡單的方法,就是展示一個快速範例,說明它如何與小型功能的開發搭配使用。我目前正與一家大型魔藥製造商合作,我們正在擴充其產品品質系統,以計算魔藥效果持續時間。系統中已支援十幾種魔藥,我們需要擴充飛行魔藥的邏輯。(我們了解到,魔藥過早失效會嚴重影響客戶留存率。)飛行魔藥引入了幾個需要處理的新因素,其中之一是二次混合過程中的月相。

我首先將最新產品原始碼複製到我的本機開發環境中。我透過使用 git pull 從中央儲存庫中檢出目前的 mainline 來執行此操作。

原始碼進入我的環境後,我執行一個指令來建置產品。此指令會檢查我的環境是否正確設定,將原始碼編譯成可執行產品,啟動產品,並對其執行全面的測試套件。這應該只需要幾分鐘,而我開始瀏覽程式碼,以決定如何開始新增新功能。此建置幾乎不會失敗,但我還是會執行它以防萬一,因為如果它真的失敗了,我希望在開始進行變更之前就得知。如果我在建置失敗的情況下進行變更,我會誤以為是自己的變更導致失敗。

現在我取得我的工作副本,並執行任何我需要執行的動作來處理月相。這將包括變更產品程式碼,以及新增或變更部分自動化測試。在那段期間,我頻繁執行自動化建置和測試。大約一小時後,我已整合月球邏輯並更新測試。

現在我準備將我的變更整合回中央儲存庫。我的第一步是再次提取,因為有可能(確實很可能)我的同事在我工作時已將變更推送到主線。確實有幾個這樣的變更,我將它們提取到我的工作副本中。我在它們之上結合我的變更,並再次執行建置。這通常感覺多餘,但這次測試失敗了。測試給我一些關於錯誤原因的線索,但我發現查看我提取的提交記錄以了解變更內容更有用。看來有人調整了一個函式,將其部分邏輯移到其呼叫方。他們修正了主線程式碼中的所有呼叫方,但我在我的變更中新增了一個新的呼叫,他們當然還看不到。我進行相同的調整並重新執行建置,這次通過了。

由於我花了幾分鐘來解決這個問題,所以我再次提取,並且再次有一個新的提交記錄。但是,建置使用這個提交記錄執行得很好,所以我能夠將我的變更 git push 到中央儲存庫。

然而,我的推送並不表示我已完成。一旦我推送到主線,持續整合服務便會注意到我的提交,將變更的程式碼簽出到 CI 代理程式,並在其中建置。由於建置在我的環境中很順利,我不認為它會在 CI 服務中失敗,但「在我的機器上運作」會成為程式設計師圈子中眾所周知的說法是有原因的。很少會錯過導致 CI 服務建置失敗的情況,但很少並不等於絕不發生。

整合機器的建置並不需要花費很多時間,但對於一位積極的開發人員來說,這段時間已經足夠讓他們開始思考計算飛行時間的下一步。但我已經老了,所以會花幾分鐘伸展一下筋骨,並讀一封電子郵件。我很快便收到 CI 服務的通知,表示一切順利,所以我再次開始變更下一個部分的程序。

持續整合的實務

以上的故事是一個持續整合的說明,希望讓您了解一般程式設計師在工作中的感受。但是,與任何事情一樣,在日常工作中執行此操作時,有許多事情需要釐清。因此,現在我們將逐步了解我們需要執行的關鍵實務。

將所有內容置於版本控制的主線中

如今,幾乎每個軟體團隊都會將其原始碼保存在版本控制系統中,以便每個開發人員都可以輕鬆地找到產品的目前狀態,以及對產品所做的所有變更。版本控制工具允許系統回滾到其開發中的任何時間點,這對於了解系統的歷史記錄非常有幫助,可以使用 差異偵錯 來找出錯誤。在我撰寫本文時,主要的版本控制系統是 git

但儘管版本控制很普遍,有些團隊卻無法充分利用版本控制。我對完整版本控制的測試是,我應該能夠使用極簡設定的環境(例如只安裝了基本作業系統的筆記型電腦)輕鬆建置和執行產品,並在複製儲存庫後執行產品。這表示儲存庫應該可靠地傳回產品原始碼、測試、資料庫結構、測試資料、組態檔、IDE 組態、安裝指令碼、第三方程式庫,以及建置軟體所需的任何工具。

我應該能夠使用只載入作業系統的筆記型電腦,並使用儲存庫取得建置和執行產品所需的一切。

您可能會注意到我說儲存庫應該傳回所有這些元素,這與儲存它們不同。我們不必將編譯器儲存在儲存庫中,但我們需要能夠取得正確的編譯器。如果我簽出去年的產品來源,我可能需要使用去年使用的編譯器來建置它們,而不是我現在使用的版本。儲存庫可以透過儲存連結到不可變資產儲存來執行此操作,不可變資產儲存是指一旦資產以 ID 儲存,我將始終能再次取得完全相同的資產。我也可以使用程式庫程式碼執行此操作,只要我同時信任資產儲存,並始終參照特定版本,而不會是「最新版本」。

類似的資產儲存方案可用於任何太大的東西,例如影片。複製儲存庫通常表示擷取所有內容,即使不需要。透過使用資產儲存的參考,建置指令碼可以選擇僅下載特定建置所需的內容。

一般來說,我們應該將建置任何東西所需的所有內容儲存在原始碼控制中,但不要儲存我們實際建置的任何內容。有些人會將建置產品保留在原始碼控制中,但我認為那是一種異味,表示有更深層的問題,通常是無法可靠地重新建置。將建置產品快取可能會很有用,但它們應該始終被視為可拋棄的,而且通常最好確保立即將它們移除,這樣人們才不會在不應該依賴它們時依賴它們。

此原則的第二個要素是應該很容易找到特定工作片段的程式碼。其中一部分是明確的名稱和 URL 方案,在儲存庫內部和更廣泛的企業內部都是如此。這也表示不必花時間找出版本控制系統中要使用的分支。持續整合依賴於有一個明確的主線,也就是一個單一的共享分支,作為產品的目前狀態。這是將部署到生產環境的下一版本。

大多數使用 git 的團隊將「main」名稱用於主線分支,但我們有時也會看到「trunk」或舊的預設值「master」。主線是中央儲存庫上的那個分支,因此要將提交新增到稱為main的主線,我需要先提交到main的本機副本,然後將該提交推送到中央伺服器。追蹤分支(稱為類似origin/main的東西)是我本機電腦上主線的副本。然而,它可能是過時的,因為在持續整合環境中,每天都會將許多提交推送到主線。

我們應盡可能使用文字檔來定義產品及其環境。我之所以這麼說,是因為儘管版本控制系統可以儲存和追蹤非文字檔,但它們通常不會提供任何功能來輕鬆查看各版本之間的差異。這使得了解所做的變更變得更加困難。未來我們可能會看到更多儲存格式具有建立有意義差異的功能,但目前明確的差異幾乎完全保留給文字格式。即使在那裡,我們也需要使用會產生可理解差異的文字格式。

自動化建置

將原始碼轉換為執行系統通常是一個複雜的過程,涉及編譯、移動檔案、將架構載入資料庫等等。然而,就像軟體開發中此部分的大多數任務一樣,它可以自動化,因此應該自動化。要求人們輸入奇怪的命令或按一下對話方塊會浪費時間,而且是錯誤的溫床。

電腦設計用來執行簡單、重複性的任務。一旦你有人類代表電腦執行重複性的任務,所有電腦就會在深夜聚在一起嘲笑你。

-- Neal Ford

大多數現代程式設計環境包含自動化建置的工具,而且此類工具已經存在很長一段時間。我第一次接觸到它們是在 make,它是最早的 Unix 工具之一。

建置的任何指令都需要儲存在儲存庫中,實際上這表示我們必須使用文字表示。這樣我們就可以輕鬆檢查它們以了解它們如何運作,而且更重要的是,在它們變更時查看差異。因此,使用持續整合的團隊會避免使用需要在使用者介面中到處按一下才能執行建置或設定環境的工具。

可以使用正規程式語言來自動化建置,事實上,簡單的建置通常會擷取為 shell 腳本。但是,隨著建置變得更複雜,最好使用專門設計用於建置自動化的工具。部分原因是因為此類工具會針對常見的建置任務內建函式。但主要原因是建置工具最適合使用特定方式來組織其邏輯 - 我稱之為 相依網路 的替代運算模式。相依網路將其邏輯組織成結構為相依圖形的任務。

一個極其簡單的相依網路可能會說「測試」任務相依於「編譯」任務。如果我呼叫測試任務,它會查看是否需要執行編譯任務,如果是,則會先呼叫它。如果編譯任務本身有相依項,網路會查看是否需要先呼叫它們,依此類推,沿著相依鏈回溯。像這樣的相依網路對建置腳本很有用,因為任務通常會花費很長時間,如果不需要它們,就會浪費時間。如果自上次執行測試以來,沒有人變更任何原始檔,那麼我就可以不用執行可能很長的編譯。

要判斷任務是否需要執行,最常見且直接的方式是查看檔案的修改時間。如果編譯的任何輸入檔案修改時間都比輸出檔案新,那麼當呼叫該任務時,我們就知道需要執行編譯。

一個常見的錯誤是不將所有內容包含在自動化建置中。建置應包含從儲存庫中取得資料庫架構,並在執行環境中啟動它。我將闡述我早先的經驗法則:任何人都應該能夠在乾淨的機器上,從儲存庫中查看原始碼,發出一個指令,並在自己的環境中執行系統。

雖然一個簡單的程式可能只需要一或兩行指令碼檔案就能建置,但複雜的系統通常有大量的相依性圖,經過微調以將建置事物所需的時間減至最少。例如,這個網站有超過一千個網頁。我的建置系統知道,如果我變更此頁面的原始碼,我只需要建置此頁面。但如果我變更發行工具鏈中的核心檔案,則需要全部重新建置。無論如何,我在編輯器中呼叫相同的指令,而建置系統會找出需要執行的部分。

根據我們的需要,我們可能需要建置不同種類的事物。我們可以建置有或沒有測試程式碼的系統,或建置不同組的測試。有些元件可以獨立建置。建置指令碼應該允許我們為不同的情況建置替代目標。

讓建置自我測試

傳統上,建置是指編譯、連結,以及讓程式執行所需的所有額外工作。程式可能會執行,但這並不表示它執行正確。現代的靜態型別語言可以偵測許多錯誤,但有更多錯誤會從網路上流出。如果我們想要像持續整合要求的那樣頻繁整合,這是一個關鍵問題。如果錯誤進入產品,我們將面臨在快速變更的程式碼庫中執行錯誤修正的艱鉅任務。手動測試太慢,無法應付變更的頻率。

面對此情況,我們需要確保錯誤一開始就不會進入產品。執行此項任務的主要技術是全面的測試套件,在每次整合之前執行一次,以找出可能出現的錯誤。當然,測試並非完美,但它可以找出許多錯誤,足以發揮其效用。我使用的早期電腦在開機時會執行可見的記憶體自我測試,這讓我想起將其稱為自我測試程式碼

撰寫自我測試程式碼會影響程式設計師的工作流程。任何程式設計任務都結合了修改程式功能以及擴充測試套件以驗證此變更行為。程式設計師的工作並非僅在於讓新功能正常運作,還包括自動化測試以證明其運作正常。

自本文第一個版本發布的二十年來,我已經看到程式設計環境越來越重視提供程式設計師建置此類測試套件的工具。這方面的最大推動力是 JUnit,最初由 Kent Beck 和 Erich Gamma 撰寫,在 1990 年代後期對 Java 社群產生顯著影響。這啟發了其他語言的類似測試架構,通常稱為Xunit架構。這些架構強調輕量級、對程式設計師友善的機制,讓程式設計師可以輕鬆地與產品程式碼同時建置測試。這些工具通常具備某種圖形進度列,如果測試通過,則顯示綠色;如果測試失敗,則顯示紅色,因此產生了「綠色建置」或「紅色條」等詞彙。

健全的測試套件絕不會允許惡作劇的小惡魔在測試未變為紅色之前造成任何損害。

此類測試套件的測試在於,如果測試為綠色,我們應確信產品中沒有重大錯誤。我想像一個惡作劇的小惡魔,它可以對產品程式碼進行簡單的修改,例如註解掉程式行或反轉條件式,但無法變更測試。健全的測試套件絕不會允許小惡魔在測試未變為紅色之前造成任何損害。而且任何測試失敗都足以讓建置失敗,即使 99.9% 為綠色,仍然是紅色。

自測試程式碼對持續整合來說非常重要,它是必要的先決條件。實作持續整合時,最大的障礙通常是測試技巧不足。

自測試程式碼和持續整合如此緊密結合並不出人意料。持續整合最初是作為 極限編程 的一部分而開發的,而測試一直是極限編程的核心實務。這種測試通常以 測試驅動開發 (TDD) 的形式進行,這是一種指導我們在撰寫新的程式碼之前,必須先修正我們剛才撰寫的測試。TDD 並非持續整合的必要條件,因為測試可以在製作程式碼之後撰寫,只要在整合之前完成即可。但我確實發現,TDD 通常是撰寫自測試程式碼的最佳方式。

測試作為程式碼庫健全性的自動檢查,雖然測試是此類自動驗證程式碼的關鍵元素,但許多程式設計環境提供額外的驗證工具。程式碼檢查器可以偵測不良的程式設計實務,並確保程式碼遵循團隊偏好的格式化樣式,漏洞掃描器可以找出安全性弱點。團隊應評估這些工具,將它們納入驗證程序中。

當然,我們不能依賴測試找出所有問題。正如人們常說的:測試無法證明沒有錯誤。然而,完美並非我們從自測試建置中獲得回報的唯一重點。不完美的測試,經常執行,遠比從未撰寫的完美測試好得多。

每個人每天都將提交推送到主線

沒有程式碼會未整合超過幾個小時。

-- Kent Beck

整合主要是關於溝通。整合允許開發人員告知其他開發人員他們所做的變更。頻繁的溝通讓人們能夠在變更發展時快速得知。

開發人員提交到主線的一個先決條件是他們可以正確建置他們的程式碼。這當然包括通過建置測試。與任何提交週期一樣,開發人員首先更新其工作副本以符合主線,解決與主線的任何衝突,然後在他們的本地機器上建置。如果建置通過,那麼他們就可以自由地推送到主線。

如果每個人都頻繁地推送到主線,開發人員很快就能發現兩個開發人員之間是否有衝突。快速解決問題的關鍵是快速找出問題。由於開發人員每隔幾個小時就提交一次,因此可以在發生衝突後幾個小時內偵測到衝突,在那個時間點,還沒有發生太多事情,而且很容易解決。未被偵測到持續數週的衝突可能會非常難以解決。

程式碼庫中的衝突有不同的形式。最容易找到和解決的是文字衝突,通常稱為「合併衝突」,當兩個開發人員以不同的方式編輯同一程式碼片段時。版本控制工具在第二個開發人員將更新的主線拉入其工作副本後,就能輕鬆偵測到這些衝突。較困難的問題是 語意衝突。如果我的同事變更函數名稱,而我在新加入的程式碼中呼叫該函數,版本控制系統無法協助我們。在靜態類型語言中,我們會得到編譯失敗,這很容易偵測,但在動態語言中,我們無法獲得這樣的協助。即使是靜態類型編譯,當同事變更我呼叫的函數主體,對其功能進行細微變更時,也無法協助我們。這就是擁有自測試程式碼如此重要的原因。

測試失敗會發出警示,表示變更之間有衝突,但我們仍必須找出衝突所在,以及如何解決它。由於提交之間只有幾個小時的變更,因此問題可能隱藏的地方並不多。此外,由於變更不多,我們可以使用差異偵錯來協助我們找出錯誤。

我的經驗法則是一天之中,每個開發人員都應該提交到主線。實際上,有持續整合經驗的開發人員會更頻繁地整合。我們整合的頻率越高,我們尋找衝突錯誤的地方就越少,而且我們修復衝突的速度也越快。

頻繁的提交鼓勵開發人員將工作分解成幾個小時的小區塊。這有助於追蹤進度並提供進度感。人們一開始常常覺得他們無法在幾個小時內完成有意義的事情,但我們發現指導和練習有助於我們學習。

每次推送到主線都應觸發建置

如果團隊中的每個人至少每天整合一次,這應該表示主線保持在健康的狀態。然而,實際上事情還是會出錯。這可能是因為紀律鬆懈、在推入之前忽略更新和建置,開發人員工作空間之間也可能有環境差異。

因此,我們需要確保每個提交都在參考環境中驗證。執行此項作業的常見方式是使用持續整合服務 (CI 服務)來監控主線。(CI 服務的範例是 Jenkins、GitHub Actions、Circle CI 等工具。)每次主線收到提交時,CI 服務就會將主線的頭端檢出到整合環境並執行完整建置。只有在這個整合建置為綠色時,開發人員才能將整合視為完成。透過確保每次推入都有建置,如果我們遇到失敗,我們就知道錯誤在於最新的推入,進而縮小必須尋找修復位置的範圍。

我要在此強調,當我們使用 CI 服務時,我們只在主線上使用它,而主線是版本控制系統參考執行個體上的主要分支。通常會使用 CI 服務來監控和建置多個分支,但整合的重點是讓所有提交共存在單一分支上。雖然使用 CI 服務來為不同的分支執行自動建置可能很有用,但這與持續整合不同,而且使用持續整合的團隊只需要 CI 服務來監控產品的單一分支。

雖然幾乎所有團隊現在都使用 CI 服務,但完全有可能在沒有 CI 服務的情況下執行持續整合。團隊成員可以手動將主線的頭端檢出到整合機器上,並執行建置來驗證整合。但當自動化如此容易取得時,手動流程就沒有什麼意義了。

(在此適時提到我在 Thoughtworks 的同事們為持續整合貢獻了許多開源工具,特別是 Cruise Control - 第一個持續整合服務。)

立即修復中斷的建置

持續整合只有在主線保持在健康狀態下才能運作。如果整合建置失敗,就需要立即修復。正如 Kent Beck 所說:「沒有任何任務比修復建置的優先順序更高。」這並不表示團隊中的每個人都必須停止手邊的工作來修復建置,通常只要幾個人就能讓事情恢復正常運作。這表示有意識地將修復建置優先化為緊急、高優先順序的任務

通常修復建置的最佳方式是從主線還原有問題的提交,讓團隊中的其他人繼續工作。

通常修復建置的最佳方式是從主線還原最新的提交,將系統回復到最後已知良好的建置。如果問題的原因很明顯,則可以用新的提交直接修復,但如果還原主線,則可以讓一些人在獨立的開發環境中找出問題,讓團隊中的其他人繼續使用主線工作。

有些團隊偏好使用Pending Head(也稱為預先測試、延遲或閘道提交)來移除中斷主線的所有風險。為此,持續整合服務需要設定,讓推送到主線進行整合的提交不會立即進入主線。相反地,它們會放置在另一個分支,直到建置完成,並在綠色建置後才會移轉到主線。雖然此技術可以避免任何中斷主線的危險,但有效的團隊很少會看到紅色主線,而且在極少數發生時,其高可見性會鼓勵人們學習如何避免它。

保持建置快速

持續整合的重點在於提供快速的回饋。沒有什麼比花費很長時間的建置更能扼殺持續整合。在此我必須承認,對於什麼被認為是長時間的建置,我這個有點古怪的老傢伙感到有些好笑。我的大多數同事都認為花費一小時的建置完全不合理。我記得團隊夢想著他們能讓建置如此快速 - 而且偶爾我們仍然會遇到很難讓建置達到那個速度的情況。

然而,對於大多數專案來說,XP 指南中十分鐘的建置完全在合理範圍內。我們的大多數現代專案都達到了這個目標。值得集中精力讓它發生,因為每次建置時間減少一分鐘,每次提交時每個開發人員就能節省一分鐘。由於持續整合需要頻繁提交,因此這可以節省很多時間。

如果我們盯著一個小時的建置時間,那麼要達到更快的建置速度可能會像一個令人望而生畏的前景。甚至在一個新專案上工作並思考如何保持快速,都可能令人望而生畏。至少對於企業應用程式,我們發現通常的瓶頸是測試,特別是涉及資料庫等外部服務的測試。

可能最重要的步驟是開始設定部署管線部署管線(也稱為建置管線分階段建置)背後的概念是實際上有多個建置按順序完成。提交到主線會觸發第一個建置,我稱之為提交建置。提交建置是當有人將提交推送到主線時所需的建置。提交建置必須快速完成,因此它會採取一些捷徑,這些捷徑將降低偵測錯誤的能力。訣竅是平衡錯誤尋找和速度的需求,以便良好的提交建置足夠穩定,讓其他人可以繼續處理。

一旦提交建置良好,其他人就可以放心地處理程式碼。然而,還有更多更慢的測試,我們可以開始進行。其他機器可以在建置上執行進一步的測試常式,這些常式需要更長的時間才能完成。

一個簡單的範例是兩階段部署管線。第一階段將執行編譯並執行更為本地的單元測試,其中緩慢的服務會由測試替身取代,例如假的記憶體中資料庫或外部服務的存根程式。此類測試可以執行得非常快,符合十分鐘的準則。然而,任何涉及更大規模互動的錯誤,特別是那些涉及真實資料庫的錯誤,都無法被發現。第二階段建置會執行不同的測試套件,這些套件會命中真實資料庫並涉及更多端對端的行為。此套件可能需要花費幾個小時才能執行。

在這種情況下,人們使用第一階段作為提交建置,並將其用作其主要 CI 週期。如果次要建置失敗,則這可能沒有相同的「停止所有作業」品質,但團隊確實會盡可能快速修正此類錯誤,同時保持提交建置執行。由於次要建置可能會慢很多,因此它可能不會在每次提交後執行。在這種情況下,它會盡可能頻繁執行,從提交階段挑選最後一個好的建置。

如果次要建置偵測到錯誤,這表示提交建置可以執行另一個測試。我們希望盡可能確保任何後續階段的失敗都會導致提交建置中新的測試,這些測試會找出錯誤,因此錯誤會持續修正於提交建置中。這樣一來,每當有問題通過提交測試時,提交測試就會增強。有些情況下無法建置一個快速執行的測試來揭露錯誤,因此我們可能會決定僅在次要建置中測試該條件。幸運的是,在大部分時間裡,我們可以將適當的測試新增到提交建置中。

加速的另一種方法是使用平行處理和多部機器。特別是雲端環境,讓團隊可以輕鬆啟動一組小型伺服器來建置。假設測試可以合理地獨立執行,寫得好的測試可以,那麼使用此類組可以獲得非常快速的建置時間。此類平行雲端建置也可能對開發人員的整合前建置有幫助。

在我們考慮更廣泛的建置程序時,值得一提的是另一類自動化,與相依性的互動。大多數軟體使用由不同組織製作的廣泛相依軟體。這些相依性的變更可能會導致產品中斷。因此,團隊應自動檢查相依性的新版本,並將其整合到建置中,基本上就像它們是另一個團隊成員一樣。這應經常進行,通常至少每天一次,具體取決於相依性變更的頻率。應對執行合約測試使用類似的方法。如果這些相依性互動變為紅色,它們不會像一般建置失敗一樣具有相同的「停止生產線」效果,但確實需要團隊立即採取行動進行調查和修復。

隱藏進行中的工作

持續整合表示在有一點進度且建置正常時立即整合。這通常表示在使用者可見功能完全成形且準備好發布之前進行整合。因此,我們需要考慮如何處理潛在程式碼:未完成功能的一部分程式碼,存在於即時發布中。

有些人擔心潛在程式碼,因為它會將非生產品質的程式碼放入已發布的可執行檔中。執行持續整合的團隊會確保傳送到主線的所有程式碼都是生產品質,以及驗證程式碼的測試。潛在程式碼可能永遠不會在生產中執行,但這並不妨礙它在測試中執行。

我們可以使用關鍵介面來防止程式碼在生產中執行 - 確保提供新功能路徑的介面是我們新增到程式碼庫中的最後一件事。測試仍可以在最後一個介面以外的所有層級檢查程式碼。在設計良好的系統中,此類介面元素應為最小,因此透過簡短的程式設計情節即可輕鬆新增。

使用暗黑啟動,我們可以在將變更顯示給使用者之前,在生產中測試一些變更。此技術對於評估效能影響很有用,

關鍵介面涵蓋潛在程式碼的大多數情況,但在無法使用的情況下,我們會使用功能旗標。每當我們準備執行潛在程式碼時,就會檢查功能旗標,它們會設定為環境的一部分,可能在特定於環境的設定檔中。這樣,潛在程式碼可以在測試中啟用,但在生產中停用。除了啟用持續整合外,功能旗標還讓執行時期切換更容易進行 A/B 測試和金絲雀發布。然後,我們會在功能完全發布後立即移除此邏輯,以便旗標不會混亂程式碼庫。

Branch By Abstraction 是一種管理潛在程式碼的另一種技術,對於程式碼庫中大型基礎架構變更特別有用。這基本上會建立一個內部介面到正在變更的模組。介面接著可以在舊邏輯和新邏輯之間路由,隨著時間逐漸取代執行路徑。我們已經看過這用於切換諸如變更持久性平台等普遍元素。

在導入新功能時,我們應該總是確保在有問題時可以回滾。Parallel Change(又稱 expand-contract)會將變更分解成可逆的步驟。例如,如果我們重新命名資料庫欄位,我們會先建立一個具有新名稱的新欄位,然後寫入舊欄位和新欄位,接著從現有的舊欄位複製資料,然後從新欄位讀取,最後才移除舊欄位。我們可以逆轉這些步驟中的任何一個,如果我們一次進行所有變更,這是不可能的。使用持續整合的團隊通常會尋求以這種方式分解變更,讓變更保持小且容易復原。

在生產環境的複製中測試

測試的重點是在受控條件下,找出系統在生產中會發生的任何問題。其中一個重要部分是生產系統將執行的環境。如果我們在不同的環境中測試,每個差異都會導致在測試中發生的情況不會在生產中發生的風險。

因此,我們希望設定我們的測試環境,使其盡可能精確地模擬我們的生產環境。使用相同的資料庫軟體,具有相同的版本,使用相同版本的作業系統。將生產環境中的所有適當函式庫放入測試環境,即使系統實際上並未使用它們。使用相同的 IP 位址和埠,在相同的硬體上執行它。

虛擬環境讓這項工作比過去容易許多。我們在容器中執行生產軟體,並在測試中可靠地建構完全相同的容器,即使在開發人員的工作區中也是如此。這項工作和成本是值得的,與追蹤由環境不匹配所造成的單一錯誤相比,代價通常很小。

有些軟體設計為在多個環境中執行,例如不同的作業系統和平台版本。部署管線應該安排在所有這些環境中平行測試。

需要注意的一點是,當生產環境不如開發環境時。生產軟體會在連線不穩定的 Wi-Fi,例如智慧型手機上執行嗎?然後確保測試環境模擬不良的網路連線。

每個人都能看到發生了什麼事

持續整合完全是關於溝通,因此我們希望確保每個人都可以輕鬆看到系統狀態和對系統所做的變更。

最重要的溝通事項之一是主線建構的狀態。CI 服務有儀表板,讓每個人都可以看到他們正在執行的任何建構的狀態。他們通常會連結到其他工具,將建構資訊廣播到內部社群媒體工具,例如 Slack。IDE 通常會連結到這些機制,因此開發人員可以在他們用於大部分工作的工具中收到警示。許多團隊只會在建構失敗時發送通知,但我認為在成功時也值得發送訊息。這樣一來,人們會習慣定期發送訊號,並了解建構的長度。更不用說每天收到「做得好」的訊息很不錯,即使它只來自 CI 伺服器。

共用實體空間的團隊通常會為建構準備某種隨時顯示的實體顯示器。這通常採用大螢幕顯示簡化的儀表板的形式。這對於提醒所有人建構中斷特別有價值,通常會在主線提交建構中使用紅色/綠色。

我比較喜歡的一種較舊的實體顯示器是使用紅色和綠色的熔岩燈。熔岩燈的一項特色是,在開啟一段時間後,它們會開始冒泡。這個想法是,如果紅色燈亮起,團隊應該在它開始冒泡之前修復建構。建構狀態的實體顯示器通常會變得有趣,為團隊的工作區增添一些古怪的個性。我對跳舞的兔子有美好的回憶。

除了建構的目前狀態外,這些顯示器還可以顯示有關近期歷史的實用資訊,這可能是專案健全性的指標。回到世紀之交時,我與一個團隊共事,他們過去無法建立穩定的建構。我們在牆上放了一個日曆,上面顯示了一整年,每一天都有一个小方塊。每天,QA 小組都會在收到通過提交測試的一個穩定建構時,在當天貼上綠色貼紙,否則貼上紅色方塊。隨著時間的推移,日曆揭示了建構程序的狀態,顯示出穩定的進步,直到綠色方塊如此普遍,以至於日曆消失了 - 它的目的已經達成。

自動化部署

要執行持續整合,我們需要多個環境,一個用於執行提交測試,其他可能用於執行部署管線的進一步部分。由於我們每天會在這些環境之間移動可執行檔多次,因此我們希望自動執行此操作。因此,擁有允許我們輕鬆將應用程式部署到任何環境的腳本非常重要。

透過現代化的虛擬化、容器化和無伺服器工具,我們可以更進一步。不僅擁有部署產品的腳本,還擁有從頭開始建置所需環境的腳本。這樣,我們可以從現成的精簡環境開始,為產品執行建立所需的環境、安裝產品並執行產品,所有這些都完全自動化。如果我們使用功能標記來隱藏正在進行的工作,則可以設定這些環境並開啟所有功能標記,以便使用所有內在互動來測試這些功能。

這項技術的自然結果是,這些相同的腳本允許我們輕鬆地部署到生產環境。許多團隊使用這些自動化工具每天多次將新程式碼部署到生產環境,但即使我們選擇較不頻繁的節奏,自動部署也有助於加快流程並減少錯誤。由於它僅使用我們用於部署到測試環境的相同功能,因此這也是一個便宜的選項。

如果我們自動部署到生產環境,我們發現自動回滾是一個很方便的額外功能。壞事時不時會發生,如果惡臭的棕色物質擊中旋轉的金屬,能夠快速回到最後已知的良好狀態會很好。能夠自動還原也減少了部署的許多緊張感,鼓勵人們更頻繁地部署,從而快速地向用戶發布新功能。 藍綠部署 允許我們快速發布新版本,並在需要時通過轉移已部署版本之間的流量來同樣快速地回滾。

自動化部署使得設置 金絲雀發布 變得更容易,將產品的新版本部署到我們用戶的子集,以便在向所有人發布之前解決問題。

行動應用程式是很好的範例,說明自動化部署到測試環境是多麼重要,在這種情況下,部署到裝置上,以便在啟動 App Store 的守護程式之前可以探索新版本。的確,任何與裝置綁定的軟體都需要方法來輕鬆地將新版本放到測試裝置上。

部署此類軟體時,請務必確保版本資訊可見。關於畫面應包含一個建置 ID,該 ID 與版本控制相關聯,記錄應讓使用者可以輕鬆查看正在執行的軟體版本,應有一些 API 端點會提供版本資訊。

整合的風格

到目前為止,我描述了一種方法來處理整合,但如果它不是通用的,那麼一定還有其他方法。與任何事物一樣,我給出的任何分類都有模糊的界線,但我發現考慮三種處理整合的風格很有用:預發布整合、功能分支和持續整合。

最古老的是我在 80 年代在那個倉庫中看到的那個 - 預發布整合。這將整合視為軟體專案的一個階段,這個概念是 瀑布流程 的自然組成部分。在這樣的專案中,工作被分為單元,這些單元可以由個人或小組完成。每個單元都是軟體的一部分,與其他單元的互動最少。這些單元會自行建置和測試(術語「單元測試」的原始用法)。然後,一旦單元準備好,我們就會將它們整合到最終產品中。此整合會發生一次,然後進行整合測試,再到發布。因此,如果我們考慮這項工作,我們會看到兩個階段,一個是所有人都並行處理功能,然後是一個單一的整合工作流程。

work on features
work on integration

此風格中整合的頻率與發布頻率相關,通常是軟體的主要版本,通常以月或年為單位。這些團隊會對緊急錯誤修正使用不同的流程,因此它們可以與常規整合時程表分開發布。

當今最流行的整合方法之一是使用 功能分支。在此風格中,功能會分配給個人或小組,很像舊方法中的單元。但是,開發人員不會等到所有單元都完成才進行整合,而是會在完成後立即將其功能整合到主線中。一些團隊會在每次功能整合後發布到生產環境,其他團隊則偏好將幾個功能批次發布。

使用功能分支的團隊通常會希望每個人定期從主線拉取,但這是半整合。如果麗貝卡和我正在處理不同的功能,我們可能會每天從主線拉取,但直到我們其中一人完成我們的功能並整合,將其推送到主線,我們才可以看到彼此的變更。然後,另一個人會在下次拉取時看到該程式碼,並將其整合到他們的工作副本中。因此,在每個功能推送到主線後,每個其他開發人員都將進行整合工作,將此最新的主線推送與他們自己的功能分支結合起來。

when a developer completes a feature...
…all others need to integrate

這僅是半整合,因為每個開發人員都會將主線上的變更合併到他們自己的本機分支。在開發人員推播他們的變更之前,無法進行完全整合,這會導致另一輪的半整合。即使 Rebecca 和我從主線拉取相同的變更,我們也只與那些變更整合,而不是彼此的分支。

使用持續整合,我們每天都會將我們的變更推播到主線,並將其他人的變更拉取到我們自己的工作中。這會導致更多回合的整合工作,但每個回合都小得多。合併幾個小時的程式碼庫工作比合併幾天要容易得多。

持續整合的優點

在討論三種整合風格的相對優點時,大部分的討論實際上都是關於整合頻率。預發佈整合和功能分支都可以使用不同的頻率,並且可以在不變更整合風格的情況下變更整合頻率。如果我們使用預發佈整合,每月發佈和每年發佈之間有很大的差異。功能分支通常以更高的頻率運作,因為整合發生在每個功能個別推播到主線時,而不是等到將一堆單元批次處理在一起。如果一個團隊正在進行功能分支,並且其所有功能的建置工作都不到一天,那麼它們實際上與持續整合相同。但持續整合的不同之處在於它被定義為高頻率風格。持續整合將整合頻率設定為本身的目標,而不是將其約束於功能完成或發佈頻率。

因此,大多數團隊都可以透過在不變更其風格的情況下增加其頻率,看到我將在下面討論的因素的顯著改善。將功能的大小從兩個月縮小到兩週有顯著的好處。持續整合的優點是將高頻率整合設定為基準,設定使其永續的習慣和做法。

降低交付延誤的風險

很難估計執行複雜整合需要多長時間。有時在 git 中合併會很困難,但之後一切運作良好。其他時候合併可能很快,但一個微妙的整合錯誤需要花費數天才找得到並修復。整合之間的時間越長,要整合的程式碼就越多,花費的時間就越長 - 但更糟糕的是不可預測性的增加。

這一切都使預發佈整合成為一種特別形式的惡夢。由於整合是發佈前的最後步驟之一,因此時間已經很緊迫,壓力很大。在一天的晚些時候有一個難以預測的階段意味著我們面臨一個非常難以減輕的重大風險。這就是為什麼我對 80 年代的記憶如此深刻,這絕不是我唯一一次看到專案陷入整合地獄,每次他們修復一個整合錯誤時,就會再出現兩個。

任何增加整合頻率的步驟都會降低此風險。整合工作越少,在新的版本準備好之前,未知的時間就越少。功能分支有助於將此整合工作推送到個別功能串流,因此,如果單獨執行,串流可以在功能準備好後立即推送到主線。

但「單獨執行」這一點很重要。如果其他人推送到主線,那麼我們會在功能完成之前引入一些整合工作。由於分支是孤立的,因此在一個分支上工作的開發人員對於其他功能可能推送到主線,以及整合它們需要多少工作,並沒有太多可見度。雖然高優先順序功能可能會面臨整合延遲的風險,但我們可以透過防止推入較低優先順序的功能來管理此問題。

持續整合有效地消除了交付風險。整合非常小,通常會在沒有意見的情況下進行。困難的整合會花費超過幾分鐘的時間來解決。最糟糕的情況是衝突導致某人從頭開始重新工作,但這仍然不到一天的工作損失,因此不太可能困擾利益相關者的董事會。此外,我們在開發軟體時會定期進行整合,因此我們可以在有更多時間處理問題時面對問題,並練習如何解決問題。

即使團隊沒有定期發布到生產環境,持續整合也很重要,因為它允許每個人確切地看到產品的狀態。在發布之前不需要進行任何隱藏的整合工作,整合中的任何工作都已經包含在內。

減少浪費在整合上的時間

我沒有看到任何嚴肅的研究來衡量花在整合上的時間如何與整合的大小相符,但我的軼事證據強烈表明這種關係不是線性的。如果要整合的程式碼是兩倍,那麼執行整合的時間更有可能是四倍。這就像我們需要三條線才能完全連接三個節點,但需要六條線才能連接四個節點。整合就是關於連接,因此會出現非線性增加,這反映在我同事的經驗中。

在使用功能分支的組織中,個人會感受到很多這種浪費的時間。花費數小時嘗試在主線的重大變更上進行變基令人沮喪。花費幾天時間等待已完成拉取要求的程式碼檢閱,而等待期間主線的另一個重大變更更是令人沮喪。必須將新功能的工作擱置一旁,以除錯在兩週前完成的功能的整合測試中發現的問題,這會降低生產力。

當我們進行持續整合時,整合通常是非事件。我拉下主線、執行建置,然後推入。如果發生衝突,我編寫的少量程式碼在我的腦海中是新鮮的,因此通常很容易看到。工作流程是規律的,因此我們對此很熟練,並且我們有誘因盡可能自動化它。

與許多這些非線性效應一樣,整合很容易成為人們學到錯誤教訓的陷阱。困難的整合可能會造成創傷,以至於團隊決定應該減少整合的頻率,這只會在未來加劇問題。

在此發生的情況是,我們看到團隊成員之間有更密切的合作。如果兩位開發人員做出衝突的決定,我們會在整合時發現。因此,整合之間的時間越短,偵測到衝突的時間越短,我們就能在衝突擴大之前處理衝突。透過高頻率整合,我們的原始碼控制系統變成一個溝通管道,可以傳達那些無法說出的事情。

減少錯誤

錯誤 - 這些是破壞信心、搞亂時程和名聲的討厭東西。已部署軟體中的錯誤會讓使用者對我們生氣。在常規開發期間出現的錯誤會阻礙我們的進度,讓我們更難讓軟體的其餘部分正確運作。

持續整合無法消除錯誤,但它確實讓錯誤更易於尋找和移除。這比較不是因為高頻率整合,而是因為引入了自我測試程式碼。沒有自我測試程式碼,持續整合無法運作,因為沒有適當的測試,我們無法維持健康的幹線。因此,持續整合建立了常規的測試制度。如果測試不足,團隊會很快注意到,並採取矯正措施。如果因為語意衝突而出現錯誤,很容易偵測到,因為只有少量的程式碼需要整合。頻繁整合也適用於差異除錯,因此即使是數週後才發現的錯誤,也能縮小到一個小變更。

錯誤也是累積的。錯誤越多,移除每個錯誤就越困難。部分原因是我們會遇到錯誤互動,其中失敗顯示為多個故障的結果 - 讓每個故障更難找到。這也是心理因素 - 當錯誤很多時,人們會比較沒有力氣去尋找和消除錯誤。因此,由持續整合強化的自我測試程式碼在減少缺陷所造成的問題方面具有另一個指數效應。

這會遇到許多人認為違反直覺的另一種現象。看到變更的導入頻率與錯誤的導入頻率相同,人們會得出結論,要擁有高可靠度的軟體,他們需要降低發布率。這與由 Nicole Forsgren 領導的 DORA 研究計畫 堅決相違。他們發現菁英團隊更快速、更頻繁地部署到生產環境,而且在進行這些變更時,失敗的發生率大幅降低。研究也發現,當團隊在應用程式程式碼儲存庫中有三個或更少個活動分支、每天至少將分支合併到主線一次,而且沒有程式碼凍結或整合階段時,團隊的效能水準會更高。

啟用重構以維持生產力

大多數團隊觀察到,隨著時間推移,程式碼庫會惡化。早期的決策當時很好,但在經過六個月的作業後不再是最佳決策。但變更程式碼以納入團隊所學到的知識,表示在現有程式碼中導入深入的變更,這會導致困難的合併,既耗時又充滿風險。每個人都記得,有人曾經做出對未來來說會是很好的變更,但卻導致數天的努力破壞了其他人的工作。基於那次經驗,沒有人想要重新製作現有程式碼的結構,即使現在每個人都很難建立在上面,因此減緩了新功能的交付。

重構是減輕甚至逆轉這個衰退過程的一項基本技術。定期重構的團隊有一項有紀律的技術,透過使用程式碼的小型、保留行為的轉換來改善程式碼庫的結構。這些轉換的特徵大幅降低了它們導入錯誤的機率,而且可以快速完成,特別是在自測試程式碼基礎的支援下。團隊可以在每次機會中套用重構,改善現有程式碼庫的結構,讓新增新功能變得更簡單、更快速。

但這個快樂的故事可能會被整合問題破壞。兩週的重構階段可能會大幅改善程式碼,但會導致長時間的合併,因為其他所有人都在過去兩週內使用舊結構工作。這會將重構的成本提高到令人望而卻步的程度。頻繁整合透過確保執行重構的人員和其他人定期同步其工作,解決了這個兩難困境。在使用持續整合時,如果有人對我正在使用的核心程式庫進行侵入性變更,我只需要調整幾個小時的程式設計來符合這些變更。如果他們做了一些與我的變更方向衝突的事,我會立刻知道,因此有機會與他們討論,以便我們找出更好的前進方式。

到目前為止,我在這篇文章中提出了幾個關於高頻率整合優點的違反直覺的概念:我們整合的頻率越高,我們花在整合上的時間就越少,而且頻繁整合會導致更少的錯誤。以下是軟體開發中最重要的違反直覺概念:花費大量精力讓其程式碼庫保持健康的團隊可以更快、更便宜地交付功能。投資在撰寫測試和重構的時間,會在交付速度方面帶來令人印象深刻的回報,而且持續整合是讓其在團隊設定中運作的核心部分。

發佈到生產環境是一項商業決策

想像一下我們正在向利害關係人展示一些新建立的功能,而她做出反應說:「這真的很酷,而且會對業務產生重大影響。我們多久才能讓它上線?」如果該功能顯示在未整合的分支上,那麼答案可能是數週或數月,特別是如果在生產路徑上自動化程度較低。持續整合允許我們維護可發布主線,這表示將最新版本的產品發布到生產環境的決策純粹是業務決策。如果利害關係人希望最新版本上線,只需花費幾分鐘執行自動化管道即可。這讓軟體客戶可以更靈活地控制功能發布時間,並鼓勵他們與開發團隊更緊密地合作

持續整合和可發布主線消除了頻繁部署的最大障礙之一。頻繁部署很有價值,因為它允許我們的使用者更快速地取得新功能、對這些功能提供更快速的回饋,並通常在開發週期中變得更具協作性。這有助於打破客戶與開發之間的障礙,而我相信這些障礙是成功軟體開發的最大障礙。

我們不應該使用持續整合的情況

所有這些好處聽起來都很誘人。但像我一樣有經驗(或憤世嫉俗)的人總是對好處清單抱持懷疑。很少有東西沒有代價,而架構和流程的決策通常是權衡取捨的問題。

但我承認,持續整合是少數幾個對有承諾且熟練的團隊來說幾乎沒有缺點的情況之一。偶爾整合所產生的成本非常高,因此幾乎任何團隊都可以透過增加整合頻率來受益。好處停止累積時會有一些限制,但該限制在數小時而不是數天,這正是持續整合的領域。自測試程式碼、持續整合和重構之間的交互作用特別強。我們在 Thoughtworks 已經使用這種方法二十年了,我們唯一的問題是如何更有效地執行它,核心方法已經得到驗證。

但这并不意味着持续集成适合所有人。你可能会注意到我说“对于一个有承诺且熟练的团队来说,几乎没有缺点”。这两个形容词表明持续集成不适合的背景。

我所謂的「投入」,是指一個全職致力於產品的團隊。一個很好的反例是經典的開源專案,其中只有一個或兩個維護者和許多貢獻者。在這種情況下,即使是維護者也只會每週花幾個小時在專案上,他們不太認識貢獻者,而且對於貢獻者的貢獻時間或他們在貢獻時應遵循的標準,也沒有很好的了解。這就是導致功能分支工作流程和拉取請求的環境。在這種情況下,持續整合並不可行,儘管增加整合頻率的努力仍然可能是有價值的。

持續整合更適合全職致力於產品的團隊,這通常是商業軟體的情況。但在經典的開源和全職模式之間,還有很大的中間地帶。我們需要根據自己的判斷,來決定採用哪種整合政策,以符合團隊的投入。

第二個形容詞著重於團隊遵循必要實務的技能。如果一個團隊在沒有強大的測試套件的情況下嘗試持續整合,他們將會遇到各種問題,因為他們沒有篩選錯誤的機制。如果他們沒有自動化,整合將會花費太長的時間,干擾開發流程。如果人們沒有紀律地確保他們對主線的推播都是以綠色建置完成,那麼主線將會一直中斷,妨礙每個人的工作。

任何考慮導入持續整合的人,都必須牢記這些技能。在沒有自測試碼的情況下實施持續整合將無法發揮作用,而且還會對持續整合在執行良好時的情況造成不正確的印象。

話雖如此,我並不認為技能需求特別困難。我們不需要明星開發人員就能讓這個流程在團隊中發揮作用。(事實上,明星開發人員通常會成為阻礙,因為自認為是明星開發人員的人通常沒有紀律。)這些技術實務的技能並不難學習,通常的問題是找到一位好老師,並養成能結晶出紀律的習慣。一旦團隊掌握了流程,通常會感到舒適、順暢且快速。

導入持續整合

說明如何導入持續整合等實務的一件難事,在於路徑在很大程度上取決於你的起點。在撰寫這篇文章時,我不知道你正在處理什麼類型的程式碼、你的團隊具備什麼技能和習慣,更不用說更廣泛的組織背景。像我這樣的人能做的,就是指出一些常見的指標,希望這能幫助你找到自己的路徑。

在導入任何新實務時,清楚了解我們為何這樣做非常重要。我上述的優點清單包含最常見的原因,但不同的脈絡會導致這些原因的重要性有所不同。有些優點比其他優點更難理解。減少整合中的浪費可以解決令人沮喪的問題,而且隨著我們進展,可以輕易感受到。啟用重構以減少系統中的廢料並提升整體生產力則較難看出。我們需要花時間才能看到效果,而且很難感受到反事實。然而,這可能是持續整合最有價值的優點。

上述實務清單指出團隊需要學習哪些技能才能讓持續整合發揮作用。其中有些技能甚至在我們接近高整合頻率之前就能帶來價值。自我測試程式碼即使在提交次數不頻繁的情況下,也能為系統增加穩定性。

一個目標可以是將整合頻率加倍。如果功能分支通常執行十天,找出方法將它們縮短為五天。這可能涉及更好的建置和測試自動化,以及如何將大型任務拆分成較小、獨立整合任務的創意思考。如果我們使用整合前檢閱,我們可以在這些檢閱中納入明確的步驟,以檢查測試範圍並鼓勵較小的提交。

如果你正在開始一個新專案,我們可以從一開始就開始持續整合。我們應該注意建置時間,並在我們開始比十分鐘法則慢時立即採取行動。透過快速行動,我們將在程式碼庫變大到成為主要痛點之前,進行必要的重組。

最重要的是,我們應該獲得一些協助。我們應該找一位之前做過持續整合的人來幫助我們。就像任何新技術一樣,當我們不知道最終結果是什麼時,很難導入它。獲得這項支援可能需要花錢,但否則我們將以浪費時間和生產力作為代價。(免責聲明/廣告 - 是的,我們在 Thoughtworks 在這個領域做了一些諮詢。畢竟,我們已經犯了大部分可以犯的錯誤。)

常見問題

持續整合從何而來?

持續整合是由 Kent Beck 在 1990 年代作為極限編程的一部分而開發出來的實務。當時,預先發布整合是常態,發布頻率通常以年為單位衡量。已經有一股推動採用迭代開發的風潮,並縮短發布週期。但很少團隊會考慮在發布之間採用數週時間。Kent 定義了這項實務,透過他參與的專案開發它,並建立它如何與它所依賴的其他關鍵實務互動。

Microsoft 以執行每日建置(通常在夜間)而聞名,但沒有測試方案或專注於修正缺陷,而這正是持續整合的關鍵元素。

有些人認為 Grady Booch 創造了這個術語,但他只在他的物件導向設計書中的一句話中隨意地使用了這個詞組。他沒有將其視為一種定義良好的實務,事實上它沒有出現在索引中。

持續整合與基幹開發之間的差異是什麼?

隨著 CI 服務變得流行,許多人使用它們在功能分支上執行定期建置。如上所述,這根本不是持續整合,但它導致許多人說(並認為)他們在執行持續整合時,他們正在做一些顯著不同的事情,這會造成很多混淆。

有些人決定透過創造一個新術語:基幹驅動開發來解決這個語意擴散。一般來說,我將其視為持續整合的同義詞,並承認它不太會與「在我們的功能分支上執行 Jenkins」混淆。我讀過一些人試圖制定這兩者之間的區別,但我發現這些區別既不一致也不令人信服。

我沒有使用基幹驅動開發這個術語,部分原因是我不認為創造一個新名稱是對抗語意擴散的好方法,但主要是因為重新命名此技術無禮地抹去了那些人的工作,特別是 Kent Beck,他在一開始就倡導和開發了持續整合。

儘管我避免使用這個術語,但有很多關於持續整合的良好資訊是以基幹驅動開發的名義撰寫的。特別是,Paul Hammant 在他的網站上撰寫了很多優秀的材料。

我們可以在功能分支上執行 CI 服務嗎?

簡單的答案是「是的 - 但你沒有執行持續整合」。這裡的關鍵原則是「每個人每天都提交到主線」。在功能分支上執行自動化建置很有用,但它只是半整合

然而,一個常見的混淆是使用守護程式建置這種方式就是持續整合。混淆來自於將這些工具稱為持續整合服務,更好的術語應該是「持續建置服務」。雖然使用 CI 服務有助於執行持續整合,但我們不應該將工具與實務混淆。

持續整合與持續交付之間的差異是什麼?

持續整合的早期描述重點放在開發人員在團隊開發環境中與主線整合的週期。此類描述並沒有太多討論從整合主線到生產版本的過程。這並不表示它們不在人們的腦海中。像「自動化部署」和「在生產環境的複製中測試」這樣的實務清楚地表明了對生產途徑的認識。

在某些情況下,主線整合後並沒有太多其他事情。我記得 Kent 向我展示他 90 年代後期在瑞士開發的一個系統,他們每天自動部署到生產環境。但這是一個 Smalltalk 系統,生產部署沒有複雜的步驟。在 Thoughtworks 的 2000 年代初期,我們經常遇到生產環境路徑複雜得多的情況。這導致了一個觀念,即除了持續整合之外,還有一項活動來解決此路徑。該活動被稱為持續交付。

持續交付的目標是產品應始終處於我們可以發布最新版本狀態。這基本上確保發布到生產環境是一項業務決策。

對許多人來說,持續整合是將程式碼整合到開發團隊環境中的主線,而持續交付是部署管道其餘部分,朝向生產版本發布。有些人將持續交付視為包含持續整合,其他人則將它們視為密切相關的合作夥伴,通常稱為 CI/CD。另一些人則認為持續交付只是持續整合的同義詞。

持續部署如何與所有這些整合?

持續整合確保每個人至少每天將他們的程式碼整合到版本控制中的主線。然後,持續交付執行確保產品在任何人希望時都可以發布到產品所需的任何步驟。持續部署意味著只要產品通過部署管道中的所有自動化測試,就會自動發布到生產環境。

使用持續部署,作為持續整合一部分推送到主線的每個提交將自動部署到生產環境,前提是部署管道中的所有驗證都是綠色的。持續交付只是確保這一點是可能的(因此是持續部署的先決條件)。

我們如何進行拉取請求和程式碼檢閱?

Pull Request,GitHub 的一個產物,現在廣泛用於軟體專案。它們本質上提供了一種方法來為推送到主線添加一些流程,通常涉及預整合程式碼檢閱,要求另一位開發人員在推送到主線之前批准。它們主要在開源專案的功能分支中開發,以確保專案的維護人員可以審查貢獻是否適當地符合專案的風格和未來意圖。

預整合程式碼檢閱可能會對持續整合造成問題,因為它通常會為整合流程增加顯著的摩擦。我們必須找到某人來進行程式碼檢閱,安排他們的時間,並在接受檢閱之前等待反饋,而不是可以在幾分鐘內完成的自動化流程。儘管某些組織可以在幾分鐘內進入流程,但這很容易變成數小時或數天,打破了持續整合運作的時間安排。

進行持續整合的人員會透過重新調整程式碼檢閱如何融入其工作流程來處理此問題。結對程式設計很受歡迎,因為它在撰寫程式碼時建立了一個持續的即時程式碼檢閱,為檢閱產生更快的回饋迴路。發布/展示/詢問流程鼓勵團隊僅在必要時使用封鎖程式碼檢閱,認識到後整合檢閱通常是更好的選擇,因為它不會干擾整合頻率。許多團隊發現精緻程式碼檢閱是維護健康程式碼庫的一股重要力量,但在持續整合產生有利於重構的環境時,它可以發揮最佳作用。

我們應記住,事前整合檢閱源自於開放原始碼的背景,其中貢獻來自於關聯性較弱的開發人員,而且是即興出現的。在這種環境中有效的做法,需要針對一個由緊密結合的全職人員組成的團隊重新評估。

我們如何處理資料庫?

隨著我們增加整合頻率,資料庫會帶來特定的挑戰。將資料庫架構定義和載入測試資料的指令碼包含在版本控制來源中很容易。但這對版本控制之外的資料,例如生產資料庫,並無幫助。如果我們變更資料庫架構,我們需要知道如何處理現有的資料。

在傳統的預發佈整合中,資料遷移是一項重大的挑戰,通常會成立專門的團隊來執行遷移作業。乍看之下,嘗試高頻率整合會帶來難以承受的資料遷移工作量。

然而,在實際上,觀念的改變消除了這個問題。在 Thoughtworks,我們在早期使用持續整合的專案中面臨這個問題,並透過轉移到由我的同事 Pramod Sadalage 所開發的演化式資料庫設計方法來解決這個問題。這個方法的關鍵在於透過一系列的遷移指令碼來定義資料庫架構和資料,這些指令碼會變更資料庫架構和資料。每個遷移都很小,因此很容易理解和測試。這些遷移會自然地組合,因此我們可以按順序執行數百個遷移,以執行重大的架構變更,並在執行的過程中遷移資料。我們可以將這些遷移儲存在版本控制中,並與應用程式中的資料存取程式碼同步,這讓我們可以建置任何版本的軟體,並擁有正確的架構和正確結構化的資料。這些遷移可以在測試資料和生產資料庫上執行。

最後的想法

大多數的軟體開發都是關於變更現有的程式碼。將新功能新增到程式碼庫的成本和回應時間,在很大程度上取決於該程式碼庫的狀況。一個雜亂的程式碼庫更難修改,而且成本更高。為了將雜亂降到最低,一個團隊需要能夠定期重構程式碼,變更其結構以反映變更的需求,並納入團隊從產品開發工作中學到的經驗。

持續整合對於一個健全的產品至關重要,因為它是這種演化式設計生態系統的一個關鍵組成部分。它與自測試程式碼一起,並獲得自測試程式碼的支援,是重構的基礎。這些技術實務,在極限編程中共同誕生,可以讓一個團隊定期增強產品,以利用變更的需求和技術機會。


進一步閱讀

像這樣的文章只能涵蓋這麼多內容,但這是一個重要的主題,因此我在我的網站上建立了一個指南頁面,為您提供更多資訊。

若要更深入探討持續整合,我建議您閱讀 Paul Duvall 的關於此主題的書(該書獲得了 Jolt 獎 - 超過我曾經獲得的獎項)。若要深入了解持續交付的更廣泛流程,請閱讀Jez Humble 和 Dave Farley 的書 - 該書也擊敗我獲得了 Jolt 獎。

我在管理原始碼分支的模式一文中探討了更廣泛的背景,說明持續整合如何融入選擇分支策略的更廣泛決策空間。一如往常,選擇何時分支的驅動力是知道您將要整合。

持續整合的原始文章描述了我們的經驗,因為 Matt 協助在 2000 年的 Thoughtworks 項目中建立持續整合。

正如我先前所述,許多人使用「基於主幹的開發」一詞來撰寫有關持續整合的文章。Paul Hammant 的網站包含許多實用且有用的資訊。Clare Sudbery 最近撰寫了一份資訊豐富的報告,可透過 O'Reilly 取得。

致謝

首先感謝 Kent Beck 和我在 Chrysler Comprehensive Compensation (C3) 項目的許多同事。這是我的第一次機會看到持續整合在大量單元測試中發揮作用。它向我展示了可能的事物,並激勵我持續了許多年的時間。

感謝 Matt Foemmel、Dave Rice 和所有在 Atlas 上建立並維護持續整合的人。該專案是一個更大規模的 CI 標誌,並顯示了它對現有專案帶來的優點。

Paul Julius、Jason Yip、Owen Rodgers、Mike Roberts 和許多其他開源貢獻者參與建立第一個 CI 服務 CruiseControl 的某些變體。儘管 CI 服務並非必要,但大多數團隊都認為它有幫助。CruiseControl 和其他 CI 服務在推廣和讓軟體開發人員使用持續整合方面發揮了重要作用。

在 2023 年秋季,Michael Lihs 以電子郵件寄給我建議的文章修訂內容,這激勵我進行重大修改。Birgitta Böckeler、Camilla Crispim、Casey Lee、Chris Ford、Clare Sudbery、Evan Bottcher、Jez Humble、Kent Beck、Kief Morris、Mike Roberts、Paul Hammant、Pete Hodgson、Rafael Detoni、Rouan Wilsenach 和 Trisha Gee 審閱並評論了此修訂內容。

我在 Thoughtworks 工作的原因之一是能夠順利存取由有才華的人員執行的實際專案。我拜訪過的幾乎每個專案都提供了持續整合資訊的美味片段。

重大修訂

2024 年 1 月 18 日:發布修訂版本

2023 年 10 月 18 日:開始改寫以更新內容

2006 年 5 月 1 日:完全改寫文章以更新內容並釐清方法說明。

2000 年 9 月 10 日:發布原始版本。