docs(calculate-version): 重建 README 並補齊指令檔註解與 logger JSDoc #12

Merged
admin merged 6 commits from develop into master 2026-06-30 10:24:19 +00:00
11 changed files with 302 additions and 233 deletions
Showing only changes of commit c267b60695 - Show all commits
+1 -1
View File
@@ -1,7 +1,7 @@
# ============================================================================= # =============================================================================
# 用途: calculate-version 專案的 CD workflow。 # 用途: calculate-version 專案的 CD workflow。
# 在程式碼推送至 master 分支時,自動釋出並標註 (tag) 成品版本。 # 在程式碼推送至 master 分支時,自動釋出並標註 (tag) 成品版本。
# 更新日期: 2026/06/30 12:13:04 # 更新日期: 2026/06/30 17:18:23
# ============================================================================= # =============================================================================
# workflow 名稱,顯示於 Gitea Actions 介面。 # workflow 名稱,顯示於 Gitea Actions 介面。
+27 -5
View File
@@ -1,36 +1,58 @@
# ============================================================================= # =============================================================================
# 用途: calculate-version 專案的 CI workflow。 # 用途: calculate-version 專案的 CI workflow。
# 在 Pull Request 開啟或更新時,觸發 OpenCode AI 程式碼審查。 # 在 Pull Request 開啟或更新 (且目標分支非 master) 時,先以 composite
# 更新日期: 2026/06/30 12:12:59 # action 釋出並標註 beta 成品版本,再用剛標註出的版本呼叫
# calculate-version action 計算版本號。
# 更新日期: 2026/06/30 17:18:23
# ============================================================================= # =============================================================================
# workflow 名稱,顯示於 Gitea Actions 介面。 # workflow 名稱,顯示於 Gitea Actions 介面。
name: CI name: CI
# 觸發條件設定。 # 觸發條件設定,定義此 workflow 在哪些事件下被啟動
on: on:
# 於 Pull Request 事件觸發。 # 於 Pull Request 事件觸發。
pull_request: pull_request:
# 忽略目標分支為 master 的 PR (master 走 CD 流程,不在此審查)。 # 忽略目標分支為 master 的 PR (master 走 CD 流程,不在此 CI 審查)。
branches-ignore: branches-ignore:
# 排除清單的單一項目: master 分支。
- master - master
# 僅在 PR 開啟 (opened) 或有新 commit 推送 (synchronize) 時觸發。 # 僅在 PR 開啟 (opened) 或有新 commit 推送 (synchronize) 時觸發,避免其他 PR 事件 (如關閉) 重複觸發
types: [opened, synchronize] types: [opened, synchronize]
# 定義此 workflow 要執行的所有 job。
jobs: jobs:
# 第一個 job: 釋出並標註成品版本,並將版本號往外拋給後續 job 使用。
release-tag-version: release-tag-version:
# job 顯示名稱。
name: Release Tag Version name: Release Tag Version
# 指定執行環境 (runner) 標籤為 ubuntu。
runs-on: ubuntu runs-on: ubuntu
# 宣告此 job 的輸出,供後續 needs 此 job 的其他 job 取用。
outputs: outputs:
# 將下方 id 為 release-tag-version 的 step 所輸出的 version,設為此 job 的對外 version 輸出 (關鍵點: 透過 outputs 把標註出的版本往外拋)。
version: ${{ steps.release-tag-version.outputs.version }} version: ${{ steps.release-tag-version.outputs.version }}
# 此 job 依序執行的步驟。
steps: steps:
# 步驟: 呼叫 composite action 進行釋出並標註成品版本。
- name: 釋出並標註成品版本 - name: 釋出並標註成品版本
# 設定此 step 的 id,讓上方 outputs 可用 steps.release-tag-version.outputs.version 取得其輸出。
id: release-tag-version id: release-tag-version
# 引用 release-tag-version composite action,版本以變數 vars.ACTION_RELEASE_TAG_VERSION 釘選 (集中於 repo/org 變數管理,便於統一升版)。
uses: https://gitea.jsc.idv.tw/composite-actions/release-tag-version@${{ vars.ACTION_RELEASE_TAG_VERSION }} uses: https://gitea.jsc.idv.tw/composite-actions/release-tag-version@${{ vars.ACTION_RELEASE_TAG_VERSION }}
# 傳遞給該 action 的輸入參數。
with: with:
# is_beta 設為 true 表示走 beta 標版流程 (標註的是 beta 成品版本,而非正式版)。
is_beta: true is_beta: true
# 第二個 job: 以前一個 job 標註出的版本來計算版本號。
calculate-version: calculate-version:
# job 顯示名稱。
name: Calculate Version name: Calculate Version
# 指定執行環境 (runner) 標籤為 ubuntu。
runs-on: ubuntu runs-on: ubuntu
# 宣告相依於 release-tag-version job: 需等其成功後才執行,並可透過 needs 取得其 outputs (關鍵點: 以此取得剛標註出的版本)。
needs: release-tag-version needs: release-tag-version
# 此 job 依序執行的步驟。
steps: steps:
# 步驟: 呼叫 calculate-version action 計算版本號。
- name: 計算版本號 - name: 計算版本號
# 引用本專案的 calculate-version action,並以 @v<version> 釘選自身 action 版本; 其中 version 來自上一個 job 透過 needs 取得的 outputs.version (關鍵點: 用剛標註的版本釘選自身)。
uses: https://gitea.jsc.idv.tw/docker-actions/calculate-version@v${{ needs.release-tag-version.outputs.version }} uses: https://gitea.jsc.idv.tw/docker-actions/calculate-version@v${{ needs.release-tag-version.outputs.version }}
+1 -1
View File
@@ -3,7 +3,7 @@
# 用途: 建置 calculate-version Action 的容器映像。 # 用途: 建置 calculate-version Action 的容器映像。
# 採多階段建置:build 階段以最新 node 準備主程式 (/app) 與進入點腳本, # 採多階段建置:build 階段以最新 node 準備主程式 (/app) 與進入點腳本,
# runtime 階段改用 node slim 基底縮小最終映像,再由 entrypoint.sh 啟動 Node.js 主程式。 # runtime 階段改用 node slim 基底縮小最終映像,再由 entrypoint.sh 啟動 Node.js 主程式。
# 更新日期: 2026/06/30 12:12:52 # 更新日期: 2026/06/30 17:18:23
# ============================================================================= # =============================================================================
# 1. 參數處理:node 版本以 ARG 注入。預設使用最新版 (build: node:latest / runtime: node:slim) # 1. 參數處理:node 版本以 ARG 注入。預設使用最新版 (build: node:latest / runtime: node:slim)
+162 -174
View File
@@ -1,10 +1,8 @@
# calculate-version # calculate-version
> 更新時間:2026/06/30 12:32:40 計算版本號的 Gitea Action:依儲存庫現有的 release,推算下一個穩定版或 beta 版本號,並寫出為 Action output 供後續步驟取用。以 Docker 容器執行,容器內由 Node.js 主程式實作。
計算版本號的 Gitea Action。依現有 release 推算下一個穩定版或 beta 版本號,並將結果寫入 Action output `version`。核心邏輯以 Node.js 實作,置於 `app/`,由 `entrypoint.sh` 作為容器進入點啟動;容器映像採多階段建置(build 階段 `node:latest`、runtime 階段 `node:slim`)。 > 更新時間:2026/06/30 18:18:13Asia/Taipei
版本進位規則:`patch + 1`;當 `patch` 達 10 進位至 `minor``minor` 達 10 進位至 `major`。beta 版本號形如 `<next>-beta.<n>`,其中 `<n>` 為該版本既有 beta 標籤的最大序號加 1。
## 專案列表 ## 專案列表
@@ -12,7 +10,7 @@
| 專案名稱 | 專案描述 | | 專案名稱 | 專案描述 |
| --- | --- | | --- | --- |
| [calculate-version](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app) | 計算版本號的 Gitea Action:載入並驗證環境設定、分頁取 Gitea release、算下一個穩定版或 beta 版本號,並寫出 Action output。 | | [calculate-version](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app) | 計算版本號的 Gitea Action:載入並驗證環境變數設定、分頁取 Gitea release、解析最新穩定版並推算下一個穩定版或 beta 版本號,最後將結果寫入 Action output。 |
### 參考專案 ### 參考專案
@@ -24,196 +22,199 @@
| 專案名稱 | NuGet 套件列表 | | 專案名稱 | NuGet 套件列表 |
| --- | --- | | --- | --- |
| [calculate-version](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app) | 無(zero-dependency;僅使用 Node.js 內建模組與全域 fetch | | [calculate-version](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app) | 無(本專案為純 Node.jspackage.json 未宣告任何 npm 相依 |
## 功能列表 ## 功能列表
### indexapp/index.js ### calculate-version
| 功能名稱 | 功能描述 | | 功能名稱 | 功能描述 |
| --- | --- | | --- | --- |
| [index.main](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/index.js#L27) | [Action 進入點:協調設定載入、release 取得、版本計算與輸出(支援相依注入)。](#indexmain) | | [config.isUnset](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/config.js#L11) | [判斷環境變數值是否視為「未設定」(undefinednull/空字串/字面 "null")。](#configisunset) |
### loggerapp/logger.js
| 功能名稱 | 功能描述 |
| --- | --- |
| [logger.section](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/logger.js#L45) | [輸出帶標題的區塊段落至標準輸出,標題前後以分隔線包夾。](#loggersection) |
| [logger.info](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/logger.js#L60) | [輸出 INF 層級 log 訊息,格式為 `[INF][時間]: 訊息`。](#loggerinfo) |
| [logger.error](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/logger.js#L77) | [輸出 ERR 層級訊息至 stderr(格式為 `[ERR][時間]: 訊息`,僅輸出不終止行程)。](#loggererror) |
### configapp/config.js
| 功能名稱 | 功能描述 |
| --- | --- |
| [config.isUnset](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/config.js#L11) | [判斷環境變數值是否視為「未設定」。](#configisunset) |
| [config.requireEnv](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/config.js#L23) | [驗證必填環境變數,未設定時拋出錯誤。](#configrequireenv) | | [config.requireEnv](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/config.js#L23) | [驗證必填環境變數,未設定時拋出錯誤。](#configrequireenv) |
| [config.normalizeBetaFlag](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/config.js#L38) | [將 beta 旗標正規化為布林值。](#confignormalizebetaflag) | | [config.normalizeBetaFlag](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/config.js#L38) | [將 beta 旗標正規化為布林值(僅字面 "true" 為真)](#confignormalizebetaflag) |
| [config.loadConfig](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/config.js#L80) | [從環境變數載入並驗證執行所需的設定。](#configloadconfig) | | [config.loadConfig](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/config.js#L101) | [從環境變數載入並驗證執行所需的設定。](#configloadconfig) |
| [index.main](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/index.js#L30) | [Action 進入點:依序執行參數檢查、取得舊版本、計算版本號並寫出 output。](#indexmain) |
### versionapp/version.js | [logger.info](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/logger.js#L58) | [輸出 `[階段][INF][時間]` 格式的資訊訊息至標準輸出(階段選填)。](#loggerinfo) |
| [logger.error](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/logger.js#L76) | [輸出 `[階段][ERR][時間]` 格式的錯誤訊息至標準錯誤輸出(階段選填)。](#loggererror) |
| 功能名稱 | 功能描述 | | [logger.forStage](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/logger.js#L89) | [建立綁定特定階段的子記錄器,自動為每則訊息帶入階段前綴。](#loggerforstage) |
| --- | --- |
| [version.compareVersionArrays](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L14) | [逐區段比較兩個版本號數值陣列,較短者視為較小。](#versioncompareversionarrays) |
| [version.parseStableVersions](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L38) | [從 release 清單解析出所有穩定版的版本號數值陣列。](#versionparsestableversions) |
| [version.latestStableVersion](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L57) | [取得 release 清單中最新的穩定版版本號字串。](#versionlateststableversion) |
| [version.nextReleaseVersion](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L78) | [依最新穩定版計算下一個發行版本號。](#versionnextreleaseversion) |
| [version.nextBetaNumber](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L101) | [計算指定版本號的下一個 beta 流水號。](#versionnextbetanumber) |
| [version.calculateVersion](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L126) | [計算最新穩定版與下一個版本號(支援 beta)。](#versioncalculateversion) |
### releasesapp/releases.js
| 功能名稱 | 功能描述 |
| --- | --- |
| [releases.fetchReleases](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/releases.js#L24) | [以分頁方式取得指定 Gitea repo 的所有 release。](#releasesfetchreleases) |
### outputapp/output.js
| 功能名稱 | 功能描述 |
| --- | --- |
| [output.writeOutput](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/output.js#L16) | [將一行 `name=value` 附加寫入 Action 的輸出檔。](#outputwriteoutput) | | [output.writeOutput](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/output.js#L16) | [將一行 `name=value` 附加寫入 Action 的輸出檔。](#outputwriteoutput) |
| [releases.fetchReleases](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/releases.js#L24) | [以分頁方式取得指定 Gitea repo 的所有 release。](#releasesfetchreleases) |
| [version.compareVersionArrays](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L15) | [逐區段比較兩個版本號數值陣列,較短者視為較小。](#versioncompareversionarrays) |
| [version.parseStableVersions](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L39) | [從 release 清單解析出所有穩定版的版本號數值陣列。](#versionparsestableversions) |
| [version.latestStableVersion](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L58) | [取得最新(最大)的穩定版版本號字串。](#versionlateststableversion) |
| [version.nextReleaseVersion](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L79) | [依最新穩定版計算下一個發行版本號(逢 10 進位)。](#versionnextreleaseversion) |
| [version.nextBetaNumber](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L102) | [計算指定版本號的下一個 beta 流水號。](#versionnextbetanumber) |
| [version.nextVersion](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L131) | [依已知的最新穩定版計算本次要使用的版本號(含 beta)。](#versionnextversion) |
| [version.calculateVersion](https://gitea.jsc.idv.tw/docker-actions/calculate-version/src/branch/develop/app/version.js#L147) | [計算最新穩定版與下一個版本號。](#versioncalculateversion) |
## 使用範例 ## 使用範例
<a id="indexmain"></a>
### index.main
Action 進入點:依序協調設定載入、release 取得、版本計算與輸出寫出,回傳計算出的版本號。失敗時向外拋出例外(由進入點頂層攔截並以狀態碼 1 結束)。相依模組可透過 `deps` 注入,便於測試。模組僅在被直接執行(`node app/index.js`)時自動啟動。
```js
const { main } = require('./index');
// 測試或自訂情境:注入假的相依
const version = await main({
loadConfig: () => ({ serverUrl: 'https://gitea.example.com', repository: 'owner/repo', token: null, isBeta: false }),
fetchReleases: async () => ([{ tag_name: 'v1.2.3' }]),
writeOutput: () => {},
log: { section() {}, info() {}, error() {} },
});
// version === '1.2.4'
```
<a id="loggersection"></a>
### logger.section
輸出帶標題的區塊段落至標準輸出:先一個換行,接著 50 個 `=` 的主分隔線、標題文字,最後 50 個 `-` 的次分隔線,用於在 log 中建立可視段落區隔。此為結構性段落標題,不套用 `[等級][時間]` 前綴。
```js
const logger = require('./logger');
logger.section('參數檢查');
// 輸出:
//
// ==================================================
// 參數檢查
// --------------------------------------------------
```
<a id="loggerinfo"></a>
### logger.info
輸出一般資訊(INF)層級訊息至標準輸出,格式統一為 `[INF][{時間}]: {訊息}`,時間使用台灣時區(Asia/Taipei)、格式 `yyyy/MM/dd HH:mm:ss`,並於結尾換行;不會結束行程。
```js
const logger = require('./logger');
logger.info('IS_BETA=false');
// 輸出:[INF][2026/06/30 12:00:00]: IS_BETA=false
```
<a id="loggererror"></a>
### logger.error
輸出錯誤(ERR)層級訊息至標準錯誤輸出(stderr),格式統一為 `[ERR][{時間}]: {訊息}`(時間為台灣時區)。僅負責輸出,**不終止行程**;是否結束由呼叫端(進入點)決定,以利測試與錯誤復原。
```js
const logger = require('./logger');
logger.error('GITEA_SERVER_URL 未設定');
// 對 stderr 輸出:[ERR][2026/06/30 12:00:00]: GITEA_SERVER_URL 未設定
```
<a id="configisunset"></a> <a id="configisunset"></a>
### config.isUnset ### config.isUnset
判斷環境變數值是否視為「未設定」。`undefined``null`、空字串、字面字串 `"null"` 皆視為未設定。 判斷單一環境變數值是否視為「未設定」。下列任一情況回傳 `true``undefined``null`、空字串、字面字串 `"null"`;其餘回傳 `false`。常用於 `loadConfig` 內判斷必填與選填設定。
```js ```js
const { isUnset } = require('./config'); const { isUnset } = require('./app/config');
isUnset(undefined); // true isUnset(undefined); // true
isUnset('null'); // true isUnset('null'); // true
isUnset('false'); // false isUnset(''); // true
isUnset('abc'); // false
``` ```
<a id="configrequireenv"></a> <a id="configrequireenv"></a>
### config.requireEnv ### config.requireEnv
驗證必填環境變數;未設定時拋出 `Error`(訊息為 `${name} 未設定`),已設定時原樣回傳值。 驗證必填環境變數;值被視為未設定時拋出 `Error`(訊息為 `${name} 未設定`),否則原樣回傳值。
```js ```js
const { requireEnv } = require('./config'); const { requireEnv } = require('./app/config');
const url = requireEnv('GITEA_SERVER_URL', process.env.GITEA_SERVER_URL); const url = requireEnv('GITEA_SERVER_URL', process.env.GITEA_SERVER_URL);
// 未設定時拋出:Error: GITEA_SERVER_URL 未設定 // 未設定時 → throw Error('GITEA_SERVER_URL 未設定')
``` ```
<a id="confignormalizebetaflag"></a> <a id="confignormalizebetaflag"></a>
### config.normalizeBetaFlag ### config.normalizeBetaFlag
將 beta 旗標正規化為布林值。未設定時預設為 `false`;僅當值嚴格等於字面字串 `"true"` 時回傳 `true` 將 beta 旗標環境變數正規化為布林值。未設定時預設為 `false`;僅當值嚴格等於字面字串 `"true"` 時回傳 `true`
```js ```js
const { normalizeBetaFlag } = require('./config'); const { normalizeBetaFlag } = require('./app/config');
normalizeBetaFlag('true'); // true normalizeBetaFlag('true'); // true
normalizeBetaFlag('TRUE'); // false(嚴格比較,不做大小寫轉換) normalizeBetaFlag('false'); // false
normalizeBetaFlag(undefined); // false normalizeBetaFlag(undefined); // false
``` ```
<a id="configloadconfig"></a> <a id="configloadconfig"></a>
### config.loadConfig ### config.loadConfig
從環境變數載入並驗證執行所需設定。`GITEA_SERVER_URL` `GITEA_REPOSITORY` 為必填(未設定即拋錯),且 `GITEA_SERVER_URL` 須為合法的 http/https URL`GITEA_TOKEN` 非必填(未設定為 `null`);`IS_BETA` 正規化為布林值。 從環境變數載入並驗證執行所需設定。`GITEA_SERVER_URL`(須為合法 http/https URL`GITEA_REPOSITORY`(須為 `owner/repo` 形式、排除路徑穿越)為必填`GITEA_TOKEN` 填(未設定為 `null`);`IS_BETA` 正規化為布林值。驗證失敗會拋出 `Error`
```js ```js
const { loadConfig } = require('./config'); const { loadConfig } = require('./app/config');
const config = loadConfig({ const config = loadConfig(process.env);
GITEA_SERVER_URL: 'https://gitea.example.com', // → { serverUrl, repository, token, isBeta }
GITEA_REPOSITORY: 'owner/repo', ```
IS_BETA: 'true',
<a id="indexmain"></a>
### index.main
Action 進入點,依序執行三個階段:1. 參數檢查(`loadConfig`)、2. 取得舊版本(`fetchReleases` + `latestStableVersion`)、3. 計算版本號(`nextVersion`)並以 `writeOutput` 寫出。相依模組可透過 `deps` 注入以利測試;失敗時向外拋出例外。
```js
const { main } = require('./app/index');
// 以預設相依(讀環境變數、實際打 API)執行
await main();
// 測試時注入替身
await main({
loadConfig: () => ({ serverUrl: 'https://gitea.example.com', repository: 'owner/repo', token: null, isBeta: false }),
fetchReleases: async () => [{ tag_name: 'v1.2.3' }],
writeOutput: () => {},
}); });
// => { serverUrl: 'https://gitea.example.com', repository: 'owner/repo', token: null, isBeta: true } // → 回傳 '1.2.4'
```
<a id="loggerinfo"></a>
### logger.info
輸出一般資訊(INF)層級的 log 訊息至標準輸出,格式統一為 `[{階段}][INF][{台灣時間}]: {訊息}`(時間為 Asia/Taipei,格式 `yyyy/MM/dd HH:mm:ss`),結尾換行。階段(`stage`)為選填,省略時略過 `[{階段}]` 區塊。
```js
const logger = require('./app/logger');
logger.info('NEW_VERSION=1.2.4');
// 輸出:[INF][2026/06/30 17:38:51]: NEW_VERSION=1.2.4
logger.info('NEW_VERSION=1.2.4', '計算版本號');
// 輸出:[計算版本號][INF][2026/06/30 17:38:51]: NEW_VERSION=1.2.4
```
<a id="loggererror"></a>
### logger.error
輸出錯誤(ERR)層級的 log 訊息至標準錯誤輸出(stderr),格式統一為 `[{階段}][ERR][{台灣時間}]: {訊息}`。階段為選填。僅負責輸出,不終止行程,是否結束由呼叫端決定。
```js
const logger = require('./app/logger');
logger.error('GITEA_SERVER_URL 未設定');
// stderr 輸出:[ERR][2026/06/30 17:38:51]: GITEA_SERVER_URL 未設定
```
<a id="loggerforstage"></a>
### logger.forStage
建立綁定特定階段(stage)的子記錄器,回傳 `{ info, error }`;其方法只需傳入 `message`,即會自動在每則訊息開頭帶入 `[{階段}]` 前綴。進入點以此為每個處理階段建立子記錄器,使該階段(含其呼叫的 `fetchReleases` 等模組)輸出的訊息都帶一致的階段前綴。
```js
const logger = require('./app/logger');
const stageLog = logger.forStage('取得舊版本');
stageLog.info('使用授權 token 取得 release');
// 輸出:[取得舊版本][INF][2026/06/30 17:38:51]: 使用授權 token 取得 release
```
<a id="outputwriteoutput"></a>
### output.writeOutput
將一行 `name=value` 以 append 方式寫入 GitHub/Gitea Action 的輸出檔(預設取自環境變數 `GITHUB_OUTPUT`)。輸出檔路徑為 falsy 時拋出 `Error`。value 僅支援單行字串。
```js
const { writeOutput } = require('./app/output');
writeOutput('version', '1.2.4');
// 對 process.env.GITHUB_OUTPUT 指向的檔案附加一行:version=1.2.4
```
<a id="releasesfetchreleases"></a>
### releases.fetchReleases
以分頁方式(每頁 10 筆)透過全域 `fetch` 取得指定 Gitea repo 的所有 release,回傳合併後的陣列。有 token 時以 `Authorization: token <token>` 授權,否則匿名請求;遇空頁或不足一頁即停止。請求失敗、非 2xx、無法解析或非陣列時拋出 `Error`
```js
const { fetchReleases } = require('./app/releases');
const logger = require('./app/logger');
const releases = await fetchReleases(
'https://gitea.example.com/api/v1/repos/owner/repo/releases',
{ token: process.env.GITEA_TOKEN, logger },
);
``` ```
<a id="versioncompareversionarrays"></a> <a id="versioncompareversionarrays"></a>
### version.compareVersionArrays ### version.compareVersionArrays
逐區段(element-wise)比較兩個版本號數值陣列,較短的陣列視為較小。回傳`> 0` 表示 a 大於 b、`< 0` 表示 a 小於 b、`0` 表示相等;相異區段回傳的是該段差值(非固定 ±1)。 逐區段(element-wise)比較兩個版本號數值陣列,較短的陣列視為較小。回傳大於 0 表示 `a` 大於 `b`、小於 0 表示 `a` 小於 `b`、0 表示相等;相異區段回傳的是該段差值(非固定 ±1)。傳入 `null``undefined` 會在讀取 `.length` 時拋 `TypeError`
```js ```js
const { compareVersionArrays } = require('./version'); const { compareVersionArrays } = require('./app/version');
compareVersionArrays([1, 10], [1, 2, 3]); // > 01.10 > 1.2.3 compareVersionArrays([1, 10, 0], [1, 9, 0]); // > 0
compareVersionArrays([1, 2], [1, 2, 0]); // < 0(較短者較小) compareVersionArrays([1, 2], [1, 2, 0]); // < 0
compareVersionArrays([1, 2, 3], [1, 2, 3]); // 0
``` ```
<a id="versionparsestableversions"></a> <a id="versionparsestableversions"></a>
### version.parseStableVersions ### version.parseStableVersions
從 release 清單解析出所有穩定版(排除 beta、排除格式不合法者)的版本號數值陣列;會去除 tag 開頭的 `v`入非陣列時回傳空陣列。 從 release 清單解析出所有穩定版(排除`-beta.`格式不合法者)的版本號數值陣列;會去除 tag 開頭的 `v`入非陣列時回傳空陣列。
```js ```js
const { parseStableVersions } = require('./version'); const { parseStableVersions } = require('./app/version');
parseStableVersions([ parseStableVersions([
{ tag_name: 'v1.2.3' }, { tag_name: 'v1.2.3' },
{ tag_name: '2.0.0' }, { tag_name: '2.0.0' },
{ tag_name: 'v1.0.0-beta.1' }, // 被排除 { tag_name: 'v1.3.0-beta.1' },
]); ]);
// => [[1, 2, 3], [2, 0, 0]] // [[1, 2, 3], [2, 0, 0]]
``` ```
<a id="versionlateststableversion"></a> <a id="versionlateststableversion"></a>
@@ -222,76 +223,63 @@ parseStableVersions([
取得 release 清單中最新(最大)的穩定版版本號字串,固定格式化為三段 `major.minor.patch`;查無穩定版時回傳 `"0.0.0"` 取得 release 清單中最新(最大)的穩定版版本號字串,固定格式化為三段 `major.minor.patch`;查無穩定版時回傳 `"0.0.0"`
```js ```js
const { latestStableVersion } = require('./version'); const { latestStableVersion } = require('./app/version');
latestStableVersion([{ tag_name: 'v1.2.3' }, { tag_name: 'v1.9.9' }]); // "1.9.9" latestStableVersion([{ tag_name: 'v1.9.0' }, { tag_name: 'v1.10.0' }]); // '1.10.0'
latestStableVersion([]); // "0.0.0" latestStableVersion([]); // '0.0.0'
``` ```
<a id="versionnextreleaseversion"></a> <a id="versionnextreleaseversion"></a>
### version.nextReleaseVersion ### version.nextReleaseVersion
依最新穩定版字串計算下一個發行版本號:`patch + 1``patch` 達 10 進位至 `minor``minor` 達 10 進位至 `major`(「逢 10 進位為本專案自訂約定,非標準 SemVer)。 依最新穩定版字串計算下一個發行版本號:patch 1patch 達 10 進位至 minorminor 達 10 進位至 major逢 10 進位為本專案自訂約定,非標準 SemVer)。
```js ```js
const { nextReleaseVersion } = require('./version'); const { nextReleaseVersion } = require('./app/version');
nextReleaseVersion('1.2.8'); // "1.2.9" nextReleaseVersion('1.2.3'); // '1.2.4'
nextReleaseVersion('1.2.9'); // "1.3.0" nextReleaseVersion('1.2.9'); // '1.3.0'
nextReleaseVersion('1.9.9'); // "2.0.0" nextReleaseVersion('1.9.9'); // '2.0.0'
``` ```
<a id="versionnextbetanumber"></a> <a id="versionnextbetanumber"></a>
### version.nextBetaNumber ### version.nextBetaNumber
計算指定版本號的下一個 beta 流水號:取現有相符 beta 標籤(`v<version>-beta.<n>`的最大序號加 1,查無時回傳 `1``version` 參數不應含前綴 `v` 計算指定版本號的下一個 beta 流水號:取現有相符 `v<version>-beta.<n>` 標籤的最大序號加 1,查無時回傳 1
```js ```js
const { nextBetaNumber } = require('./version'); const { nextBetaNumber } = require('./app/version');
nextBetaNumber([{ tag_name: 'v1.3.0-beta.1' }, { tag_name: 'v1.3.0-beta.3' }], '1.3.0'); // 4 nextBetaNumber([
nextBetaNumber([], '1.3.0'); // 1 { tag_name: 'v1.2.4-beta.1' },
{ tag_name: 'v1.2.4-beta.3' },
], '1.2.4'); // 4
nextBetaNumber([], '1.2.4'); // 1
```
<a id="versionnextversion"></a>
### version.nextVersion
依「已知的最新穩定版」計算本次要使用的版本號(不負責取得舊版本)。`isBeta``false` 時回傳下一個發行版;為 `true` 時回傳 `<next>-beta.<n>`
```js
const { nextVersion } = require('./app/version');
nextVersion('1.2.3', [], false); // '1.2.4'
nextVersion('1.2.3', [{ tag_name: 'v1.2.4-beta.2' }], true); // '1.2.4-beta.3'
``` ```
<a id="versioncalculateversion"></a> <a id="versioncalculateversion"></a>
### version.calculateVersion ### version.calculateVersion
計算最新穩定版與下一個版本號;`isBeta` `true` 時產生 beta 版本號。回傳物件 `{ latest, version }` 計算最新穩定版與下一個版本號;組合 `latestStableVersion` `nextVersion`。回傳物件 `latest`(現有最新穩定版,查無為 `"0.0.0"`)與 `version`(本次版本號)
```js ```js
const { calculateVersion } = require('./version'); const { calculateVersion } = require('./app/version');
calculateVersion([{ tag_name: 'v1.2.3' }], false); calculateVersion([{ tag_name: 'v1.2.3' }], false);
// => { latest: '1.2.3', version: '1.2.4' } // { latest: '1.2.3', version: '1.2.4' }
calculateVersion([{ tag_name: 'v1.2.3' }], true); calculateVersion([{ tag_name: 'v1.2.3' }, { tag_name: 'v1.2.4-beta.2' }], true);
// => { latest: '1.2.3', version: '1.2.4-beta.1' } // { latest: '1.2.3', version: '1.2.4-beta.3' }
```
<a id="releasesfetchreleases"></a>
### releases.fetchReleases
以分頁方式取得指定 Gitea repo 的所有 release,並回傳合併後的陣列。使用全域 `fetch`Node.js 18+)逐頁請求(每頁 limit 為 10);當某頁回傳空資料、`null` 或筆數少於上限時即停止。提供 `options.token` 時以授權方式請求。網路失敗、回應讀取失敗、非 2xx、無法解析或非陣列回應時拋出 `Error`(含頁碼)。
```js
const { fetchReleases } = require('./releases');
const logger = require('./logger');
const releases = await fetchReleases(
'https://gitea.example.com/api/v1/repos/owner/repo/releases',
{ token: process.env.GITEA_TOKEN, logger },
);
// => 所有 release 物件合併後的陣列
```
<a id="outputwriteoutput"></a>
### output.writeOutput
將一行 `name=value` 附加寫入 GitHub/Gitea Action 的輸出檔(預設取 `process.env.GITHUB_OUTPUT`)。以 append 方式寫入、不覆蓋既有內容;輸出檔路徑為 falsy 時拋出 `Error`
```js
const { writeOutput } = require('./output');
writeOutput('version', '1.2.4');
// 對 $GITHUB_OUTPUT 附加一行:version=1.2.4
``` ```
+24
View File
@@ -1,18 +1,42 @@
# =============================================================================
# 用途:此檔為 calculate-version Gitea Action 的定義,宣告其 inputsis_beta)、
# outputsversion)與以 docker 方式執行時注入給容器的環境變數。
# 更新日期:2026/06/30 17:18:23
# =============================================================================
# Action 名稱,會顯示於 Gitea workflow 執行畫面上以利辨識
name: 'Calculate Version' name: 'Calculate Version'
# Action 簡述,說明此 Action 的功能為計算版本號
description: '計算版本號' description: '計算版本號'
# Action 作者標示,供維護與聯絡用
author: 'Jeffery' author: 'Jeffery'
# inputs 區塊:宣告呼叫此 Action 時可傳入的參數
inputs: inputs:
# is_beta:控制此次要計算穩定版還是 beta 版本號的旗標
is_beta: is_beta:
# 參數說明:是否為 beta 版本
description: '是否為 beta 版本' description: '是否為 beta 版本'
# 預設值為 false,表示未指定時預設計算穩定版本號
default: false default: false
# outputs 區塊:宣告此 Action 執行後對外提供的輸出值
outputs: outputs:
# version:容器內主程式計算完成後寫出的版本號,供後續步驟取用
version: version:
# 輸出說明:計算出的版本號
description: '計算出的版本號' description: '計算出的版本號'
# runs 區塊:定義此 Action 的執行方式與相關設定
runs: runs:
# 以 docker 容器方式執行此 Action
using: 'docker' using: 'docker'
# 指定容器映像來源為同目錄的 Dockerfile,會即時建置成映像後執行
image: 'Dockerfile' image: 'Dockerfile'
# env 區塊:把 Gitea 平台 context 與 inputs 對應成容器內環境變數,供 Node.js 主程式(app/index.js)讀取
env: env:
# 注入 Gitea 伺服器網址,供主程式組出 release API 端點時使用
GITEA_SERVER_URL: ${{ gitea.server_url }} GITEA_SERVER_URL: ${{ gitea.server_url }}
# 注入目前 repositoryowner/repo),供主程式定位要查詢 release 的儲存庫
GITEA_REPOSITORY: ${{ gitea.repository }} GITEA_REPOSITORY: ${{ gitea.repository }}
# 注入 Gitea token,供主程式呼叫 release API 時通過身分驗證(屬敏感憑證,勿外洩)
GITEA_TOKEN: ${{ gitea.token }} GITEA_TOKEN: ${{ gitea.token }}
# 將 inputs.is_beta 對應成 IS_BETA 環境變數,供主程式判斷要推算穩定版或 beta 版本號
IS_BETA: ${{ inputs.is_beta }} IS_BETA: ${{ inputs.is_beta }}
+12 -12
View File
@@ -20,7 +20,7 @@ const { writeOutput } = require('./output');
* @param {Function} [deps.latestStableVersion] - 從 release 清單取得最新穩定版(舊版本)的函式。 * @param {Function} [deps.latestStableVersion] - 從 release 清單取得最新穩定版(舊版本)的函式。
* @param {Function} [deps.nextVersion] - 依舊版本計算本次版本號的函式。 * @param {Function} [deps.nextVersion] - 依舊版本計算本次版本號的函式。
* @param {Function} [deps.writeOutput] - 寫出 output 的函式。 * @param {Function} [deps.writeOutput] - 寫出 output 的函式。
* @param {{section:Function, info:Function, error:Function}} [deps.log] - log 記錄器。 * @param {{forStage:Function, info:Function, error:Function}} [deps.log] - log 記錄器;以 forStage(階段) 取得綁定階段的子記錄器後輸出
* @returns {Promise<string>} 計算出的版本號。 * @returns {Promise<string>} 計算出的版本號。
* @throws {Error} 當任一注入相依(loadConfigfetchReleaseslatestStableVersionnextVersionwriteOutput * @throws {Error} 當任一注入相依(loadConfigfetchReleaseslatestStableVersionnextVersionwriteOutput
* 拋出例外時,原樣向外傳播,由呼叫端決定如何結束。 * 拋出例外時,原樣向外傳播,由呼叫端決定如何結束。
@@ -38,29 +38,29 @@ async function main(deps = {}) {
} = deps; } = deps;
// 1. 參數檢查:載入並驗證環境變數設定 // 1. 參數檢查:載入並驗證環境變數設定
log.section('參數檢查'); const checkLog = log.forStage('參數檢查');
const config = loadConfigFn(); const config = loadConfigFn();
log.info(`GITEA_SERVER_URL=${config.serverUrl}`); checkLog.info(`GITEA_SERVER_URL=${config.serverUrl}`);
log.info(`GITEA_REPOSITORY=${config.repository}`); checkLog.info(`GITEA_REPOSITORY=${config.repository}`);
log.info(config.token ? 'GITEA_TOKEN=***' : 'GITEA_TOKEN=未提供'); checkLog.info(config.token ? 'GITEA_TOKEN=***' : 'GITEA_TOKEN=未提供');
log.info(`IS_BETA=${config.isBeta}`); checkLog.info(`IS_BETA=${config.isBeta}`);
// 2. 取得舊版本:抓取 release 清單並解析出最新穩定版 // 2. 取得舊版本:抓取 release 清單並解析出最新穩定版
log.section('取得舊版本'); const fetchLog = log.forStage('取得舊版本');
const releaseUrl = `${config.serverUrl}/api/v1/repos/${config.repository}/releases`; const releaseUrl = `${config.serverUrl}/api/v1/repos/${config.repository}/releases`;
log.info(`RELEASE_URL=${releaseUrl}`); fetchLog.info(`RELEASE_URL=${releaseUrl}`);
const releases = await fetchReleasesFn(releaseUrl, { token: config.token, logger: log }); const releases = await fetchReleasesFn(releaseUrl, { token: config.token, logger: fetchLog });
const latest = latestStableVersionFn(releases); const latest = latestStableVersionFn(releases);
log.info(`LATEST_VERSION=${latest}`); fetchLog.info(`LATEST_VERSION=${latest}`);
// 3. 計算版本號:依舊版本算出本次版本號並寫出 output // 3. 計算版本號:依舊版本算出本次版本號並寫出 output
log.section('計算版本號'); const calcLog = log.forStage('計算版本號');
const version = nextVersionFn(latest, releases, config.isBeta); const version = nextVersionFn(latest, releases, config.isBeta);
log.info(`NEW_VERSION=${version}`); calcLog.info(`NEW_VERSION=${version}`);
writeOutputFn('version', version); writeOutputFn('version', version);
return version; return version;
+42 -27
View File
@@ -1,13 +1,9 @@
'use strict'; 'use strict';
// 主分隔線與次分隔線寬度(沿用原 entrypoint.sh 的視覺樣式)
const LINE = '='.repeat(50);
const SUBLINE = '-'.repeat(50);
/** /**
* 產生台灣時區(Asia/Taipei)的時間戳,格式固定為 `yyyy/MM/dd HH:mm:ss`。 * 產生台灣時區(Asia/Taipei)的時間戳,格式固定為 `yyyy/MM/dd HH:mm:ss`。
* *
* 供 log 訊息統一格式 `[{等級}][{時間}]: {訊息}` 的時間欄位使用。 * 供 log 訊息統一格式 `[{階段}][{等級}][{時間}]: {訊息}` 的時間欄位使用。
* *
* @returns {string} 台灣時區當下時間字串,例如 `2026/06/30 12:13:04`。 * @returns {string} 台灣時區當下時間字串,例如 `2026/06/30 12:13:04`。
*/ */
@@ -31,51 +27,70 @@ function taipeiTimestamp() {
} }
/** /**
* 輸出帶標題的區塊段落至標準輸出,標題前後以分隔線包夾, * 將單一 log 訊息寫入指定輸出串流,格式統一為 `[{階段}][{等級}][{時間}]: {訊息}` 並於結尾換行。
* 用於在 log 中建立可視的段落區隔。
* *
* 輸出格式為:換行 + 主分隔線(50 個 `=`)+ 標題 + 次分隔線(50 個 `-`) * 階段(stage)為選填;省略或空字串時略過開頭的 `[{階段}]` 區塊,輸出退化為 `[{等級}][{時間}]: {訊息}`
* 此為結構性段落標題(非 INF/WRN/ERR 等級訊息),故不套用 `[{等級}][{時間}]` 前綴 * 一次呼叫只輸出一則訊息(一行一則)
* *
* @param {string} title - 區塊標題文字;會原樣輸出於兩條分隔線之間 * @param {NodeJS.WriteStream} stream - 目標輸出串流(process.stdout 或 process.stderr
* @param {string} level - 訊息等級,限 `INF`/`WRN`/`ERR`/`TRC`/`DBG`。
* @param {string} message - 要輸出的訊息內容。
* @param {string} [stage] - 階段名稱(選填)。
* @returns {void} * @returns {void}
* @remarks 通常在進入一個處理階段前呼叫(例如「參數檢查」「取得版本資料」),
* 用以在 CI/容器 log 中分隔各階段輸出,便於閱讀與定位。
*/ */
function section(title) { function write(stream, level, message, stage) {
process.stdout.write(`\n${LINE}\n${title}\n${SUBLINE}\n`); const stagePrefix = stage ? `[${stage}]` : '';
stream.write(`${stagePrefix}[${level}][${taipeiTimestamp()}]: ${message}\n`);
} }
/** /**
* 輸出一般資訊(INF)層級的 log 訊息至標準輸出。 * 輸出一般資訊(INF)層級的 log 訊息至標準輸出。
* *
* 訊息格式統一為 `[INF][{台灣時間}]: {訊息}`,時間使用台灣時區(Asia/Taipei), * 訊息格式統一為 `[{階段}][INF][{台灣時間}]: {訊息}`,時間使用台灣時區(Asia/Taipei),
* 格式為 `yyyy/MM/dd HH:mm:ss`,並於結尾換行。 * 格式為 `yyyy/MM/dd HH:mm:ss`,並於結尾換行;未提供 stage 時略過 `[{階段}]` 區塊
* *
* @param {string} message - 要輸出的資訊內容。 * @param {string} message - 要輸出的資訊內容。
* @param {string} [stage] - 階段名稱(選填),用於在訊息開頭標示所屬處理階段。
* @returns {void} * @returns {void}
* @remarks 用於回報正常流程進度(如設定值、URL、各頁取得筆數);此調整僅變更輸出前綴格式, * @remarks 用於回報正常流程進度(如設定值、URL、各頁取得筆數);可直接呼叫,或經 forStage()
* 不改變訊息所反映的實際行為與內容 * 取得綁定階段的子記錄器後以單一 message 參數呼叫
*/ */
function info(message) { function info(message, stage) {
process.stdout.write(`[INF][${taipeiTimestamp()}]: ${message}\n`); write(process.stdout, 'INF', message, stage);
} }
/** /**
* 輸出錯誤(ERR)層級的 log 訊息至標準錯誤輸出(stderr)。 * 輸出錯誤(ERR)層級的 log 訊息至標準錯誤輸出(stderr)。
* *
* 訊息格式統一為 `[ERR][{台灣時間}]: {訊息}`,時間使用台灣時區(Asia/Taipei), * 訊息格式統一為 `[{階段}][ERR][{台灣時間}]: {訊息}`,時間使用台灣時區(Asia/Taipei),
* 格式為 `yyyy/MM/dd HH:mm:ss`,並於結尾換行。 * 格式為 `yyyy/MM/dd HH:mm:ss`,並於結尾換行;未提供 stage 時略過 `[{階段}]` 區塊
* *
* 僅負責輸出,不終止行程;是否結束由呼叫端(進入點)決定,以利測試與錯誤復原。 * 僅負責輸出,不終止行程;是否結束由呼叫端(進入點)決定,以利測試與錯誤復原。
* *
* @param {string} message - 要輸出的錯誤描述內容。 * @param {string} message - 要輸出的錯誤描述內容。
* @param {string} [stage] - 階段名稱(選填),用於在訊息開頭標示所屬處理階段。
* @returns {void} * @returns {void}
* @remarks 由進入點在頂層捕捉到例外時呼叫,輸出至 stderr 後再由呼叫端決定 exit code * @remarks 由進入點在頂層捕捉到例外時呼叫(此情境無對應階段,省略 stage,輸出至 stderr 後
* 此調整僅變更輸出前綴格式,不改變訊息所反映的實際行為與內容 * 再由呼叫端決定 exit code
*/ */
function error(message) { function error(message, stage) {
process.stderr.write(`[ERR][${taipeiTimestamp()}]: ${message}\n`); write(process.stderr, 'ERR', message, stage);
} }
module.exports = { section, info, error }; /**
* 建立綁定特定階段(stage)的子記錄器;其 info/error 會自動帶入該階段名稱,
* 呼叫端僅需傳入 message 即可輸出 `[{階段}][{等級}][{時間}]: {訊息}`。
*
* @param {string} stage - 階段名稱,例如「參數檢查」「取得舊版本」「計算版本號」。
* @returns {{ info: (message: string) => void, error: (message: string) => void }} 綁定該階段的記錄器。
* @remarks 由進入點為每個處理階段建立一個子記錄器,使該階段(含其呼叫的 fetchReleases 等模組)
* 輸出的每一則訊息都帶有一致的階段前綴,取代過往以分隔線區塊標示階段的做法。
*/
function forStage(stage) {
return {
info: (message) => info(message, stage),
error: (message) => error(message, stage),
};
}
module.exports = { info, error, forStage };
+6 -2
View File
@@ -5,8 +5,12 @@ const assert = require('node:assert/strict');
const { main } = require('../index'); const { main } = require('../index');
// 安靜的 log 記錄器,避免測試輸出雜訊 // 安靜的 log 記錄器,避免測試輸出雜訊;forStage 回傳同樣安靜的子記錄器
const silentLog = { section() {}, info() {}, error() {} }; const silentLog = {
info() {},
error() {},
forStage() { return { info() {}, error() {} }; },
};
const okConfig = { const okConfig = {
serverUrl: 'https://gitea.example.com', serverUrl: 'https://gitea.example.com',
+23 -8
View File
@@ -29,24 +29,39 @@ function capture(run) {
const TIME = /\[\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}\]/; const TIME = /\[\d{4}\/\d{2}\/\d{2} \d{2}:\d{2}:\d{2}\]/;
test('section 於 stdout 輸出標題與 50 寬分隔線區塊', () => { test('info 未帶 stage 時以 [INF][時間]: 格式輸出至 stdout', () => {
const { out, err } = capture(() => logger.section('參數檢查'));
assert.equal(out, `\n${'='.repeat(50)}\n參數檢查\n${'-'.repeat(50)}\n`);
assert.equal(err, '');
});
test('info 以 [INF][時間]: 格式輸出至 stdout', () => {
const { out, err } = capture(() => logger.info('IS_BETA=false')); const { out, err } = capture(() => logger.info('IS_BETA=false'));
assert.match(out, new RegExp(`^\\[INF\\]${TIME.source}: IS_BETA=false\\n$`)); assert.match(out, new RegExp(`^\\[INF\\]${TIME.source}: IS_BETA=false\\n$`));
assert.equal(err, ''); assert.equal(err, '');
}); });
test('error 以 [ERR][時間]: 格式輸出至 stderr,且不寫入 stdout', () => { test('info 帶 stage 時以 [階段][INF][時間]: 格式輸出至 stdout', () => {
const { out, err } = capture(() => logger.info('IS_BETA=false', '參數檢查'));
assert.match(out, new RegExp(`^\\[參數檢查\\]\\[INF\\]${TIME.source}: IS_BETA=false\\n$`));
assert.equal(err, '');
});
test('error 未帶 stage 時以 [ERR][時間]: 格式輸出至 stderr,且不寫入 stdout', () => {
const { out, err } = capture(() => logger.error('GITEA_SERVER_URL 未設定')); const { out, err } = capture(() => logger.error('GITEA_SERVER_URL 未設定'));
assert.match(err, new RegExp(`^\\[ERR\\]${TIME.source}: GITEA_SERVER_URL 未設定\\n$`)); assert.match(err, new RegExp(`^\\[ERR\\]${TIME.source}: GITEA_SERVER_URL 未設定\\n$`));
assert.equal(out, ''); assert.equal(out, '');
}); });
test('error 帶 stage 時以 [階段][ERR][時間]: 格式輸出至 stderr', () => {
const { err } = capture(() => logger.error('讀取失敗', '取得舊版本'));
assert.match(err, new RegExp(`^\\[取得舊版本\\]\\[ERR\\]${TIME.source}: 讀取失敗\\n$`));
});
test('forStage 回傳的子記錄器自動帶入階段名稱', () => {
const staged = logger.forStage('計算版本號');
const { out, err } = capture(() => {
staged.info('NEW_VERSION=0.0.6');
staged.error('計算錯誤');
});
assert.match(out, new RegExp(`^\\[計算版本號\\]\\[INF\\]${TIME.source}: NEW_VERSION=0.0.6\\n$`));
assert.match(err, new RegExp(`^\\[計算版本號\\]\\[ERR\\]${TIME.source}: 計算錯誤\\n$`));
});
test('info/error 的時間戳為 yyyy/MM/dd HH:mm:ss 格式', () => { test('info/error 的時間戳為 yyyy/MM/dd HH:mm:ss 格式', () => {
const { out } = capture(() => logger.info('x')); const { out } = capture(() => logger.info('x'));
assert.match(out, TIME); assert.match(out, TIME);
+2 -1
View File
@@ -9,7 +9,8 @@ const SEGMENT_LIMIT = 10;
* @param {number[]} b - 第二個版本號區段陣列,例如 [1, 2, 0]。 * @param {number[]} b - 第二個版本號區段陣列,例如 [1, 2, 0]。
* @returns {number} 大於 0 表示 a 大於 b;小於 0 表示 a 小於 b;0 表示兩者相等。 * @returns {number} 大於 0 表示 a 大於 b;小於 0 表示 a 小於 b;0 表示兩者相等。
* 相異區段回傳的是該段差值(非固定 ±1)。 * 相異區段回傳的是該段差值(非固定 ±1)。
* @throws {TypeError} 當 a 或 b 非陣列(無 length 屬性)時拋出 * @throws {TypeError} 當 a 或 b 為 null/undefined(讀取 .length 即報錯)時拋出
* 傳入「有 length 但非數值陣列」(如字串)不會拋例外,但會得到非預期結果。
*/ */
function compareVersionArrays(a, b) { function compareVersionArrays(a, b) {
const length = Math.max(a.length, b.length); const length = Math.max(a.length, b.length);
+2 -2
View File
@@ -3,7 +3,7 @@
# 用途: calculate-version Action 的容器進入點 (entrypoint)。 # 用途: calculate-version Action 的容器進入點 (entrypoint)。
# 專案已由 bash 改寫為 Node.js,本腳本先輸出 Action 資訊橫幅, # 專案已由 bash 改寫為 Node.js,本腳本先輸出 Action 資訊橫幅,
# 再啟動 /app/index.js,並將容器收到的引數原封傳遞給 Node.js 程式。 # 再啟動 /app/index.js,並將容器收到的引數原封傳遞給 Node.js 程式。
# 更新日期: 2026/06/30 12:12:56 # 更新日期: 2026/06/30 17:18:23
# ============================================================================= # =============================================================================
# set -e: 任一指令失敗即中止; set -u: 使用未定義變數即報錯; # set -e: 任一指令失敗即中止; set -u: 使用未定義變數即報錯;
@@ -13,7 +13,7 @@ set -euo pipefail
# Action 基本資訊:與 action.yaml 的 name/description 一致;更新時間為本腳本最後異動的台灣時間。 # Action 基本資訊:與 action.yaml 的 name/description 一致;更新時間為本腳本最後異動的台灣時間。
ACTION_NAME='Calculate Version' ACTION_NAME='Calculate Version'
ACTION_DESC='計算版本號' ACTION_DESC='計算版本號'
ACTION_UPDATED='2026/06/30 12:12:56' ACTION_UPDATED='2026/06/30 17:18:23'
# 啟動時印出 Action 名稱 / 用途 / 更新時間橫幅,方便在 CI/容器 log 中辨識本次執行。 # 啟動時印出 Action 名稱 / 用途 / 更新時間橫幅,方便在 CI/容器 log 中辨識本次執行。
printf '\n%s\n' '==================================================' printf '\n%s\n' '=================================================='