Monorepo 倉(cāng)庫(kù)代碼質(zhì)量提升實(shí)踐
一、背景
Monorepo 是一種項(xiàng)目代碼管理方式,指單個(gè)倉(cāng)庫(kù)中管理多個(gè)項(xiàng)目,有助于簡(jiǎn)化代碼共享、版本控制、構(gòu)建和部署等方面的復(fù)雜性,并提供更好的可重用性和協(xié)作性。
Monorepo 倉(cāng)庫(kù)包含多個(gè)項(xiàng)目,可能涉及跨業(yè)務(wù)方向乃至跨部門的開(kāi)發(fā)者,迭代頻繁且代碼量通常較大。公共代碼的調(diào)用方往往涉及多個(gè)項(xiàng)目,修改公共代碼的影響范圍廣,回歸成本高。因此可以從以下兩個(gè)角度出發(fā),建立代碼質(zhì)量保障機(jī)制,將代碼變更的風(fēng)險(xiǎn)攔截在開(kāi)發(fā)階段:
-
自動(dòng)化測(cè)試:創(chuàng)建及更新 MR 時(shí)自動(dòng)運(yùn)行測(cè)試用例,確認(rèn)代碼變更對(duì)于歷史功能的影響 -
人工 review:基于 Code Owner 機(jī)制,根據(jù)代碼修改范圍指定對(duì)應(yīng)的代碼負(fù)責(zé)人進(jìn)行 review。
二、以 Git Submodule 的形式引入單元測(cè)試
2.1 名詞解釋
2.2 使用 Git Submodule 引入單測(cè)的原因
從代碼倉(cāng)庫(kù)維度上隔離生產(chǎn)環(huán)境的業(yè)務(wù)代碼和開(kāi)發(fā)環(huán)境的測(cè)試代碼,各自獨(dú)立維護(hù)。一方面避免測(cè)試代碼影響生產(chǎn)環(huán)境,另一方面測(cè)試倉(cāng)庫(kù)的變更風(fēng)險(xiǎn)可控,可以采取更寬松的變更規(guī)則(如無(wú)需 review),降低測(cè)試代碼的變更成本。
2.3 接入步驟
2.3.1 初始化測(cè)試倉(cāng)庫(kù)
測(cè)試倉(cāng)庫(kù)主要用于承載測(cè)試用例、jest 配置文件。
2.3.2 主倉(cāng)庫(kù)引入測(cè)試模塊
git submodule add xxx.git(測(cè)試倉(cāng)庫(kù)git地址) test(本地目錄)
2.3.3 配置測(cè)試環(huán)境
-
待測(cè)項(xiàng)目下安裝測(cè)試依賴
npm i -D @ies/eden-test
-
根據(jù)測(cè)試模塊(子模塊)是否放在待測(cè)項(xiàng)目下,有兩種配置方案
-
方案一:子模塊放在待測(cè)項(xiàng)目的目錄下,在待測(cè)項(xiàng)目中運(yùn)行測(cè)試命令,即可執(zhí)行子模塊中的測(cè)試用例
-
在待測(cè)項(xiàng)目中配置測(cè)試命令,將子模塊中的 jest 配置文件軟鏈到待測(cè)項(xiàng)目根目錄下,并執(zhí)行測(cè)試用例
// src/infrastructure/package.json
{
"scripts": {
// 首次使用需要初始化submodule
"test:init": "git submodule update --init && git submodule foreach git checkout origin/master && ln submodule/jest.config.js(子模塊中jest配置文件的路徑) jest.config.js && eden-test",
// 已有子模塊后運(yùn)行單測(cè)
"test": "ln submodule/jest.config.js(子模塊中jest配置文件的路徑) jest.config.js && eden-test"
}
} -
方案二:子模塊不在待測(cè)項(xiàng)目的目錄下,需要將子模塊中的測(cè)試用例和測(cè)試配置文件軟鏈到待測(cè)項(xiàng)目下,確保測(cè)試過(guò)程中找到子模塊的測(cè)試用例
-
編寫測(cè)試腳本 src/infrastructure/link.sh ,把測(cè)試子模塊的內(nèi)容(測(cè)試用例和 jest 配置)遞歸地軟鏈到待測(cè)項(xiàng)目下。
由于 jest 支持文件軟鏈而暫不支持目錄軟鏈,因此此處采用遞歸創(chuàng)建文件軟鏈的方式,而非直接創(chuàng)建目錄軟鏈。
#!/bin/bash
# 遞歸創(chuàng)建軟鏈腳本
# 軟鏈的源目錄
source_dir=${1:-../../test/infrastructure/src/utils}
# 軟鏈的目標(biāo)目錄
target_dir=${2:-test/utils}
# jest.config.js目錄
config_dir='../../test/infrastructure/jest.config.js'
if [ ! -f 'jest.config.js' ]; then
ln -s $config_dir jest.config.js
fi
# 遍歷源目錄下的所有文件和目錄
for file in $source_dir/*
do
# 獲取文件或目錄名
filename=$(basename $file)
# 如果是目錄,則遞歸調(diào)用腳本創(chuàng)建同名目錄
if [ -d $file ]; then
mkdir -p "$target_dir/$filename"
bash $0 "$file" "$target_dir/$filename"
fi
# 如果是文件,則在目標(biāo)目錄下創(chuàng)建軟鏈
if [ -f $file ]; then
# echo "===from===" "$PWD/$file" "===to===" "$PWD/$target_dir/$filename"
ln -s "$PWD/$file" "$PWD/$target_dir/$filename"
fi
done
echo "軟鏈創(chuàng)建完成!" -
配置測(cè)試命令,運(yùn)行創(chuàng)建軟鏈的腳本并運(yùn)行測(cè)試用例
// package.json
{
"scripts": {
// 首次使用需要初始化submodule
"test:init": "git submodule update --init && git submodule foreach git checkout origin/master && ./link.sh && eden-test",
// 已有子模塊后運(yùn)行單測(cè)
"test": "./link.sh && eden-test"
}
} -
修改子模塊的 jest 配置,在運(yùn)行測(cè)試用例的過(guò)程中支持抓取軟鏈接
// submodule/jest.config.js
module.exports = {
...,
watchman: false,
haste: {
enableSymlinks: true,
},
}
2.3.4 CI 環(huán)境自動(dòng)運(yùn)行
創(chuàng)建 CI 配置文件,定義兩個(gè)執(zhí)行步驟,分別為 Unit Test 和 codecov,其中 Unit Test 用于運(yùn)行單測(cè)、生成測(cè)試覆蓋率文件,codecov 用于定義前端頁(yè)面如何顯示測(cè)試覆蓋率。
測(cè)試模塊的版本管理
默認(rèn)的版本管理方式:
子模塊被提交到主倉(cāng)庫(kù)時(shí),會(huì)記錄子模塊的 commit id。在下一次拉取子模塊時(shí)自動(dòng)切換到對(duì)應(yīng)的 commit id,從而實(shí)現(xiàn)子模塊的版本管理。因此理論上更新測(cè)試用例時(shí),需要去主倉(cāng)庫(kù)更新 commit id。
預(yù)期效果&解決思路:
為了達(dá)到主倉(cāng)庫(kù)無(wú)感知的效果,本方案在 CI 環(huán)境下會(huì)忽略 commit id,執(zhí)行切換分支的操作。切換原則為若測(cè)試倉(cāng)庫(kù)存在與當(dāng)前 MR 的源分支同名的分支,則使用同名分支的測(cè)試用例,否則使用測(cè)試倉(cāng)庫(kù) master 的測(cè)試用例。
使用流程:
在主倉(cāng)庫(kù) A 分支開(kāi)發(fā)時(shí)新增了公共方法(待測(cè)代碼),則需要在測(cè)試倉(cāng)庫(kù)起一個(gè)同名的 A 分支編寫對(duì)應(yīng)的測(cè)試用例,提交 MR 后觸發(fā)測(cè)試流水線,CI 環(huán)境下會(huì)拉取子模塊并切換到 A 分支運(yùn)行測(cè)試用例。在完成開(kāi)發(fā)后再分別將主倉(cāng)庫(kù)和測(cè)試倉(cāng)庫(kù)的 A 分支合入 master。
運(yùn)行單測(cè)的核心命令
# 初始化子模塊
- git submodule update --init
# 進(jìn)入子模塊目錄下切換分支
- cd test/infrastructure
# 優(yōu)先使用同名分支的測(cè)試用例,master的測(cè)試用例作為兜底
- git checkout master
- git show-branch $CI_EVENT_CHANGE_SOURCE_BRANCH &>/dev/null && git checkout $CI_EVENT_CHANGE_SOURCE_BRANCH || echo $CI_EVENT_CHANGE_SOURCE_BRANCH not exist
# 回到待測(cè)項(xiàng)目下執(zhí)行測(cè)試用例
- cd ../../src/infrastructure
- mkdir -p test
- cp -r ../../test/infrastructure/src/utils test
- npm i
- npm run test
2.4 接入效果
2.3.1 本地開(kāi)發(fā)
主倉(cāng)庫(kù)和測(cè)試倉(cāng)庫(kù)的代碼獨(dú)立提交
2.3.2 CI 環(huán)境自動(dòng)運(yùn)行
創(chuàng)建 MR 后自動(dòng)運(yùn)行流水線,在 MR 界面查看測(cè)試用例運(yùn)行結(jié)果及覆蓋率。當(dāng)測(cè)試用例執(zhí)行失敗,或測(cè)試覆蓋率不達(dá)標(biāo)時(shí),阻塞 merge 操作。
三、人工 review
3.1 引入 Code Owner 機(jī)制
3.1.1 為什么需要 Code Owner 機(jī)制
我們?cè)谌粘i_(kāi)發(fā)時(shí),通常會(huì)約定一些 review 規(guī)則,如:
-
修改公共代碼時(shí),存在一定風(fēng)險(xiǎn),需要對(duì)應(yīng)的模塊負(fù)責(zé)人 review。 -
在代碼按領(lǐng)域組織的場(chǎng)景下,修改領(lǐng)域?qū)哟a需要對(duì)應(yīng)的領(lǐng)域負(fù)責(zé)人 review。
在落地過(guò)程中,存在的問(wèn)題可能包括:忘記自己修改了公共代碼、不確定模塊負(fù)責(zé)人從而增加溝通成本等,因此需要從流程上規(guī)范 review 規(guī)則,對(duì)公共代碼的修改進(jìn)行嚴(yán)格把關(guān)。
為了滿足更細(xì)粒度的準(zhǔn)入控制,引入 CODE OWNERS 機(jī)制,基于 change 的代碼變更添加 reviewer 并要求這些 reviewer approve 后才能合入。
3.1.2 接入步驟
在 CODEOWNERS 文件中定義 review 規(guī)則,每條規(guī)則包含匹配路徑及 reviewer。每行一個(gè)規(guī)則,從上到下匹配,后匹配到的規(guī)則會(huì)覆蓋先匹配到的規(guī)則。
# 每行一個(gè)規(guī)則,從上到下匹配,后匹配到的規(guī)則會(huì)覆蓋先匹配到的規(guī)則
# 例如:
/sample_feature/ @user1 @user2
*.js @user3
/sample_feature/*.js @group1
路徑語(yǔ)法同 gitignore:
所有空行或者以 # 開(kāi)頭的行都會(huì)被 Git 忽略。
可以使用標(biāo)準(zhǔn)的 glob 模式匹配。
星號(hào)(*)匹配零個(gè)或多個(gè)任意字符; [abc]匹配任何一個(gè)列在方括號(hào)中的字符(這個(gè)例子要么匹配一個(gè) a,要么匹配一個(gè) b,要么匹配一個(gè) c); 問(wèn)號(hào)(?)只匹配一個(gè)任意字符; 如果在方括號(hào)中使用短劃線分隔兩個(gè)字符,表示所有在這兩個(gè)字符范圍內(nèi)的都可以匹配(比如 [0-9] 表示匹配所有 0 到 9 的數(shù)字)。 使用兩個(gè)星號(hào)(**)表示匹配任意中間目錄,比如 a/**/z可以匹配 a/z, a/b/z 或 a/b/c/z 等。匹配模式可以以(/)開(kāi)頭防止遞歸。
匹配模式可以以(/)結(jié)尾指定目錄。
要忽略指定模式以外的文件或目錄,可以在模式前加上驚嘆號(hào)(!)取反。
3.1.3 接入效果
創(chuàng)建 MR 后,在 MR 界面會(huì)查看命中的各個(gè)代碼路徑匹配規(guī)則、對(duì)應(yīng)的 owner 及滿足情況。(需要確保目標(biāo)分支下已有 OWNERS 文件)
3.2 CR 通知收斂到飛書話題群
3.2.1 解決的問(wèn)題
-
部分 MR 命中的 owner 規(guī)則比較多,需要在群里一一@或者私聊,存在一定的觸達(dá)成本 -
現(xiàn)有的觸達(dá)方案中,通知內(nèi)容包含 MR 所有的狀態(tài)變更(發(fā)起、approve、評(píng)論、更新、合入等),因此通知比較頻繁、觸達(dá)效率較低
3.2.2 解決思路
-
將發(fā)送群通知提醒 review 的能力集成到 MR 流水線中,并支持通過(guò) MR 標(biāo)題中的 WIP 標(biāo)識(shí)來(lái)決定是否發(fā)送群通知。 -
結(jié)合飛書話題群的功能,在發(fā)送群通知時(shí)@相關(guān)的 reviewers 及發(fā)起人,相關(guān)人員自動(dòng)訂閱話題,在話題下回復(fù)的消息均會(huì)以消息卡片的形式推送給相關(guān)人??梢杂糜谠?MR 狀態(tài)變更時(shí)提醒相關(guān)人員,同時(shí)將 MR 相關(guān)的討論收斂到一個(gè)話題下,提升溝通效率。
四、總結(jié)
在代碼評(píng)審階段,根據(jù)代碼修改范圍邀請(qǐng)?jiān)u審人、運(yùn)行單元測(cè)試,可以在較大程度上保障代碼功能符合預(yù)期,降低代碼修改帶來(lái)的質(zhì)量風(fēng)險(xiǎn)。后續(xù)可以嘗試與 AI 結(jié)合,例如集成大模型自動(dòng)生成測(cè)試用例、review 代碼的能力,進(jìn)一步降低質(zhì)量保障方案的實(shí)現(xiàn)成本,從而提升研發(fā)效率。
