將 GitLab CI Pipeline 的執行結果,整合至 Merge Request 中

GitLab 提供了各種方便的 API,如果願意發揮一些巧思,你也可以做到自行將 CI Job 產生的資訊,通通顯示在當前的 Merge Request 頁面中。

在多人協作的軟體開發流程中,運用 Git 及 Branch 已經是一件再平常不過的事情了,想必大家多少都有經歷過多人開發的工作情境,在這樣的情境中,多位開發工程師會各自在不同的 Branch 開發功能,最後透過 Merge Request(MR)將程式碼進行合併。

在這樣的情境中,MR 肩負了重要的任務,它會是一個重要的資訊彙整、進度同步、討論、稽核⋯⋯的關鍵點。

既然 MR 如此重要,同時它又是團隊協作及軟體開發流程中經常發生的一件事,可以的話,我們應該要依據團隊實務上的需要,讓使用者可以更方便的在 MR 頁面彙整並瀏覽各種必要的資訊。

GitLab 原廠當然有注意到 MR 的重要性,因此在 GitLab 免費版中,MR 頁面已經有整合了多項必要的基本資訊。(當然付費版比起免費版擁有更多的整合。)

首先在 GitLab 的 MR 頁面,可以看到 OverviewCommitsPipelinesChanges 四個分頁。後三者比較單純,分別用來讓使用者快速一覽當前的 MR 包含了哪些 Commits、觸發了哪些 Pipelines、以及實際上有哪些檔案被異動了。

Overview 的分頁中,則是資訊大彙整,包含了這項 MR 的基本資訊、最新一條 CI Pipeline 的執行狀態、MR 是否有通過 Code Reviewer 的 Approval、以及團隊夥伴是否有對此 MR 留下各種 Comments。

如上圖所示,在 MR 的 Overview 頁面上,有整合了最新一條 CI Pipeline 的執行狀態,但你只能知道 Pipeline 最終是成功還是失敗,如果你想要知道 CI Pipeline 內產生的更多資訊(例如:CI Job 產出的 Reports 內容),一般來說你只有兩個選擇:

  1. 付錢升級,啟用付費功能(如果有你需要的功能)
  2. 點擊連結,前往 CI Pipeline 查看對應的 CI Job Log 及直接瀏覽 Job Artifacts 的檔案

如果上面兩種方法你都不滿意,那也許你可以考慮一下本文將要介紹的第三種方法!即是運用 GitLab API,讓 CI Pipeline 自動將 CI Job 執行結果發佈到 MR 的 Comments 中,藉此實現在同一個 MR 頁面即可看到更多 CI Job 產生的重要資訊。

操作步驟

建立 Merge Request 的 CI Pipeline

首先這次實驗我們會使用一種特別的 CI Pipeline,即是 Merge Request 之 Pipeline,下面是一個簡單的示範,如何在 .gitlab-ci.yml 中做出此種 Pipeline。

auto-test:
  stage: test
  script:
  - echo "假裝我有跑了某種自動化測試"
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

在上面的 .gitlab-ci.yml,我規劃了一個 CI Job,它的特別之處在於我設置了 rules:,並且條件是 if: $CI_PIPELINE_SOURCE == "merge_request_event",這意味著只有發生 MR 時,這個 Job 才會被執行。

為了要讓它試跑一下,所以我先隨便建立了一個 Feature branch - demo-mr,並且送出一個 Commit,接著建立 Merge request。

如上圖,在 Pipelines 頁面,我們會看到出現了一條 CI Pipeline,並且它會被加上 merge request 的藍色標籤,代表它與其他的 CI Pipeline 不同,是專門搭配 MR 的 Pipeline。

在有 MR 存在的狀況下,每當我在 Branch - demo-mr 送出一批新的 Commits,都會觸發 GitLab CI 執行這條 MR Pipeline;如下圖,隨著我 git push 兩個新的 commits 到 Branch - demo-mr,該 MR Pipeline 就再次被驅動執行。

測試 GitLab API - Create new merge request note

接著我們要測試一下 GitLab API,這裡我們會用到的 API 是 Create new merge request note;這個 API 可以讓我們在目標 MR 新增一則 Comment。

先讓我們用手動的方式測試它。

  1. 先取得使用 API 時必備的 Access Token。這裡看你要使用的是 PersonalProjectGroup access tokens 都可以。
  2. 取得 Project id。如果要手動取得 Project id,你可以進到該 Project 的入口頁,在 Project name 的下方就能看到啦。如下圖,Project id 即是 52056724
  3. 取得 Merge request id。如果要手動取得 MR id,你可以進到 MR 頁面,看一下網址或 MR 的編號即可。以下圖為例, MR id 即是 1
  4. 組合出所需的 curl 指令,就試著送出看看結果。
    #  curl 範例
    ## :id 換成你的 project id
    ## :merge_request_iid 換成你的 MR id
    ## <YOUR_ACCESS_TOKEN> 要換成你的 Token
    curl --request POST \
    --url 'https://gitlab.com/api/v4/projects/:id/merge_requests/:merge_request_iid/notes' \
    --header 'Authorization: Bearer <YOUR_ACCESS_TOKEN>' \
    --header 'Content-Type: application/json' \
    --data '{
        "body": "Use the force Harry!!"
    }'
    
    # 以前面的內容為例 curl 範例
    curl --request POST \
    --url 'https://gitlab.com/api/v4/projects/52056724/merge_requests/1/notes' \
    --header 'Authorization: Bearer 這就換成我的Token啦' \
    --header 'Content-Type: application/json' \
    --data '{
        "body": "Use the force Harry!!"
    }'    
    

如果 API 有執行成功,即可得到類似下圖的成果,順利在 MR 頁面新增了一則 Comment。

測試透過 GitLab API 在 MR Comment 送出多行內容與文字格式

成功使用 API 後,接著要來多一點變化,測試如何送出多行內容到 MR Comment 中。這裡就不賣關子了,要送出多行內容是可行的,只是你要自己在內容中加上 \n 換行符號。

# 繼續使用前面的範例
curl --request POST \
--url 'https://gitlab.com/api/v4/projects/52056724/merge_requests/1/notes' \
--header 'Authorization: Bearer 這就換成我的Token啦' \
--header 'Content-Type: application/json' \
--data '{
    "body": "Use the force Harry!!\n\n——Gandalf"
}'    

上面的範例會得到下圖的成果。

如果你有仔細看,你可能會感到奇怪,為什麼會是兩個 \n\n?如果只是要換行,不是只要一個 \n 就夠了嗎?

這是因為 MR Comments 是支援 Markdown 格式的,如果你熟悉 Markdown 格式,你應該知道只有一個換行時,會變成什麼樣子,讓我們用真實的圖片說明。

如上圖,以純文字的模式編輯 Comment 時,看起來文字內容是有換行,形成兩行文字的排版,但在 Markdown 語法,它會直接將第二行的內容接續在第一行的後面,因此如果你希望的排版是第一行與第二行文字彼此是有間隔的,請記得要使用兩個 \n

既然 Comment 接受的是 Markdown 語法,這裡就讓我們再做一次實驗,透過 API 送一個 Markdown 語法撰寫的表格試試看。

# 繼續使用前面的範例
curl --request POST \
--url 'https://gitlab.com/api/v4/projects/52056724/merge_requests/1/notes' \
--header 'Authorization: Bearer 這就換成我的Token啦' \
--header 'Content-Type: application/json' \
--data '{
    "body": "Here is a Markdown table:\n\n| Name | ID |\n|----------|----------|\n| Captain   | 99   |\n| Cheng Wei   | 9999   |"
}'

這次得到下面的成果。

在 CI Job 中使用 GitLab API

經過前面的實驗,差不多可以實際在 CI Job 內實作了,這裡就讓我延續上一篇文章的範例來當作實驗標的。

在上篇文章中,我嘗試將 OSV Scanner 的 Report 整合進 CI Pipeline 的介面,這次我打算將 Report 的內容,透過本文的方法直接放進 MR 頁面的 Comment 中。

一樣先上 .gitlab-ci.yml

# 沿用上篇文章的 CI Job,先完成 osv scanner
osv scanner:
  image: chengweisdocker/osv-scanner-for-ci:1.4.3
  stage: test
  script:
  - set +e
  - cd $CI_PROJECT_DIR
  - /root/osv-scanner -r --format=table --output=osv-report.txt .
  - cat osv-report.txt
  artifacts:
    paths:
    - osv-report.txt
  # 補一個 rules,讓它也只會在 MR Pipeline 中執行
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

# 接著準備下一個 CI Job 專門丟 GitLab API
push to MR:
  image: python:3.12
  stage: test
  # 相依上一個 Job
  needs:
    - "osv scanner"
  # 需要上一個 Job 的 artifacts
  dependencies:
    - "osv scanner"
  script:
  # 寫了一個簡單的 python,專門用來讀取 osv scanner 的 report
  # 並將內容丟到 GitLab API,
  # 因為只是 DEMO,我就偷懶把檔案直接存進同一個 Project 中了。
  # pip install 裝一下 python 套件
  - pip install requests
  # 執行我的小程式來丟 GitLab API
  - python post_api.py
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"

由於用 shell 想要組合複雜的 json 內容會有一點辛苦,所以在上面的範例中,我是透過執行 python post_api.py 來幫我完成原本透過 curl 執行 API 的動作。

這個簡單的 python 程式碼如下:

# post_api.py
import os
import re
import requests
import json

# 從 ENV 中取得執行 GitLab API 所需的 Token
AUTH_TOKEN = os.environ.get("AUTH_TOKEN")
# 從 ENV 中取得 GitLab Project ID
CI_PROJECT_ID = os.environ.get("CI_PROJECT_ID")

# 從 ENV 中取得變數 CI_OPEN_MERGE_REQUESTS,
# 並且透過文字處理,從中取得 MR ID
ci_open_merge_requests = os.environ.get("CI_OPEN_MERGE_REQUESTS")
match = re.search(r'.*!(\d+)$', ci_open_merge_requests)
MR_ID = match.group(1)

# 讀取 osv-report.txt 的內容
with open('osv-report.txt', 'r') as f:
    CONTENT = f.read()

# 組合出 request 的內容
url = f"https://gitlab.com/api/v4/projects/{CI_PROJECT_ID}/merge_requests/{MR_ID}/notes"
headers = {
    "Authorization": f"Bearer {AUTH_TOKEN}",
    "Content-Type": "application/json",
}

# 由於 report 的內容每一行寬度會超過 Comment 的寬度,
# 所以乾脆將內容塞進 Markdown 的 code fenced blocks 中
data = {
    "body": f"```{CONTENT}```"
}
json_data = json.dumps(data)

response = requests.request("POST", url, headers=headers, data=json_data)

在前面的 Python 小程式中有從 ENV 取得 $AUTH_TOKEN,因此我還需要在 Project settings 中,將具備使用 API 權限的 Access Token 設定為 CI/CD Variables。

最後,就讓 MR Pipeline 跑一下啦!

(查看 CI Job Log,我的 post_api.py 有成功執行!)

最後如下圖,成功將 OSV Scanner 的 Report 送進 MR 頁面的 Comments 啦!

小結

在這篇文章中,我們示範了如何透過 GitLab API 讓我們可以自由的將 CI Job 中必要的資訊彙整在 MR 頁面,在無需修改 GitLab 原始碼與額外付費的前提下,這算是一個尚可接受的折衷辦法。

一樣要提醒一下,本文中的範例仍不夠完美,只是一個簡單的示範,還有很多可以改進的地方,例如:

  1. 是否有必要拆分成兩個 CI Job 分別處理 OSV Scanner 與執行 GitLab API?
  2. 有沒有更方便與安全的方式可以執行 GitLab API?畢竟不同的 Reports 格式可能不同?本文最後使用 Python 來輔助執行 GitLab API 也會導致 CI Job 的執行環境需要相依 Python,這點可能也不太方便?
  3. 本文使用的 GitLab API 在 body 是有字數限制的,如何避免超過上限?
  4. 只把 Reports 的資訊送進 MR Comment 是否足夠?是不是還應該加上更多的資訊,例如是哪一次的 Pipeline 或 CI Job 產生這些內容的並附上對應的超連結。

希望本文有激發到大家的創意,一起來思考一下到底在 MR 頁面,你會想要直接一覽哪些資訊呢?有沒有什麼你覺得很重要,但是在目前的 MR 頁面中是有所欠缺的?有任何想法都歡迎與我分享交流喔!

參考資料

轉貼本文時禁止修改,禁止商業使用,並且必須註明來自「艦長,你有事嗎?」原創作者 Cheng Wei Chen,及附上原文連結。

工商服務

更多文章