運用 Danger JS 在 Merge Request 記錄 Bundle 容量

Eason Lin
23 min readSep 16, 2024

--

Photo by Gary Bendig on Unsplash

大家好,這篇文要記錄一下自己實作 GitLab 團隊在 Merge Request 階段中會發布 Bundle Size 比較表的功能。這既不是一個 GitLab, Github 或任何原始碼管理平台內建的功能、也沒有任何整合此功能的套件外。它更像是一個 GitLab 團隊仍在實驗階段的自建流程。也因此,我在實作中是直接將 GitLab 專案 Clone 下來慢慢翻閱,最後整理出一套流程的。也就是說它不是標準解答,但也許可以作為參考。

GitLab 團隊在 Merge Request 階段會有一個類似 Bot 的角色發布分支與主線的打包比較表

要實作此功能,對以下工具/技術會需要有基本的了解:

  • Danger JS
  • Gitlab CI or Github Actions
  • Merge Request or Pull Request
  • Vite, Webpack or Rspack
  • Ajax (我們會串接 GitLab File API)

我會盡可能清楚地描述實作流程與思路,因此如果不了解上述工具或技術也沒關係,讀到覺得不是很清楚的段落可以直接留言或是把原始碼丟給 AI 請它說明。

我把實作流程整理成以下步驟:

  1. 產生測試用 Repo 並同步在 GitLab
  2. 在專案中導入 vite-bundle-visualizer,建立容量打包記錄檔的邏輯
  3. 建立儲存容量打包記錄檔用的 Repo
  4. 建立 GitLab CI 用的 yaml 檔,撰寫 pushmerge_requests 的 CI 腳本
  5. 導入 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 scriptsbuild 打包邏輯改為使用 vite-bundle-visualizer

"build": vite-bundle-visualizer -t list -o vite-report/stats.yaml

嘗試在專案中輸入:

npm run build

應該會產出一個裡面包含 stat.ymlvite-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 檔,撰寫 pushmerge_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 來定義我們的執行階段,這邊定義了 buildmerge 兩種。
  • 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.shscripts/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-analyserwebpack-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,權限部分我選擇了 apiwrite_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 被推送時

  1. 進行 Repository 的依賴安裝
  2. 打包 Repository 的原始碼
  3. 安裝 bundle size runner
  4. 跑 bundle size runner 自帶的 webpack-entry-point-analyser ,將產出檔案寫入 ./bundle-size-review/analysis.json ,並將紀錄推送至 Storage Repository

當 Merge Request 被發出時

  1. 進行 Repository 的依賴安裝
  2. 打包 Repository 的原始碼
  3. 安裝 bundle size runner
  4. 跑 bundle size runner 自帶的 webpack-entry-point-analyser ,將產出檔案寫入 ./bundle-size-review/analysis.json ,並將紀錄推送至 Storage Repository
  5. 跑 bundle size runner 自帶的 webpack-compare-reports ,抓取分支自身及母分支(這個案例為 main 的某個版本)的 bundle size 紀錄,進行比較並產出 ./bundle-size-review/comparison.md
  6. 跑 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 容量的方法,如果在執行步驟時碰到問題,歡迎留言給我,如果內文有任何錯誤也十分歡迎指出,希望能幫到大家,謝謝。

--

--

Eason Lin
Eason Lin

Written by Eason Lin

Frontend Web Developer | Books

No responses yet