大家好,這篇文要記錄一下自己實作 GitLab 團隊在 Merge Request 階段中會發布 Bundle Size 比較表的功能。這既不是一個 GitLab, Github 或任何原始碼管理平台內建的功能、也沒有任何整合此功能的套件外。它更像是一個 GitLab 團隊仍在實驗階段的自建流程。也因此,我在實作中是直接將 GitLab 專案 Clone 下來慢慢翻閱,最後整理出一套流程的。也就是說它不是標準解答,但也許可以作為參考。
要實作此功能,對以下工具/技術會需要有基本的了解:
- Danger JS
- Gitlab CI or Github Actions
- Merge Request or Pull Request
- Vite, Webpack or Rspack
- Ajax (我們會串接 GitLab File API)
我會盡可能清楚地描述實作流程與思路,因此如果不了解上述工具或技術也沒關係,讀到覺得不是很清楚的段落可以直接留言或是把原始碼丟給 AI 請它說明。
我把實作流程整理成以下步驟:
- 產生測試用 Repo 並同步在 GitLab
- 在專案中導入
vite-bundle-visualizer
,建立容量打包記錄檔的邏輯 - 建立儲存容量打包記錄檔用的 Repo
- 建立 GitLab CI 用的 yaml 檔,撰寫
push
和merge_requests
的 CI 腳本 - 導入 Danger JS,使其能在 Merge Request 中留言
產生測試用 Repo 並同步在 GitLab
這裡我使用 Vite 的 react-ts
作為專案測試的模板,在這篇文中僅會基於需要發 MR 的需要對專案做異動,所以即使完全不懂 React 也不會影響看這篇文。
我們先在 GitLab 建立一個空的 Repository(不要勾選自動建立 README),接著進行以下操作:
npm create vite@latest <project-name> -- --template react-ts
cd <project-name>
npm install
git init --initial-branch=main
git remote add origin <your-gitlab-origin-project-url>
git add .
git commit -m "Initial commit"
git push -u origin main
至此為止都是依照 Vite 或 GitLab 的指示操作。完成後在 Repository 頁進行重新整理應該會看到以下畫面:
在專案中導入 vite-bundle-visualizer
,建立容量打包記錄檔的邏輯
這個步驟源自於我參考 GitLab 團隊的自建 CI。GitLab 使用的是 webpack-bundle-analyzer,這是一個可以分析打包容量的套件,Vite 相對應的工具是 vite-bundle-visualizer。首先我們先在專案安裝 vite-bundle-visualizer
:
npm install -D vite-bundle-visualizer
接下來我們將 package.json
scripts
的 build
打包邏輯改為使用 vite-bundle-visualizer
:
"build": vite-bundle-visualizer -t list -o vite-report/stats.yaml
嘗試在專案中輸入:
npm run build
應該會產出一個裡面包含 stat.yml
的 vite-report
。將 vite-report/
加入到 .gitignore
中,因為我們在 CI 階段中產生它就好:
// .gitignore
vite-report
建立儲存容量打包記錄檔用的 Repo
為了能在不同 CI 中比較 Bundle Size 的大小,GitLab 採用的方法是一個空的 Repository 搭配 Access Token 和 GitLab API 將資料在 CI 階段中儲存到 Repository 中。
建立一個 Repository,我將其命名為 vite-bundle-visualizer-storage
。建立一個 bundleStorage
資料夾,這個資料夾會被用於儲存 Bundle Size 大小的 .json
檔,我們可以透過在資料夾內建立一個 .gitkeep
的空檔案來確保這個資料夾可以被推送至 GitLab。完成後將其 commit 並推送至 GitLab:
接下來我們來建立 vite-bundle-visualizer-storage
專案的 Access token,Access token 的概念有點抽象但並不複雜。
例如當我們今天買了票進去一間美術館看展,工作人員可能會給我們一個手環,並告訴我們說:「當日憑手環都可以再次進場。」只要我們沒有銷毀手環,我們在這一天就能無限次進出這個美術館,這就相當於美術館給了我們一個期限為一天,擁有參觀權限的 Access token。
Access token 就像我們在一些網站上登入時,伺服器端可能會簽發給瀏覽器端的 JWT,其實本質上就是一個字串而已,只要在發 API 請求到 GitLab 時依照 GitLab 的規範夾帶在 Request Header 中,就能依照該 Access token 擁有的權限直接對 Repository 進行操作,例如直接在 Repository 中抓取、建立、修改甚至刪除檔案。
在 GitLab 上 Repository 的頁面中,點選 Setting>Access Tokens>Add new token:
Token name 我取名為 STORAGE_API_TOKEN;到期日我選擇了一個月後(最久是一年後),role 我選擇 Developer,scopes 則是選擇了 api, read_api, read_repository, write_repository。
點選 Create project access token,會帶你回到原本頁面,並提供 token 的字串。請務必在離開這頁前將 token 內容先複製到記事本之類的地方放好,稍後會使用到:
最後在離開此 Repository 之前,要確保 main branch 的保護關閉的,避免我們無法透過 API 將檔案推送到 Repository。我們可以在 Settings>Repository>Protected branches 找到對應區塊,點選 Unprotect 即可。(如果你在 CI 上看到 “You are not allowed to push into this branch”,有可能會是這個原因)
建立 GitLab CI 用的 yaml 檔,撰寫 push
和 merge_requests
的 CI 腳本
當我們要將專案部署到機器上時,一定會有一些固定的流程要跑,例如跑 test, typecheck, lint, build 等,跑完之後也需要將資料同步到遠端的機器。如果每一次我們在部署時都要手動跑這些流程,除了可能因人為疏失造成問題外、也會浪費工程師額外的精力。CI/CD 能夠藉由機器幫助我們完成這些步驟,並能確保每次都會把所有流程走一次。
讓我們回到透過 Vite 建立的主專案,於根目錄建立 .gitlab-ci.yml
。這支檔案會藉由一系列的設定告訴 GitLab 我們的 CI 流程要怎麼跑:
image: node:lts
stages:
- build
- merge
variables:
GITLAB_PROJECT_ID: $CI_PROJECT_ID
GITLAB_STORAGE_PROJECT_ID: 61678948
GITLAB_API_URL: https://gitlab.com/api/v4/projects
STORAGE_BOT_NAME: "Storage Bot"
STORAGE_BOT_EMAIL: "storagebot@gmail.com"
# triggers only when main branch is updated
build-analyze:
stage: build
cache:
key:
files:
- package-lock.json
prefix: npm
paths:
- node_modules/
script:
- npm install
- npm run build
- mkdir -p bundle-size-review
- chmod +x scripts/bundle_size_review.sh
- scripts/bundle_size_review.sh
artifacts:
when: always
name: bundle-size-review
expire_in: 1 hour
paths:
- bundle-size-review/
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# triggers only when merge request is created
danger-compare:
stage: merge
script:
- npm install
- npm run build
- mkdir -p bundle-size-review
- chmod +x scripts/bundle_size_compare.sh
- scripts/bundle_size_compare.sh
artifacts:
when: always
name: bundle-size-review
expire_in: 1 hour
paths:
- bundle-size-review/
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
這邊說明一下各個設定:
- image:我們可以讓 GitLab CI 在 Docker Container 中跑,這邊的 image 指的就是 Docker Container 中採用的映像檔,這邊我們使用
node:lts
版本。 - stages:我們可以運用 stages 來定義我們的執行階段,這邊定義了
build
和merge
兩種。 - variables:CI 上定義的環境變數,主要是由 bundle size runner 取用。GITLAB_PROJECT_ID 為 Vite 建立的專案 ID,這邊由於 CI 會在 Vite 建立的專案,可直接沿用 GitLab 提供的預先定義變數;GITLAB_STORAGE_PROJECT_ID 為 Storage Repository 的 Project ID,後面三個變數,GITLAB_API_URL 除非網域是自定義的否則不必提供;STORAGE_BOT_NAME 和 STORAGE_BOT_EMAIL 為 Storage Repository 在儲存 bundle size 記錄時的 author name 和 author email,也可不提供。
- build-analyze:這是一個我們定義的任務,隸屬於
build
階段,cache 讓我們可以快取node_modules
來減少每次安裝依賴所需的時間;script
是我們要執行的腳本,這邊會先build
,接著建立bundle_size_review
資料夾,並運行稍後會提供的 shell 腳本;artifacts
可以讓我們保存執行階段後的工作,因為我在這篇文不會花篇幅說明 bundle size runner 運作的方式,這邊設定了bundle-size-review
是方便我們跑完腳本後,可以查看bundle-size-review
裡面到底放了什麼;rules
則是觸發條件,這邊設定的意思是此工作只會在main
分支更新時觸發。 - danger-compare:這也是我們定義的任務,隸屬於
merge
階段。danger-compare 指的是我們會採用Danger
這個工具去發布比較 bundle size 大小的回覆,而非進行什麼危險的比較。與build-analyze
主要的兩個不同,一個是script
執行的腳本內容不同,另一個則是rules
,此工作只會在merge_request
發生時觸發。
接下來我們在 Vite 建立的專案中分別建立 scripts/utils.sh
, scripts/bundle_size_review.sh
和 scripts/bundle_size_compare.sh
:
# scripts/utils.sh
function run_timed_command() {
local cmd="${1}"
local metric_name="${2:-no}"
local timed_metric_file
local start=$(date +%s)
echosuccess "\$ ${cmd}"
eval "${cmd}"
local ret=$?
local end=$(date +%s)
local runtime=$((end-start))
if [[ $ret -eq 0 ]]; then
echosuccess "==> '${cmd}' succeeded in ${runtime} seconds."
if [[ "${metric_name}" != "no" ]]; then
timed_metric_file=$(timed_metric_file $metric_name)
echo "# TYPE ${metric_name} gauge" > "${timed_metric_file}"
echo "# UNIT ${metric_name} seconds" >> "${timed_metric_file}"
echo "${metric_name} ${runtime}" >> "${timed_metric_file}"
fi
return 0
else
echoerr "==> '${cmd}' failed (${ret}) in ${runtime} seconds."
return $ret
fi
}
function echosuccess() {
local header="${2:-no}"
if [ "${header}" != "no" ]; then
printf "\n\033[0;32m** %s **\n\033[0m" "${1}" >&2;
else
printf "\033[0;32m%s\n\033[0m" "${1}" >&2;
fi
}
function echoerr() {
local header="${2:-no}"
if [ "${header}" != "no" ]; then
printf "\n\033[0;31m** %s **\n\033[0m" "${1}" >&2;
else
printf "\033[0;31m%s\n\033[0m" "${1}" >&2;
fi
}
# scripts/bundle_size_review.sh
#!/usr/bin/env bash
source scripts/utils.sh
if [[ -z "${CI:-}" ]]; then
echo 'Not running in a CI context, skipping bundle analysis'
exit "0"
fi
# Install the package using npm
run_timed_command "npm install -g https://gitlab.com/EasonLin0716/vite-bundle-visualizer-generator.git"
# Create smaller analysis.json
run_timed_command "webpack-entry-point-analyser --from-file ./vite-report/stats.yaml --json ./bundle-size-review/analysis.json --sha ${CI_COMMIT_SHA}"
# scripts/bundle_size_compare.sh
#!/usr/bin/env bash
source scripts/bundle_size_review.sh
# Run comparison
run_timed_command "webpack-compare-reports --job ${CI_JOB_ID} --to-file ./bundle-size-review/analysis.json --markdown ./bundle-size-review/comparison.md --sha ${CI_COMMIT_SHA}"
# Execute danger
run_timed_command "npm i -g danger"
run_timed_command "danger ci"
utils.sh
這支檔案就是宣告一些方便在 CI 上看指令執行情形的 function, bundle_size_review.sh
這支檔案會先做一層基本防呆,確認執行環境為 CI 環境,接著會安裝 bundle size runner,由於我沒有將其釋出為獨立的套件,因此直接跑 npm install -g https://gitlab.com/EasonLin0716/vite-bundle-visualizer-generator.git
。這個 GitLab Repo 中包含兩個 bin,裡面分別是 webpack-entry-point-analyser
和 webpack-compare-reports
。剩餘的指令分別是:
- 執行
webpack-entry-point-analyser
,這個指令最終會將轉換後的 json 檔寫入bundle-size-review/analysis.json
- 執行
webpack-compare-reports
,這個指令會比較分支與分支來源 bundle size 的大小 - 執行 Danger CI,
Danger
套件會讀取bundle-size-review/comparison.md
並發佈在 Merge Request 的 Comment 中。
到目前為止已經很接近需求了,讓我們來導入 Danger JS 的相關設定。
導入 Danger JS,使其能在 Merge Request 中留言
最後我們來準備 Danger JS 所需要的要素,分別是 Vite 建立的專案本體的 Access Token 及 dangerfile.js
:
import { message, danger, warn, markdown } from "danger"
const fs = require('fs')
const modifiedMD = danger.git.modified_files.join("- ")
message("Changed Files in this PR: \n - " + modifiedMD)
fs.readFile('./bundle-size-review/comparison.md', 'utf8', (err, data) => {
if (err) {
console.error(err)
return
}
markdown(data)
})
if (danger.gitlab.mr.title.includes("WIP")) {
warn("PR is considered WIP")
}
dangerfile.js
是 Danger CI 執行時會讀取的檔案,它會在讀取時執行其中撰寫的資訊。以這份 dangerfile 來說,他很單純地就是讀取我們產出的 comparison.md
並將其印出於 Merge Request 的 comment 中。
Access Token 這塊,原先由 Storage Repository 產了一個,因為我們需要能在 Merge Request 發布 comment,所以在 Vite 產生的專案也建立一個:
名稱為 DANGER_GITLAB_API_TOKEN,Role 我選擇 Reporter,權限部分我選擇了 api
與 write_repository
,建立後一樣記得將 Access Token 的字串先存在一個地方避免丟失。
最後我們來建立 CI/CD 所需的環境變數,到 Setting>CI/CD>Variables 將 Variables 展開,並新增兩個環境變數供 runner 使用。由於 runner 本身取用的變數名稱是固定的,此處的變數名稱務必與下列提供的變數名稱相同:
- DANGER_GITLAB_API_TOKEN:這個是剛剛才建立好的 Access Token,將 key value 分別設定為 DANGER_GITLAB_API_TOKEN 和 Access Token 的值
- STORAGE_BOT_GITLAB_API_TOKEN:還記得稍早建立 Storage Repo 時有先存一個 Access Token 起來嗎?將 key value 分別設定為 STORAGE_BOT_GITLAB_API_TOKEN 和 Access Token 的值
一切就緒!接下來將 Vite 建立的專案異動 commit,並推送至主線:
git commit -m <your-commit-message>
git push -u origin main
如果 CI 設定有符合預期的話,應該可以看到 main branch 的 CI 在跑:
在 CI 完成後,如果到 Storage Repo 去查看,應該會發現 CI 直接透過 GitLab API 將 runner 產出的檔案推送上來:
接下來可以試著發布 Merge Request。GitLab Merge Request 或 MR,類似於 Github 常聽到的 Pull Request 或 PR,簡單地說就是「我想要改 Repository,請求 Repository 的擁有者採納我的改動」,我們在團隊協作時,會針對 Repository 中依據所需進行一些調整,絕大多數情況下為了避免直接推送主線造成的非預期問題,我們會在獨立分支中工作,並在完成後將改動發布到程式碼託管平臺上讓團隊審核,並在確認無誤後才進行合併或拉取。
透過這些操作,我們可以建立分支並推送改動:
git checkout -b <branch-name>
# 可以到 src/App.tsx 增加一些文字
git add .
git commit -m <commit-message>
git push -u origin <branch-name>
在 Repository 頁面中,藉由 Merge requests 區塊並點選 New merge request
選擇推送的分支便可以發布 Merge Request。
在 Merge Request 的 CI Pipeline 跑完後,Danger CI 應該就會發布包含異動紀錄的 comment 在 Merge Request 中:
若有看到 comment 就表示成功了。CI 上大致的執行流程如下:
當 main 被推送時
- 進行 Repository 的依賴安裝
- 打包 Repository 的原始碼
- 安裝 bundle size runner
- 跑 bundle size runner 自帶的
webpack-entry-point-analyser
,將產出檔案寫入./bundle-size-review/analysis.json
,並將紀錄推送至 Storage Repository
當 Merge Request 被發出時
- 進行 Repository 的依賴安裝
- 打包 Repository 的原始碼
- 安裝 bundle size runner
- 跑 bundle size runner 自帶的
webpack-entry-point-analyser
,將產出檔案寫入./bundle-size-review/analysis.json
,並將紀錄推送至 Storage Repository - 跑 bundle size runner 自帶的
webpack-compare-reports
,抓取分支自身及母分支(這個案例為 main 的某個版本)的 bundle size 紀錄,進行比較並產出./bundle-size-review/comparison.md
- 跑 Danger CI,Danger CI 會讀取 comparison.md 並在 Merge Request 頁面中 comment
碰到問題時可能遺漏的:
- Vite 建立的主專案需要有 DANGER_GITLAB_API_TOKEN,這是由 Vite 建立的主專案產生的 Access token;STORAGE_BOT_GITLAB_API_TOKEN,這是由 Storage Repository 產生的 Access Token
- 主專案的主線分支名稱必須為 main,如果不是 main 的話此例會無法使用
- CI 中的 Variables,GITLAB_PROJECT_ID 為主專案 Project id;GITLAB_STORAGE_PROJECT_ID 為 Storage Repository 的 Project id;GITLAB_API_URL 如果 GitLab 的 origin 是自己的就需要填入,否則可不填,runner 會自帶預設值;STORAGE_BOT_NAME 和 STORAGE_BOT_EMAIL 就是 Storage Repository 寫入檔案時採用的 name 和 email,可不填,runner 會自帶預設值。
Runner 本身的原始碼在此,調整自 GitLab 的這個實驗專案,Runner 本身沒有很複雜,就是幾個用了一點 fs 和 GitLab API 的 JavaScript 程式碼而已,歡迎 Clone 過去改。
以上就是關於如何運用 Danger JS 在 Merge Request 記錄 Bundle 容量的方法,如果在執行步驟時碰到問題,歡迎留言給我,如果內文有任何錯誤也十分歡迎指出,希望能幫到大家,謝謝。