
現(xiàn)代軟件的研發(fā)流程基本上均會(huì)配備一定程度的CI/CD(這篇文章解釋了為何需要在企業(yè)里實(shí)施CI/CD),整個(gè)流程主要分為CI和CD部分,這篇文章將圍繞CI部分展開,并通過一個(gè)具體的例子解釋如何0成本在github上構(gòu)建CI。構(gòu)建CI的最佳實(shí)踐離不開Trunk Based Development的分支策略,感興趣的讀者可以通過這篇文章來了解什么是Trunk Based Development。在github上構(gòu)建CI有2個(gè)好處:無需任何費(fèi)用和有大量可以用于構(gòu)建CI的模塊,借助這2個(gè)好處,小規(guī)模團(tuán)隊(duì)可以快速地搭建還不錯(cuò)的CI流程。接下來,讓我們結(jié)合一個(gè)使用Go編寫的Hello World例子以及基于Trunk-Based Development模式來構(gòu)建這個(gè)CI流程。
這篇文章將分為以下幾個(gè)部分來講解:
- 在github上構(gòu)建CI的基本思路
- 在github上構(gòu)建CI的優(yōu)勢(shì)
- 通過一個(gè)Go示例在github上構(gòu)建CI
- 總結(jié)
在github上構(gòu)建CI的基本思路
構(gòu)建CI有2種方式,一種是組建團(tuán)隊(duì)從0開始,另外一種是借助第三方服務(wù)開始。在github上構(gòu)建CI屬于后者,其優(yōu)勢(shì)在于github提供了許多方便開發(fā)者研發(fā)的服務(wù),其中有3種服務(wù)可用于免費(fèi)構(gòu)建CI,它們分別是:免費(fèi)托管源碼,免費(fèi)存儲(chǔ)以及免費(fèi)構(gòu)建服務(wù)(也就是最近推出的Actions服務(wù))。有了這3種服務(wù),任何一個(gè)團(tuán)隊(duì)均可以根據(jù)自身的情況來構(gòu)建CI。接下來,我將基于Trunk-Based Development模式提出實(shí)踐CI的一種方法,這種方法提出了2個(gè)獨(dú)立的流程,并定義了觸發(fā)這2個(gè)流程的條件。
首先,我們需要定義一個(gè)流程(master_workflow),這個(gè)流程的作用是快速響應(yīng)master分支上的每一次改動(dòng)。該分支上每一次改動(dòng)都會(huì)自動(dòng)啟動(dòng)服務(wù)器或虛擬機(jī)來執(zhí)行該流程,并將結(jié)果反饋(比如通過郵件通知的方式)給研發(fā)團(tuán)隊(duì)。這個(gè)流程的主要作用在于每天都確保master分支是健康的,比如語法規(guī)則是正確的,編譯是成功的和單元測(cè)試能通過,因此該流程的一大特點(diǎn)是執(zhí)行周期通常限制在10~30分鐘內(nèi)。這一要求使得構(gòu)成該流程的步驟盡可能的少,下面是構(gòu)成該流程的幾個(gè)步驟:
- 準(zhǔn)備編譯環(huán)境
- 安裝依賴庫
- 獲取源代碼
- 檢測(cè)代碼的合法性
- 編譯源代碼
- 執(zhí)行自動(dòng)化測(cè)試(僅僅包括單元測(cè)試)
- 生成測(cè)試報(bào)告
為了縮短這個(gè)流程的執(zhí)行周期,可以考慮這些方法:將準(zhǔn)備編譯環(huán)境和安裝依賴庫步驟提前合并成一個(gè)步驟(通過Docker技術(shù)),無需在運(yùn)行時(shí)準(zhǔn)備;將檢測(cè)代碼的合法性和編譯源代碼步驟分布在不同的機(jī)器上同時(shí)執(zhí)行;在執(zhí)行自動(dòng)化測(cè)試的步驟中并發(fā)執(zhí)行單元測(cè)試。縮短這個(gè)流程的執(zhí)行周期是為了讓整個(gè)團(tuán)隊(duì)更快地看到每一次修改的結(jié)果,如果這個(gè)修改阻礙了團(tuán)隊(duì)的工作(比如編譯失敗了),那么提交該修改的研發(fā)工作者能夠第一時(shí)間修復(fù)。
其次,我們還需要定義一個(gè)集成流程(integration_workflow),這個(gè)流程的作用是將所有組件集成在一個(gè)完整的壓縮包里,并發(fā)布到一個(gè)共有的存儲(chǔ)空間,以便測(cè)試團(tuán)隊(duì)和DevOps團(tuán)隊(duì)展開后續(xù)的測(cè)試和部署工作。這個(gè)流程不僅包括之前流程所定義的步驟,而且還新增了集成和歸檔步驟,如下所示:
- 準(zhǔn)備編譯環(huán)境
- 安裝依賴庫
- 獲取源代碼
- 檢測(cè)代碼的合法性
- 編譯源代碼
- 執(zhí)行自動(dòng)化測(cè)試(包括單元測(cè)試和集成測(cè)試)
- 生成測(cè)試報(bào)告
- 集成和歸檔
注:此時(shí),執(zhí)行自動(dòng)化測(cè)試包括了集成測(cè)試。因此,從總體而言,這個(gè)流程的運(yùn)行周期會(huì)更長一點(diǎn),通常在30~60分鐘。
以上就是基于Trunk-Based Development模式,在github上構(gòu)建CI的基本思路。首先,我們需要為master分支定義一個(gè)流程,該分支上的每次修改都會(huì)觸發(fā)該流程;其次,我們需要為release分支定義另外一個(gè)流程,該分支上的每一次修改都會(huì)觸發(fā)該流程,并將集成包發(fā)布到一個(gè)共有存儲(chǔ)空間。為何需要定義這2個(gè)流程,讀者可以參考這篇文章。
在github上構(gòu)建CI的優(yōu)勢(shì)
你可以選擇組建一支團(tuán)隊(duì)來打造CI/CD,這種方式需要自己搭建服務(wù)器,安裝軟件(比如Jenkins)和配置,因此所需時(shí)間會(huì)較長。另外,你也可以選擇第三方服務(wù)來搭建CI/CD(比如在github上構(gòu)建CI)。在github上搭建CI有2個(gè)好處,它們分別是免費(fèi)和共享其他人的成果。
github向開發(fā)者提供了3種免費(fèi)的服務(wù)來搭建CI,它們分別是源碼托管,歸檔存儲(chǔ)和Actions服務(wù)。開發(fā)者可以免費(fèi)地將代碼發(fā)布到github上,世界各地的開發(fā)者可以參與進(jìn)來共同開發(fā);開發(fā)者也可以免費(fèi)地使用github所提供的Actions服務(wù)來構(gòu)建流程;開發(fā)者可以將流程輸出的集成包發(fā)布到github提供的存儲(chǔ)服務(wù)里,供用戶使用。
這3種服務(wù)不僅免費(fèi),而且其中Actions服務(wù)提供了可復(fù)用的模塊。這些可復(fù)用的模塊是由全世界的開發(fā)者貢獻(xiàn)的,因此可以直接將這些模塊組合在一起構(gòu)成適合自己的CI流程。比如這篇文章的示例使用了Go相關(guān)的Actions模塊來構(gòu)建上一節(jié)提到的2個(gè)流程。
github平臺(tái)存儲(chǔ)了開發(fā)者的代碼,提供了搭建CI的Action服務(wù),擁有大量可復(fù)用的模塊以及支持存儲(chǔ),此時(shí),開發(fā)者只需要使用這些可復(fù)用的模塊來定義流程,便可以將代碼,Actions服務(wù)和存儲(chǔ)服務(wù)聯(lián)系在一起。而流程的定義是通過yaml文件來完成的,比如上一節(jié)的2個(gè)流程就分別對(duì)應(yīng)著文件master_workflow.yaml和integration_workflow.yaml。
組建一個(gè)團(tuán)隊(duì)來搭建CI,需要準(zhǔn)備服務(wù)器,安裝軟件,用網(wǎng)線連接服務(wù)器等,而借助github,則只需要編寫yaml文件就能快速構(gòu)建出一個(gè)穩(wěn)定的CI,這種轉(zhuǎn)變大大地縮短了搭建CI的時(shí)間,讓開發(fā)者專注于軟件的功能研發(fā)!
接下來讓我們看一個(gè)具體的例子來實(shí)踐在github上構(gòu)建CI
通過一個(gè)Go示例在github上構(gòu)建CI
這個(gè)例子是由Go語言來編寫的,完整的源碼可以到這里獲取,其目錄結(jié)構(gòu)如下所示:
.
|____.github
| |____workflows
| | |____integration_workflow.yaml
| | |____master_workflow.yaml
|____go.mod
|____main.go
|____main_integration_test.go
|____main_test.go
|____Makefile
|____mylib
| |____external_lib.go
| |____external_lib_test.go
其中.github/workflows目錄下有2個(gè)文件master_workflow.yaml和integration_workflow.yaml,由它們構(gòu)成這個(gè)示例的CI,其余部分是Go相關(guān)的源碼。github的Actions服務(wù)會(huì)根據(jù)這2個(gè)文件中的內(nèi)容來啟動(dòng)虛擬機(jī)并執(zhí)行其中定義的步驟。接下來讓我們看看這2個(gè)文件的區(qū)別。
首先,讓我們看看master_workflow.yaml中的內(nèi)容:
name: Daily routines
on:
push:
branches:
- master
pull_request:
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.13
- name: Check out code
uses: actions/checkout@v1
- name: Lint Go Code
run: |
export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14
go get -u golang.org/x/lint/golint
make lint
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.13
- name: Check out code
uses: actions/checkout@v1
- name: Run Unit tests.
run: make test-coverage
以上內(nèi)容主要分為以下2部分:
-
on定義了觸發(fā)條件
這部分的含義是如果有修改推送或者PR到master分支,那么Actions服務(wù)將會(huì)根據(jù)jobs中定義的內(nèi)容來啟動(dòng)虛擬機(jī)并執(zhí)行相關(guān)的步驟
-
jobs定義了執(zhí)行步驟
這部分定義了2個(gè)job,它們分別是lint和test。每一個(gè)job運(yùn)行在一臺(tái)虛擬機(jī)或者容器里,上面運(yùn)行著Ubuntu操作系統(tǒng),job之間是相互獨(dú)立同時(shí)運(yùn)行的。這些job可以引用一些可復(fù)用的Actions模塊(比如lint中的actions/setup-go@v1和actions/checkout@v1),每個(gè)模塊定義了一些執(zhí)行步驟(比如準(zhǔn)備Go環(huán)境和拉取該Go示例的源碼)。
當(dāng)研發(fā)人員向master分支提交代碼,Actions就會(huì)根據(jù)該yaml文件,創(chuàng)建2臺(tái)虛擬機(jī)或者容器,同時(shí)執(zhí)行lint和test。lint的作用是檢查Go的語法問題,而test的作用是運(yùn)行單元測(cè)試并生成測(cè)試報(bào)告。如果其中有一個(gè)job失敗了,那么整個(gè)流程是失敗的,研發(fā)工作者可以及時(shí)看到整個(gè)流程的結(jié)果,如下圖所示:

其次,讓我們看看integration_workflow.yaml中的內(nèi)容:
name: Package routines
on:
create:
tags:
- v*
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.13
- name: Check out code
uses: actions/checkout@v1
- name: Lint Go Code
run: |
export PATH=$PATH:$(go env GOPATH)/bin # temporary fix. See https://github.com/actions/setup-go/issues/14
go get -u golang.org/x/lint/golint
make lint
test:
name: Test
runs-on: ubuntu-latest
steps:
- name: Set up Go
uses: actions/setup-go@v1
with:
go-version: 1.13
- name: Check out code
uses: actions/checkout@v1
- name: Run all tests.
run: make all-tests-coverage
build:
name: Integration
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- name: Check out code
uses: actions/checkout@v1
- name: Validates GO releaser config
uses: docker://goreleaser/goreleaser:latest
with:
args: check
- name: Create package on GitHub
uses: docker://goreleaser/goreleaser:latest
with:
args: release
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
以上內(nèi)容新增了build任務(wù),這個(gè)任務(wù)需要等待lint和test任務(wù)成功之后才會(huì)執(zhí)行。它使用了docker://goreleaser/goreleaser:latest來制作集成包,并發(fā)布到github的存儲(chǔ)服務(wù)(如下圖所示)。除此之外,這個(gè)workflow的test任務(wù)執(zhí)行了單元測(cè)試和集成測(cè)試,之所以執(zhí)行集成測(cè)試,是因?yàn)樵谶@個(gè)階段需要將各個(gè)模塊集成到一起測(cè)試,確保軟件整體是正常工作的,同時(shí)也確保了下游團(tuán)隊(duì)拿到的集成包是可信的。由于添加了集成測(cè)試,因此執(zhí)行該過程所需的時(shí)間會(huì)比前面另外一個(gè)過程所需的時(shí)間長,從而使得團(tuán)隊(duì)無法及時(shí)看到結(jié)果。這也就是為什么我們不希望將集成測(cè)試放在master_workflow.yaml中執(zhí)行。

有了以上2個(gè)文件,研發(fā)工作者只需要專注于軟件功能的研發(fā)。在新功能的研發(fā)階段,研發(fā)工作者只需要修改master分支,其對(duì)應(yīng)的master_workflow流程會(huì)及時(shí)響應(yīng)每一次修改。在準(zhǔn)備發(fā)布新功能的階段,發(fā)布工作者只需要拉取新的分支(比如integration)并為其打上類似v0.0.2的tag,對(duì)應(yīng)的integration_workflow流程將生成打包結(jié)果并歸檔到github的免費(fèi)存儲(chǔ)服務(wù),供其他團(tuán)隊(duì)使用。
總結(jié)
構(gòu)建現(xiàn)代CI的方式有很多種,其中g(shù)ithub提供了一些免費(fèi)的服務(wù)來解決這個(gè)問題,這些服務(wù)分別是源碼托管服務(wù)、Actions服務(wù)和存儲(chǔ)服務(wù)。開發(fā)者只需要定義yaml文件就可以將這3個(gè)服務(wù)串聯(lián)在一起創(chuàng)建出一個(gè)可靠的CI。在github上構(gòu)建CI的優(yōu)勢(shì)有2個(gè):免費(fèi)和共享其他開發(fā)者的成果。任何團(tuán)隊(duì)都可以免費(fèi)地使用這3種服務(wù),除此之外,CI的構(gòu)建者可以使用其他人制作好的可復(fù)用模塊來快速搭建CI。
本文通過一個(gè)Go示例來解釋了基于Trunk-Based Development,在github上構(gòu)建CI。這個(gè)示例定義了2個(gè)yaml文件,它們分別是:master_workflow.yaml和integration_workflow.yaml。每個(gè)文件對(duì)應(yīng)一個(gè)分支,并作用于不同的研發(fā)階段,比如master_worflow流程的主要作用在于確保master分支一直處于健康狀態(tài),而integration_workflow流程則確保對(duì)外發(fā)布一個(gè)可信度較高的集成包。
雖然通過github能夠輕松地構(gòu)建CI,但是它也是有局限的。首先,它的Actions服務(wù)的免費(fèi)套餐是有時(shí)間限制的(2,000 minutes/month),超出了這個(gè)限制則需要升級(jí)為付費(fèi)用戶。其次,通過中國區(qū)訪問它的存儲(chǔ)服務(wù)的延時(shí)較大,從而導(dǎo)致用戶下載集成包的過程變慢了。最后,如果要集成CD,那么需要設(shè)置訪問憑證,從而暴露了風(fēng)險(xiǎn)。
關(guān)于Actions服務(wù)的了解,讀者可以參考以下文章