CI/CD必知:落後master分支檢測

Denzel發表於2021-10-26

背景

來公司一年多,業務之餘一直在參與做BU自己的前端釋出平臺;以前我們的構建底層(CI/CD)大多依賴於集團的能力,所以經常一個應用某個迭代釋出之後,其他迭代再構建部署,就會報錯,這時就需要開啟集團系統的構建日誌,會看到類似下面的報錯提示:

master 分支有新的提交,請合併分支後再繼續部署

今年由於有新的業務系統要對接,我們需要有自己的CI/CD底層,而整合這個能力,在最初我也是走了很多彎路,故記錄一下。

為什麼落後檢測很重要

目前多數簡單的前端構建部署,都是以分支來打理;比如我們完成一個需求時:我們會從master主幹拉一個迭代分支,然後我們會用這個分支在開發-測試等環境做構建部署,線上釋出前,我們再將更改合併回主幹。

上面的流程,是一個很通用的流程,但有可能也有其他的做法,比如做的特別前沿的:部署分支每次都是從master主幹拉取,然後合併迭代分支,然後再部署。這種情況不在今天的討論範圍之內,因為這種策略不會有這個落後的煩勞。

回到前面,為什麼我們需要做落後檢測?因為很多時候,一個應用會存在多個人維護,存在多個迭代(A,B),這裡我們假設10.10號這兩個分支都是從主幹同一節點拉取。A,B自己正常開發部署,然後A的迭代在10.22號上線了,釋出完時,程式碼合併到了master;然而B 迭代10.24號上線,但並不知道A有釋出,如果這時部署系統沒有master分支落後檢測,B就順順利利的用分支B上線了,這會造成什麼後果?

  • B上線後,分支合併回主幹報錯(大概率),後面的迭代沒有這個功能,導致後面會有故障發生;
  • A迭代上的功能沒了,線上事故(重一點,3.25)。這鍋算誰的?A的?B的?還是平臺的?在我看來,這個鍋就是平臺的

可能你會問,為什麼上線前不先合master,然後再構建部署上線?我們平臺側,有這樣兩個考慮:

  • 上線前有灰度階段,如果先合了master,如果灰度發現有bug,或者其他迭代要先上,退出灰度就很麻煩,這時主幹分支也會被汙染;
  • 從master重新拉分支,合併程式碼構建部署,如果是手動操作,這樣對開發者而言,會顯得很麻煩;如果平臺自己來做這個操作,會有代價,而且都整合這個能力了,就可以直接再往前建設一點,走前面提到的前沿方案

## 我們的做法

首先要明白,什麼情況,我們稱之為落後master,上個圖:

20211024110303

20211024113530
由於feat/1.0.0 釋出後merge到了master,導致feat/1.0.1 落後master分支, 這種落後與1.0.1提交多少commit無關,只與是否和master分支commit資訊同步相關;

其實去網上查了關於分支比對這方面的資料,發現基本都是shell指令碼處理, 在stackoverflow有一個帖子和我述求基本一致:連結地址

Is there a way to do a diff between my branch and master that excludes changes in master that have not been merged into my branch yet?

裡面的高贊答案,就提到了:git diff branch...master,並很好心了給了官方連結解釋;

裡面提到了一個概念叫merge-base, 等同於兩個分支共同的起點,比如上面兩個分支,這個點就是X;

git diff master...feat/1.0.1 is equivalent to git diff $(git merge-base master feat/1.0.1) feat/1.0.1 is equivalent to git diff commitX commitF

所以當執行:git diff feat/1.0.1...master, 可以看到:

20211024113742

從上圖的結果可以看到,列出的差異點只有A/B兩次提交;

所以兩次差異比較就等同於:git diff X B

That's it, wo got it!!!

但我們的平臺是基於gitlab API的,沒法直接執行這種命令列,幸運的是網際網路是無所不能的,這個API就是:gitlab.Repositories.compare

// Repositories.compare(projectId: string | number, from: string, to: string, options?: Sudo)
const info: any = await gitlab.Repositories.compare(projectId, commitId, 'master');

注意一下from和to的位置,from是feat/1.0.1, to是master,這個很重要;

得到的info結果長下面這樣:

{
  "commit": {},
  "commits": [],
  "diffs": [],
  "compare_timeout": false,
  "compare_same_ref": false,
  "web_url": "https://gitlab.example.com/thedude/gitlab-foss/-/compare/ae73cb07c9eeaf35924a10f713b364d32b2dd34f...0b4bc9a49b562e85de7cc9e834518ea6828729b9"
}

所以如果分支沒有落後master,即master分支沒有新的提交,那麼commit就是一個null值,commit 和 diffs就是一個空陣列;另外要注意compare_timeout的值,如果分支比對工作量過於龐大,則有可能造成超時,compare_timeout為true,那麼這時檢測也是無效的;

20211025175835
還有一個需要注意的,就是這個api第四個引數是個option,option.straight 為真時,這時的diff結果,就不是我們預想的結果,所以呼叫時也要注意。

野路子分享

其實事情,並不像上面描述的那樣順利,最初我們因為時間緊迫沒有發現compare這個API,而是採用了遞迴的方式,就是不斷去回溯分支節點,試圖找到和當前master的commitId相同的節點,最大查詢範圍為向下8層,如果超過8層還沒找到,那就判斷為落後master分支,否則就是安全的,我個人覺得這個演算法實現不低於leetCode的中等題;

在一些簡單的迭代分支管理上,上面的演算法還能奏效,但對於過於複雜的分支,要不就是超時,要不就是超過了8層;超時是因為如果一個節點不匹配,就需要調API拿到下一堆子節點,API呼叫過程是非常耗時的:

// 部分程式碼實現
if (level > MaxLevel || this.globalFinish) {
  return false;
}

const reocords = []
for (let i = 0; i < parentIds.length; i++) {
  const currentId = parentIds[i];
  if (this.lookedIds.has(currentId)) {
    continue;
  }
  this.lookedIds.add(currentId);
  if (currentId === masterId) {
    has = true;
    break;
  }
  const commitInfo = await gitlab.Commits.show(projectId, currentId);
  
  reocords.push({
    gitlab,
    projectId,
    parentIds: commitInfo.parent_ids,
    masterId,
    level: level + 1,
    recursion: true
  });
}

上面這個演算法,在平臺剛上線時,還執行了一兩天,還沒遇到什麼問題;但在知道compare這個演算法時,我們就果斷換了,連夜測試上線,因為官方的API更可靠。

這個分享到此為止,如果你看到了這裡,希望對你有用。

公眾號:前端黑洞

相關文章