feat(CI 工作流程): 僅在 beta 啟用 AI 審查 #7
@@ -1,3 +1,8 @@
|
||||
# --------------------------------------------------
|
||||
# 用途:定義 CD 工作流程,於 master push 後部署並顯示相關 tag。
|
||||
# 更新日期:2026/07/11 17:44:33(Asia/Taipei)
|
||||
# --------------------------------------------------
|
||||
|
||||
name: CD
|
||||
on:
|
||||
push:
|
||||
@@ -16,6 +21,8 @@ jobs:
|
||||
fetch-depth: 0
|
||||
- name: Get Commit Tag
|
||||
id: commit
|
||||
run: echo "tag=$(git for-each-ref --sort=-refname --format='%(refname:strip=2)' refs/tags --contains ${{ env.COMMIT_SHA }} | head -n 1)" >> $GITEA_OUTPUT
|
||||
run: >-
|
||||
echo "tag=$(git for-each-ref --sort=-refname --format='%(refname:strip=2)' refs/tags --contains ${{ env.COMMIT_SHA }} | head -n 1)" >> $GITEA_OUTPUT
|
||||
- name: Show Tag
|
||||
run: echo "${{ steps.commit.outputs.tag }}"
|
||||
run: >-
|
||||
echo "[DEPLOY][INF][$(TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M:%S')]: ${{ steps.commit.outputs.tag }}"
|
||||
|
||||
+136
-1
@@ -6,10 +6,24 @@ const { URL } = require('url');
|
||||
const RELEASES_PER_PAGE = 10;
|
||||
let currentStage = null;
|
||||
|
||||
/**
|
||||
* 設定目前訊息輸出的階段名稱。
|
||||
*
|
||||
* @param {string} title 階段名稱,通常是流程區塊名稱。
|
||||
* @remarks
|
||||
* 供 `info()` 與錯誤輸出使用,讓訊息可以帶上統一前綴。
|
||||
*/
|
||||
function section(title) {
|
||||
currentStage = title;
|
||||
}
|
||||
|
||||
/**
|
||||
* 以統一格式輸出資訊訊息。
|
||||
*
|
||||
* @param {string} message 要輸出的訊息內容。
|
||||
* @remarks
|
||||
* 會附加目前階段與 Asia/Taipei 時區時間戳記,適合用於流程進度與狀態輸出。
|
||||
*/
|
||||
function info(message) {
|
||||
const timestamp = new Date()
|
||||
.toLocaleString('sv-SE', { timeZone: 'Asia/Taipei', hour12: false })
|
||||
@@ -19,10 +33,26 @@ function info(message) {
|
||||
process.stdout.write(`${stagePrefix}[INF][${timestamp}]: ${message}\n`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 以例外方式終止流程。
|
||||
*
|
||||
* @param {string} message 錯誤訊息。
|
||||
* @remarks
|
||||
* 當輸入參數缺失或流程無法繼續時使用,呼叫後會直接拋出 `Error`。
|
||||
*/
|
||||
function fail(message) {
|
||||
throw new Error(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 驗證必要環境變數是否有值。
|
||||
*
|
||||
* @param {string} name 環境變數名稱,用於錯誤訊息。
|
||||
* @param {string} value 環境變數值。
|
||||
* @returns {string} 驗證通過後的原始值。
|
||||
* @remarks
|
||||
* 適合用在 action 啟動時檢查 GITEA 相關必要設定。若值缺失,會直接拋出例外。
|
||||
*/
|
||||
function requireEnv(name, value) {
|
||||
if (!value || value === 'null') {
|
||||
fail(`${name} 未設定`);
|
||||
@@ -31,6 +61,14 @@ function requireEnv(name, value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 將 beta 旗標正規化成字串。
|
||||
*
|
||||
* @param {string} [value='false'] 原始 beta 旗標。
|
||||
* @returns {string} 正規化後的字串,空值會變成 `false`。
|
||||
* @remarks
|
||||
* 用於 action 入口參數,避免 `undefined`、空字串或字串 `null` 進入後續判斷。
|
||||
*/
|
||||
function normalizeBetaFlag(value = 'false') {
|
||||
if (!value || value === 'null') {
|
||||
return 'false';
|
||||
@@ -39,6 +77,13 @@ function normalizeBetaFlag(value = 'false') {
|
||||
return String(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* 將計算結果寫入 action output。
|
||||
*
|
||||
* @param {string} version 要輸出的版本號。
|
||||
* @remarks
|
||||
* 若執行環境提供 `GITHUB_OUTPUT`,會寫入檔案;否則退回標準輸出,方便本機除錯。
|
||||
*/
|
||||
function writeOutput(version) {
|
||||
const line = `version=${version}\n`;
|
||||
const outputPath = process.env.GITHUB_OUTPUT;
|
||||
@@ -51,6 +96,14 @@ function writeOutput(version) {
|
||||
process.stdout.write(line);
|
||||
}
|
||||
|
||||
/**
|
||||
* 將版本字串拆成數字陣列。
|
||||
*
|
||||
* @param {string|number} version 版本字串。
|
||||
* @returns {number[]} 由 major、minor、patch 等片段組成的數字陣列。
|
||||
* @remarks
|
||||
* 非數字片段會被轉成 `0`,適合用於後續比較與遞增計算。
|
||||
*/
|
||||
function parseVersionParts(version) {
|
||||
return String(version)
|
||||
.split('.')
|
||||
@@ -60,6 +113,15 @@ function parseVersionParts(version) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 比較兩組版本片段的大小。
|
||||
*
|
||||
* @param {number[]} left 左側版本片段。
|
||||
* @param {number[]} right 右側版本片段。
|
||||
* @returns {number} 左小於右回傳負值,左大於右回傳正值,相等回傳 0。
|
||||
* @remarks
|
||||
* 可直接搭配 `Array.prototype.sort()` 使用,用來找出最新穩定版本。
|
||||
*/
|
||||
function compareVersionParts(left, right) {
|
||||
const maxLength = Math.max(left.length, right.length);
|
||||
|
||||
@@ -75,6 +137,14 @@ function compareVersionParts(left, right) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 從 release tag 解析穩定版版本資訊。
|
||||
*
|
||||
* @param {string} tagName release tag 名稱。
|
||||
* @returns {number[]|null} 解析成功時回傳版本片段,否則回傳 `null`。
|
||||
* @remarks
|
||||
* 只接受純數字與點號組成的穩定版標籤,例如 `v1.2.3` 或 `1.2.3`。
|
||||
*/
|
||||
function stableVersionFromTag(tagName) {
|
||||
if (typeof tagName !== 'string' || tagName.includes('-beta.')) {
|
||||
return null;
|
||||
@@ -88,6 +158,14 @@ function stableVersionFromTag(tagName) {
|
||||
return parseVersionParts(versionText);
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得最新的穩定版版本號。
|
||||
*
|
||||
* @param {Array<object>} releaseJson release API 回傳資料。
|
||||
* @returns {string} 最新穩定版版本號;找不到時回傳 `0.0.0`。
|
||||
* @remarks
|
||||
* 主要用於計算下一版版本號的基礎值,會忽略 beta 標籤與不合法版本字串。
|
||||
*/
|
||||
function latestStableVersion(releaseJson) {
|
||||
if (!Array.isArray(releaseJson) || releaseJson.length === 0) {
|
||||
return '0.0.0';
|
||||
@@ -106,6 +184,14 @@ function latestStableVersion(releaseJson) {
|
||||
return `${latest[0] ?? 0}.${latest[1] ?? 0}.${latest[2] ?? 0}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算下一個正式版版本號。
|
||||
*
|
||||
* @param {string} latestVersion 目前最新的正式版版本號。
|
||||
* @returns {string} 下一個正式版版本號。
|
||||
* @remarks
|
||||
* 進位規則以 10 為界,適合與 `latestStableVersion()` 搭配使用。
|
||||
*/
|
||||
function nextReleaseVersion(latestVersion) {
|
||||
const [majorRaw, minorRaw, patchRaw] = parseVersionParts(latestVersion);
|
||||
let major = majorRaw ?? 0;
|
||||
@@ -126,6 +212,15 @@ function nextReleaseVersion(latestVersion) {
|
||||
return `${major}.${minor}.${patch}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算下一個 beta 編號。
|
||||
*
|
||||
* @param {Array<object>} releaseJson release API 回傳資料。
|
||||
* @param {string} version 目標正式版版本號。
|
||||
* @returns {number} 下一個 beta 編號,最小為 1。
|
||||
* @remarks
|
||||
* 用於 beta 發版流程,會忽略不符合命名格式或無法解析成整數的 tag。
|
||||
*/
|
||||
function nextBetaNumber(releaseJson, version) {
|
||||
if (!Array.isArray(releaseJson) || releaseJson.length === 0) {
|
||||
return 1;
|
||||
@@ -145,6 +240,15 @@ function nextBetaNumber(releaseJson, version) {
|
||||
return Math.max(...values) + 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 計算目前最新版本與下一個版本。
|
||||
*
|
||||
* @param {Array<object>} releaseJson release API 回傳資料。
|
||||
* @param {string} isBeta 是否計算 beta 版本,`true` 代表 beta。
|
||||
* @returns {string[]} 回傳 `[latestVersion, nextVersion]`。
|
||||
* @remarks
|
||||
* 這是 action 的核心邏輯,通常由 `main()` 取得 release 後再呼叫。
|
||||
*/
|
||||
function calculateVersion(releaseJson, isBeta) {
|
||||
if (!Array.isArray(releaseJson) || releaseJson.length === 0) {
|
||||
return isBeta === 'true'
|
||||
@@ -163,6 +267,15 @@ function calculateVersion(releaseJson, isBeta) {
|
||||
return [baseVersion, nextVersion];
|
||||
}
|
||||
|
||||
/**
|
||||
* 以 HTTP/HTTPS 取得 JSON 回應。
|
||||
*
|
||||
* @param {URL} url 請求目標。
|
||||
* @param {string} token 驗證 token;空值或字串 `null` 會被忽略。
|
||||
* @returns {Promise<unknown>} 成功時解析為 JSON 物件或空字串。
|
||||
* @remarks
|
||||
* 失敗時會拋出錯誤,適合拿來包裝 release API 的低階請求。
|
||||
*/
|
||||
function requestJson(url, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const client = url.protocol === 'http:' ? http : https;
|
||||
@@ -208,6 +321,16 @@ function requestJson(url, token) {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 逐頁抓取 repository 的 release 清單。
|
||||
*
|
||||
* @param {string|URL} baseUrl Gitea API 基底網址。
|
||||
* @param {string} repository repository 路徑。
|
||||
* @param {string} token API 存取 token。
|
||||
* @returns {Promise<Array<object>>} 合併後的 release 清單。
|
||||
* @remarks
|
||||
* 會依 `RELEASES_PER_PAGE` 分頁,當 API 回傳空資料或最後一頁不足數量時停止。
|
||||
*/
|
||||
async function fetchReleases(baseUrl, repository, token) {
|
||||
const combined = [];
|
||||
let page = 1;
|
||||
@@ -240,6 +363,13 @@ async function fetchReleases(baseUrl, repository, token) {
|
||||
return combined;
|
||||
}
|
||||
|
||||
/**
|
||||
* 執行 calculate-version action 的主流程。
|
||||
*
|
||||
* @returns {Promise<void>} 完成時不回傳值,失敗時會拋出例外。
|
||||
* @remarks
|
||||
* 需要在 Gitea action 環境中執行,並依賴 `GITEA_SERVER_URL`、`GITEA_REPOSITORY` 與 token 相關環境變數。
|
||||
*/
|
||||
async function main() {
|
||||
section('參數檢查');
|
||||
|
||||
@@ -268,7 +398,12 @@ async function main() {
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch((error) => {
|
||||
process.stderr.write(`[error] ${error.message}\n`);
|
||||
const timestamp = new Date()
|
||||
|
|
||||
.toLocaleString('sv-SE', { timeZone: 'Asia/Taipei', hour12: false })
|
||||
.replace(/-/g, '/');
|
||||
const stagePrefix = currentStage ? `[${currentStage}]` : '';
|
||||
|
||||
process.stderr.write(`${stagePrefix}[ERR][${timestamp}]: ${error.message}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user
嚴重等級:🟡 警告
審查員:Leo
問題:這裡在
catch裡重新手寫一次時間戳與前綴格式,和前面的info()形成兩套彼此獨立的輸出格式。未來只要調整 log 格式、時區或錯誤前綴,就必須記得同步兩處,半年後很容易出現訊息格式不一致的問題。建議:把時間戳與 prefix 組裝抽成共用的 formatter,例如
formatLogLine()或formatTimestamp(),讓info()與錯誤處理共用同一套輸出規則。嚴重等級:🟡 警告
審查員:Maya
問題:這次把錯誤輸出從單純的
[error] ...改成包含currentStage與台灣時區時間戳的格式,但沒有任何測試覆蓋失敗路徑。只要之後格式字串、stage 前綴或process.exit(1)的行為回歸,CI 很容易看不出來。建議:補一組針對
main().catch()的單元測試:模擬main()拋錯或回傳 rejected promise,先設定currentStage,再斷言process.stderr.write寫出的內容包含[階段][ERR][YYYY/MM/DD ...]: 錯誤訊息,同時驗證process.exit(1)有被呼叫。建議再加一個currentStage === null時的案例,確認前綴會正確省略。