refactor: 統一日誌格式為 [階段][等級][時間] 並補齊指令檔文件與 README
docker-actions/template: CI / BUILD (pull_request) Successful in 4s
docker-actions/template: CI / BUILD (pull_request) Successful in 4s
- logger.js format() 組字順序改為階段在前、等級居中、時間在後(Asia/Taipei) - entrypoint.sh 啟動訊息改用新格式並更新標頭時間 - action.yml 新增用途/更新時間標頭與逐行繁中註解,設定值不變 - dockerfile 檔名維持小寫並與 action.yml 的 image 引用一致 - 新增 src/logger.js、src/version.js 模組與完整 JSDoc;重建 README.md Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
a44092c7a2
commit
1773c646d5
@@ -0,0 +1,191 @@
|
|||||||
|
# Calculate Next Version
|
||||||
|
|
||||||
|
從 git tag 取得最新版號,依 `is_beta` 計算下一個正式版或 beta 版號(各號碼滿 9 進位)並輸出為 `value` 的 Gitea/GitHub Docker container action。
|
||||||
|
|
||||||
|
- 更新時間:2026/07/16 09:23:47
|
||||||
|
|
||||||
|
## action 使用方式
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0 # 需要完整 tag 歷史才能計算版號
|
||||||
|
- id: version
|
||||||
|
uses: docker-actions/calculate-next-version@master
|
||||||
|
with:
|
||||||
|
is_beta: 'false'
|
||||||
|
- run: echo "下一版號:${{ steps.version.outputs.value }}"
|
||||||
|
```
|
||||||
|
|
||||||
|
| 類型 | 名稱 | 必填 | 預設值 | 說明 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| input | `is_beta` | 否 | `false` | 是否為 beta 版(true 時產生 X.Y.Z-beta.N 版號) |
|
||||||
|
| output | `value` | — | — | 計算出的下一版號 |
|
||||||
|
|
||||||
|
版號規則:
|
||||||
|
|
||||||
|
- 版號來源為 repo 的 git tag(格式 `X.Y.Z` 或 `X.Y.Z-beta.N`,不帶前綴),取最大版號;無任何版號 tag 時從 `0.0.1`(beta 為 `0.0.1-beta.1`)起算。
|
||||||
|
- `is_beta=false`:最新為正式版 → patch +1(`1.2.3` → `1.2.4`);最新為 beta → 去掉 beta 尾碼轉正式(`1.2.4-beta.3` → `1.2.4`)。
|
||||||
|
- `is_beta=true`:最新為正式版 → patch +1 加 `-beta.1`;最新為 beta → beta 號 +1(`beta.9` → `beta.10`,無上限)。
|
||||||
|
- major/minor/patch 各上限 9,滿 9 進位(`1.2.9` → `1.3.0`、`1.9.9` → `2.0.0`);`9.9.9` 再進位則報錯並以非零 exit code 結束。
|
||||||
|
|
||||||
|
日誌格式:所有輸出訊息統一為 `[階段][等級][時間]: 訊息`(`階段` 選填;`等級` 為 `INF`/`WRN`/`ERR`/`TRC`/`DBG`;`時間` 為 Asia/Taipei 時區的 `yyyy/MM/dd HH:mm:ss`)。
|
||||||
|
|
||||||
|
## 專案列表
|
||||||
|
|
||||||
|
### 專案描述表
|
||||||
|
|
||||||
|
| 專案名稱 | 專案描述 |
|
||||||
|
| --- | --- |
|
||||||
|
| [calculate-next-version](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master) | Docker container action:提供版號解析/比較/進位計算(version 模組)與統一格式日誌輸出(logger 模組),從 git tag 計算下一個正式版或 beta 版號並寫出為 action 輸出 `value`。 |
|
||||||
|
|
||||||
|
### 參考專案表
|
||||||
|
|
||||||
|
| 專案名稱 | 參考專案列表 |
|
||||||
|
| --- | --- |
|
||||||
|
| [calculate-next-version](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master) | 無 |
|
||||||
|
|
||||||
|
### NuGet 套件表
|
||||||
|
|
||||||
|
| 專案名稱 | NuGet 套件列表 |
|
||||||
|
| --- | --- |
|
||||||
|
| [calculate-next-version](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master) | 無(Node.js 專案,僅使用 node 內建模組,無外部相依) |
|
||||||
|
|
||||||
|
## 功能列表
|
||||||
|
|
||||||
|
### calculate-next-version
|
||||||
|
|
||||||
|
| 功能名稱 | 功能描述 |
|
||||||
|
| --- | --- |
|
||||||
|
| [logger.inf](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master/src/logger.js#L86) | [輸出 INF(一般資訊)等級的日誌訊息到 stdout](#loggerinf) |
|
||||||
|
| [logger.wrn](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master/src/logger.js#L101) | [輸出 WRN(警告)等級的日誌訊息到 stdout](#loggerwrn) |
|
||||||
|
| [logger.err](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master/src/logger.js#L116) | [輸出 ERR(錯誤)等級的日誌訊息到 stderr](#loggererr) |
|
||||||
|
| [logger.trc](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master/src/logger.js#L131) | [輸出 TRC(細部追蹤)等級的日誌訊息到 stdout](#loggertrc) |
|
||||||
|
| [logger.dbg](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master/src/logger.js#L146) | [輸出 DBG(除錯)等級的日誌訊息到 stdout](#loggerdbg) |
|
||||||
|
| [version.parse](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master/src/version.js#L26) | [解析版號字串為版號物件,不符格式回傳 null](#versionparse) |
|
||||||
|
| [version.stringify](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master/src/version.js#L52) | [將版號物件轉換為版號字串](#versionstringify) |
|
||||||
|
| [version.compare](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master/src/version.js#L72) | [比較兩個版號物件的大小,正式版大於同號 beta](#versioncompare) |
|
||||||
|
| [version.next](https://gitea.jsc.idv.tw/docker-actions/calculate-next-version/src/branch/master/src/version.js#L136) | [依最新版號與 is_beta 旗標計算下一版號](#versionnext) |
|
||||||
|
|
||||||
|
## 使用範例
|
||||||
|
|
||||||
|
<a id="loggerinf"></a>
|
||||||
|
### logger.inf
|
||||||
|
|
||||||
|
輸出 INF(一般資訊)等級的日誌訊息到 stdout,格式為 `[階段][等級][時間]: 訊息`(時間為 Asia/Taipei 時區的 `yyyy/MM/dd HH:mm:ss`);`stage` 選填,省略時訊息不含 `[階段]` 區塊。用於記錄流程中的正常進度,例如讀到的輸入、計算結果、寫出的輸出。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
logger.inf('下一版號:1.2.4', '計算版號');
|
||||||
|
// [計算版號][INF][2026/07/16 09:23:47]: 下一版號:1.2.4
|
||||||
|
|
||||||
|
logger.inf('計算完成');
|
||||||
|
// [INF][2026/07/16 09:23:47]: 計算完成
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="loggerwrn"></a>
|
||||||
|
### logger.wrn
|
||||||
|
|
||||||
|
輸出 WRN(警告)等級的日誌訊息到 stdout,格式與參數同 `logger.inf`。用於非致命的異常狀況,例如找不到任何版號 tag、需注意的邊界條件。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
logger.wrn('找不到任何符合 X.Y.Z 或 X.Y.Z-beta.N 格式的 tag,將從起始版號計算', '讀取版號');
|
||||||
|
// [讀取版號][WRN][2026/07/16 09:23:47]: 找不到任何符合 X.Y.Z 或 X.Y.Z-beta.N 格式的 tag,將從起始版號計算
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="loggererr"></a>
|
||||||
|
### logger.err
|
||||||
|
|
||||||
|
輸出 ERR(錯誤)等級的日誌訊息到 stderr(`console.error`),格式與參數同 `logger.inf`。用於可預期錯誤與未捕捉例外的回報;呼叫端通常在輸出後以非零 exit code 結束。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
logger.err('找不到環境變數 GITHUB_OUTPUT,無法寫出輸出', '寫出結果');
|
||||||
|
// [寫出結果][ERR][2026/07/16 09:23:47]: 找不到環境變數 GITHUB_OUTPUT,無法寫出輸出
|
||||||
|
process.exit(1);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="loggertrc"></a>
|
||||||
|
### logger.trc
|
||||||
|
|
||||||
|
輸出 TRC(細部追蹤)等級的日誌訊息到 stdout,格式與參數同 `logger.inf`。用於記錄低層次操作的細節,例如實際執行的外部指令、逐筆解析的中間結果。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
logger.trc('執行指令:git tag --list', '讀取版號');
|
||||||
|
// [讀取版號][TRC][2026/07/16 09:23:47]: 執行指令:git tag --list
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="loggerdbg"></a>
|
||||||
|
### logger.dbg
|
||||||
|
|
||||||
|
輸出 DBG(除錯)等級的日誌訊息到 stdout,格式與參數同 `logger.inf`。用於協助開發者追蹤程式流程與詳細狀態,例如被略過的不符格式 tag。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const logger = require('./logger');
|
||||||
|
|
||||||
|
logger.dbg('略過不符合版號格式的 tag:not-a-version', '讀取版號');
|
||||||
|
// [讀取版號][DBG][2026/07/16 09:23:47]: 略過不符合版號格式的 tag:not-a-version
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="versionparse"></a>
|
||||||
|
### version.parse
|
||||||
|
|
||||||
|
解析版號字串為版號物件 `{ major, minor, patch, beta }`;支援正式版(`X.Y.Z`)與 beta 版(`X.Y.Z-beta.N`)兩種格式,`beta` 為 `null` 表示正式版。不符合格式(含 `null`、非字串轉出的值)時回傳 `null`、不拋出例外,適合逐一解析 git tag 並過濾非版號 tag。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { parse } = require('./version');
|
||||||
|
|
||||||
|
parse('1.2.3'); // => { major: 1, minor: 2, patch: 3, beta: null }
|
||||||
|
parse('1.2.3-beta.5'); // => { major: 1, minor: 2, patch: 3, beta: 5 }
|
||||||
|
parse('v1.2'); // => null(不符合格式)
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="versionstringify"></a>
|
||||||
|
### version.stringify
|
||||||
|
|
||||||
|
將版號物件轉換為版號字串;`beta` 為 `null` 時輸出 `X.Y.Z`,否則輸出 `X.Y.Z-beta.N`。為 `version.parse` 的反向操作,用於日誌輸出與最終寫出 action 的 `value`。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { stringify } = require('./version');
|
||||||
|
|
||||||
|
stringify({ major: 1, minor: 2, patch: 3, beta: null }); // => '1.2.3'
|
||||||
|
stringify({ major: 1, minor: 2, patch: 3, beta: 5 }); // => '1.2.3-beta.5'
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="versioncompare"></a>
|
||||||
|
### version.compare
|
||||||
|
|
||||||
|
比較兩個版號物件的大小,回傳正數(a > b)、0(相等)或負數(a < b)。依 major → minor → patch → beta 的順序比較;同號碼時正式版大於 beta 版(`1.2.4` > `1.2.4-beta.3`),beta 版之間依號碼比較。用於在所有 git tag 中挑出最大(最新)的版號。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { parse, compare } = require('./version');
|
||||||
|
|
||||||
|
compare(parse('1.2.4'), parse('1.2.4-beta.3')); // => 1(正式版較大)
|
||||||
|
compare(parse('1.2.4-beta.5'), parse('1.2.4-beta.3')); // => 2(beta 依號碼比較)
|
||||||
|
compare(parse('2.0.0'), parse('1.9.9')); // => 1
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="versionnext"></a>
|
||||||
|
### version.next
|
||||||
|
|
||||||
|
依最新版號與 `is_beta` 旗標計算下一版號。`latest` 為 `null` 時起算 `0.0.1`(beta 為 `0.0.1-beta.1`);最新為 beta 版時,`isBeta=false` 去尾碼轉正式、`isBeta=true` 則 beta 號 +1(無上限);最新為正式版時 patch 進位(各位數滿 9 進位),`isBeta=true` 再加 `-beta.1`。最新正式版已達 `9.9.9` 需再進位時拋出 `Error`(呼叫端輸出 ERR 後以非零 exit code 結束)。
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const { parse, next, stringify } = require('./version');
|
||||||
|
|
||||||
|
stringify(next(parse('1.2.3'), false)); // => '1.2.4'
|
||||||
|
stringify(next(parse('1.2.3'), true)); // => '1.2.4-beta.1'
|
||||||
|
stringify(next(parse('1.2.4-beta.3'), false)); // => '1.2.4'(轉正式)
|
||||||
|
stringify(next(parse('1.2.9'), false)); // => '1.3.0'(滿 9 進位)
|
||||||
|
stringify(next(null, true)); // => '0.0.1-beta.1'
|
||||||
|
```
|
||||||
+47
-7
@@ -1,14 +1,54 @@
|
|||||||
name: 'Gitea Docker Template'
|
# ============================================================================
|
||||||
description: 'Gitea Docker 範本'
|
# 用途:Gitea/GitHub Docker container action「Calculate Next Version」的
|
||||||
|
# metadata 定義檔。宣告此 action 的名稱、輸入(is_beta)、輸出(value)
|
||||||
|
# 與執行方式(runner 於執行時就地以 Dockerfile 建置映像並以 entrypoint.sh 啟動)。
|
||||||
|
# 由 workflow 以 `uses:` 引用本 repo 時,runner 讀取此檔決定如何執行。
|
||||||
|
# 更新時間:2026/07/16 09:17:01
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# action 的顯示名稱:出現在 workflow log 與 marketplace/action 清單中,
|
||||||
|
# 供使用者辨識此步驟在做什麼
|
||||||
|
name: 'Calculate Next Version'
|
||||||
|
|
||||||
|
# action 的一句話用途說明:從 git tag 取得最新版號,依 is_beta 分流計算
|
||||||
|
# 下一個正式版(X.Y.Z)或 beta 版(X.Y.Z-beta.N)版號,各號碼滿 9 進位,
|
||||||
|
# 結果輸出為 value;與 entrypoint.sh 啟動訊息、README 的描述一致
|
||||||
|
description: '從 git tag 取得最新版號,依 is_beta 計算下一個正式版或 beta 版號(各號碼滿 9 進位)並輸出為 value。'
|
||||||
|
|
||||||
|
# action 作者,僅供標示用途,不影響執行
|
||||||
author: 'Jeffery'
|
author: 'Jeffery'
|
||||||
|
|
||||||
|
# 輸入參數區:workflow 以 `with:` 傳入的值,
|
||||||
|
# runner 會轉成 INPUT_<大寫參數名> 環境變數注入容器
|
||||||
inputs:
|
inputs:
|
||||||
message:
|
# is_beta:是否產生 beta 版號;runner 會以 INPUT_IS_BETA 環境變數
|
||||||
description: '輸入訊息'
|
# 傳入容器,由主程式 src/index.js 讀取後決定版號計算分流
|
||||||
|
is_beta:
|
||||||
|
# 參數說明:true 時產生 X.Y.Z-beta.N 格式的 beta 版號,
|
||||||
|
# false(預設)時產生下一個正式版 X.Y.Z
|
||||||
|
description: '是否為 beta 版(true 時產生 X.Y.Z-beta.N 版號)'
|
||||||
|
# 非必填:未提供時使用下方 default 值
|
||||||
required: false
|
required: false
|
||||||
default: 'Hello, World!'
|
# 預設為 'false'(正式版分流);YAML 需以字串表示,
|
||||||
|
# action 輸入一律為字串,布林判斷由主程式自行解析
|
||||||
|
default: 'false'
|
||||||
|
|
||||||
|
# 輸出參數區:供 workflow 後續步驟以 steps.<id>.outputs.value 取用
|
||||||
outputs:
|
outputs:
|
||||||
message:
|
# value:計算出的下一版號;由主程式(src/index.js)將
|
||||||
description: '輸出訊息'
|
# 「value=<版號>」寫入 GITHUB_OUTPUT 檔案而產生,
|
||||||
|
# container action 不需在此宣告 value 的來源(無 composite 的 value: 欄位)
|
||||||
|
value:
|
||||||
|
description: '計算出的下一版號'
|
||||||
|
|
||||||
|
# 執行方式定義區:宣告此 action 為 Docker container action 及其啟動方式
|
||||||
runs:
|
runs:
|
||||||
|
# 使用 docker 執行模式:runner 會以容器方式執行此 action
|
||||||
using: docker
|
using: docker
|
||||||
|
# image 指向 repo 內的 Dockerfile:表示 runner 於「執行時」就地建置映像
|
||||||
|
# (非預建映像),每次執行都可能觸發 docker build(有 layer 快取時較快)
|
||||||
image: dockerfile
|
image: dockerfile
|
||||||
|
# 覆寫容器進入點為 /action/entrypoint.sh:
|
||||||
|
# 該腳本輸出啟動訊息後以 exec 交棒給 node 主程式 /action/src/index.js;
|
||||||
|
# 與 Dockerfile 的 ENTRYPOINT ["/action/entrypoint.sh"] 一致(此處為明確重申)
|
||||||
|
entrypoint: /action/entrypoint.sh
|
||||||
|
|||||||
+54
-6
@@ -1,11 +1,59 @@
|
|||||||
ARG NODE_VERSION=alpine
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
FROM node:${NODE_VERSION}
|
# ============================================================================
|
||||||
|
# 用途:建置「Calculate Next Version」Gitea/GitHub Docker action 的執行映像;
|
||||||
|
# 採兩階段建置(build 整理產物、runtime 以 slim 基底縮小映像),
|
||||||
|
# runtime 額外安裝 git 供主程式讀取 repo tag,最終以 entrypoint.sh 啟動 node 主程式。
|
||||||
|
# 更新時間:2026/07/16 09:16:51
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 1. 參數處理:node 版本以 ARG 注入(預設最新版;--node-version 可覆寫)
|
||||||
|
# 置於第一個 FROM 之前的全域 ARG,兩個 FROM 皆可引用;
|
||||||
|
# NODE_VERSION 決定 build 階段基底、NODE_RUNTIME 決定 runtime 階段基底。
|
||||||
|
# 需人工確認:latest / slim 皆為浮動 tag,未鎖定版本,重建結果可能隨上游更新而改變(不可重現建置)。
|
||||||
|
ARG NODE_VERSION=latest
|
||||||
|
|
||||||
|
# runtime 基底 tag(slim 變體,體積較小);同屬全域 ARG,供第二個 FROM 引用。
|
||||||
|
ARG NODE_RUNTIME=slim
|
||||||
|
|
||||||
|
# ---- build 階段:整理產物 ----
|
||||||
|
# 以完整版 node 映像作為 build 階段基底,僅用來集中整理 action 產物,
|
||||||
|
# 不會進入最終映像,因此體積不影響結果。
|
||||||
|
FROM node:${NODE_VERSION} AS build
|
||||||
|
|
||||||
|
# 設定工作目錄為 /action,後續 COPY/RUN 皆以此為基準路徑。
|
||||||
WORKDIR /action
|
WORKDIR /action
|
||||||
|
|
||||||
COPY src/ /action/
|
# 2. 安裝套件:無外部相依,僅使用 node 內建模組,故無 npm install
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
|
|
||||||
RUN chmod +x /entrypoint.sh
|
# 3. 複製檔案:帶入主程式與入口
|
||||||
|
# 複製 src/(index.js / logger.js / version.js 等主程式模組)到 /action/src/。
|
||||||
|
COPY src/ /action/src/
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
# 複製容器入口腳本,負責輸出啟動訊息並以 exec 啟動 node 主程式。
|
||||||
|
COPY entrypoint.sh /action/entrypoint.sh
|
||||||
|
|
||||||
|
# 4. 執行程序:賦予 entrypoint 執行權限
|
||||||
|
# 確保 entrypoint.sh 可被容器直接執行(避免來源檔案系統權限遺失時 ENTRYPOINT 失敗)。
|
||||||
|
RUN chmod +x /action/entrypoint.sh
|
||||||
|
|
||||||
|
# 5. 縮小映像檔:runtime 改用 slim 基底,只帶必要產物;主程式需呼叫 git 讀 tag
|
||||||
|
# 第二階段為最終映像,捨棄 build 階段中不必要的內容以縮小體積。
|
||||||
|
FROM node:${NODE_RUNTIME} AS runtime
|
||||||
|
|
||||||
|
# 安裝 git(slim 基底未內建),主程式需以 git 指令讀取 repo 的 tag 計算版號;
|
||||||
|
# --no-install-recommends 避免拉入非必要套件,安裝後清除 apt 快取以縮小 layer。
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# 設定最終映像的工作目錄為 /action。
|
||||||
|
WORKDIR /action
|
||||||
|
|
||||||
|
# 自 build 階段複製整理好的產物(src/ 與 entrypoint.sh)到最終映像。
|
||||||
|
COPY --from=build /action /action
|
||||||
|
|
||||||
|
# 6. 設定入口:以 entrypoint.sh 啟動 node 主程式
|
||||||
|
# 使用 exec 形式的 ENTRYPOINT;entrypoint.sh 內以 exec node 取代 shell,
|
||||||
|
# 正確傳遞訊號與 exit code;與 action.yml 的 entrypoint 設定一致。
|
||||||
|
ENTRYPOINT ["/action/entrypoint.sh"]
|
||||||
|
|||||||
Regular → Executable
+30
-6
@@ -1,10 +1,34 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
# ============================================================================
|
||||||
|
# 用途:Calculate Next Version(Docker container action)的容器進入點。
|
||||||
|
# 由 action.yml 的 runs.entrypoint 指定,容器啟動時先輸出統一格式的
|
||||||
|
# 啟動訊息,再以 exec 交棒給 node 主程式 /action/src/index.js 計算下一版號。
|
||||||
|
# 更新時間:2026/07/16 09:16:42
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# 開啟「任一指令失敗即中止」模式:任何指令回傳非 0 就立刻結束腳本,
|
||||||
|
# 避免在錯誤狀態下繼續執行後續步驟,讓 action 失敗能正確反映在 job 結果上
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
echo "================================================"
|
# action 啟動訊息:名稱/用途/更新時間(此檔由 code-action-docker 產生)
|
||||||
echo "Action : Gitea Docker Template"
|
# 依正規化格式 [階段][等級][時間]: 訊息 輸出(階段在前、等級居中、時間在後),
|
||||||
echo "用途 : Gitea Docker 範本"
|
# 讓 workflow log 可以清楚看出 action 名稱與此次啟動的入口
|
||||||
echo "更新時間: 2026/07/02 09:41:31"
|
# 需人工確認:訊息中時間為產生檔案時寫死的固定字串,非執行當下時間;
|
||||||
echo "================================================"
|
# 若期望顯示實際執行時間需另行調整(此處不修改邏輯)
|
||||||
|
echo "[啟動][INF][2026/07/16 09:16:42]: Action:Calculate Next Version"
|
||||||
|
|
||||||
exec node /action/index.js "$@"
|
# 啟動訊息第二行:說明此 action 的用途(與 action.yml 的 description 一致),
|
||||||
|
# 讓使用者在 log 中即可了解版號計算規則(is_beta 分流、各號碼滿 9 進位)
|
||||||
|
echo "[啟動][INF][2026/07/16 09:16:42]: 用途:從 git tag 取得最新版號,依 is_beta 計算下一個正式版或 beta 版號(各號碼滿 9 進位)並輸出為 value。"
|
||||||
|
|
||||||
|
# 啟動訊息第三行:標示此 entrypoint 的更新時間,方便追溯部署的版本
|
||||||
|
echo "[啟動][INF][2026/07/16 09:16:42]: 更新時間:2026/07/16 09:16:42"
|
||||||
|
|
||||||
|
# 啟動 node 主程式(以 exec 取代 shell,正確處理訊號與 exit code):
|
||||||
|
# - exec 讓 node 直接成為 PID 1 的接班程序,SIGTERM 等訊號可直達 node,
|
||||||
|
# 且 node 的 exit code 會原封不動成為容器(即 action step)的結束碼
|
||||||
|
# - "$@" 將容器收到的所有引數原樣轉交給主程式(雖然本 action 實際以
|
||||||
|
# INPUT_IS_BETA 等環境變數傳遞輸入,仍保留引數轉發以維持彈性)
|
||||||
|
# - 主程式副作用:讀取 GITHUB_WORKSPACE 的 git tag、設定 git safe.directory、
|
||||||
|
# 並將計算結果 value 寫入 GITHUB_OUTPUT
|
||||||
|
exec node /action/src/index.js "$@"
|
||||||
|
|||||||
@@ -1,375 +0,0 @@
|
|||||||
# Gitea Docker Container Action 範本
|
|
||||||
|
|
||||||
Docker container(容器)action 讓你把整個執行環境打包成一個 Docker image:action 在你指定的容器內執行,環境、相依套件、工具版本全部固定,跨機器結果一致。適合需要特定系統套件、編譯環境或非 JavaScript 語言撰寫的 action。
|
|
||||||
|
|
||||||
本文件整理 Docker container action `action.yml` 中**所有可用參數、說明與限制**,包含 `Dockerfile` 撰寫注意事項,並特別標出 **Gitea 與 GitHub Actions 的差異**。
|
|
||||||
|
|
||||||
> 語法基準:Gitea Actions 以相容 GitHub Actions metadata 語法為目標,但兩者有明確差異(見「Gitea vs GitHub」章節)。Gitea 端的行為亦受底層 [`act`](https://gitea.com/gitea/act) runner 版本影響,實作前建議以測試機驗證。
|
|
||||||
>
|
|
||||||
> ⚠️ **平台限制**:Docker container action **只能在 Linux runner 上執行**,且該 runner 必須安裝 Docker。Windows / macOS runner 不支援。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目錄
|
|
||||||
|
|
||||||
- [完整結構總覽](#完整結構總覽)
|
|
||||||
- [頂層參數](#頂層參數)
|
|
||||||
- [`inputs`(輸入參數)](#inputs輸入參數)
|
|
||||||
- [`outputs`(輸出)](#outputs輸出)
|
|
||||||
- [`runs`(執行設定)](#runs執行設定)
|
|
||||||
- [`Dockerfile` 撰寫注意事項](#dockerfile-撰寫注意事項)
|
|
||||||
- [Docker action 的限制與注意事項](#docker-action-的限制與注意事項)
|
|
||||||
- [Gitea vs GitHub Actions 差異](#gitea-vs-github-actions-差異)
|
|
||||||
- [本 repo 範例對照](#本-repo-範例對照)
|
|
||||||
- [參考來源](#參考來源)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 完整結構總覽
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: 'Gitea Docker Template' # 必填
|
|
||||||
description: 'Gitea Docker 範本' # 必填
|
|
||||||
author: 'Jeffery' # 選填
|
|
||||||
|
|
||||||
inputs: # 選填,定義輸入參數
|
|
||||||
message:
|
|
||||||
description: '輸入訊息'
|
|
||||||
required: false
|
|
||||||
default: 'Hello, World!'
|
|
||||||
|
|
||||||
outputs: # 選填,定義輸出(docker action 只宣告,不用 value)
|
|
||||||
message:
|
|
||||||
description: '輸出訊息'
|
|
||||||
|
|
||||||
runs: # 必填
|
|
||||||
using: 'docker' # 必填,固定為 docker
|
|
||||||
image: 'Dockerfile' # 必填,本機 Dockerfile 或 docker://image
|
|
||||||
# entrypoint: '/entrypoint.sh' # 選填,覆寫 Dockerfile 的 ENTRYPOINT
|
|
||||||
# pre-entrypoint: '/setup.sh' # 選填,主程式前執行(另開容器)
|
|
||||||
# post-entrypoint: '/cleanup.sh' # 選填,主程式後執行(另開容器)
|
|
||||||
env: # 選填,容器內環境變數
|
|
||||||
GREETING: ${{ inputs.message }}
|
|
||||||
args: # 選填,傳給 ENTRYPOINT 的參數(取代 CMD)
|
|
||||||
- ${{ inputs.message }}
|
|
||||||
|
|
||||||
branding: # 選填(Marketplace 用,Gitea 內部可省略)
|
|
||||||
icon: 'activity'
|
|
||||||
color: 'blue'
|
|
||||||
```
|
|
||||||
|
|
||||||
> 📌 `action.yml`(或 `action.yaml`)放在 action repo 根目錄;`image: 'Dockerfile'` 時,`Dockerfile` 也放在同一目錄。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 頂層參數
|
|
||||||
|
|
||||||
| 參數 | 必填 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `name` | ✅ | Action 名稱。 |
|
|
||||||
| `description` | ✅ | Action 簡短說明。 |
|
|
||||||
| `author` | ❌ | 作者名稱。 |
|
|
||||||
| `inputs` | ❌ | 輸入參數定義(見下)。 |
|
|
||||||
| `outputs` | ❌ | 輸出定義(見下)。 |
|
|
||||||
| `runs` | ✅ | 執行設定;docker 固定用 `using: 'docker'` + `image`。 |
|
|
||||||
| `branding` | ❌ | Marketplace 顯示用的 `icon` 與 `color`(`color` 限 `white`/`black`/`yellow`/`blue`/`green`/`orange`/`red`/`purple`/`gray-dark`;`icon` 為 Feather 圖示名稱)。 |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `inputs`(輸入參數)
|
|
||||||
|
|
||||||
每個 input 是 `inputs.<input_id>` 底下的一組設定:
|
|
||||||
|
|
||||||
| 欄位 | 必填 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `description` | ✅ | 參數說明。 |
|
|
||||||
| `required` | ❌ | 是否必填,布林值,預設 `false`。 |
|
|
||||||
| `default` | ❌ | 預設值;呼叫端沒傳時採用。**只能是字串**。 |
|
|
||||||
| `deprecationMessage` | ❌ | 標記此 input 已棄用,使用時發出警告訊息。 |
|
|
||||||
|
|
||||||
### Docker action 怎麼取用 input
|
|
||||||
|
|
||||||
這是 Docker action **與 composite action 最大的差異**。GitHub / Gitea 會把每個 input 轉成環境變數 `INPUT_<NAME>`:
|
|
||||||
|
|
||||||
- 名稱轉大寫、空白換成底線。例:input `octocat-eye-color` → 環境變數 `INPUT_OCTOCAT_EYE_COLOR`。
|
|
||||||
- input `message` → `INPUT_MESSAGE`。
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
inputs:
|
|
||||||
message:
|
|
||||||
description: '輸入訊息'
|
|
||||||
required: false
|
|
||||||
default: 'Hello, World!'
|
|
||||||
```
|
|
||||||
|
|
||||||
容器內就能直接讀:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
echo "$INPUT_MESSAGE"
|
|
||||||
```
|
|
||||||
|
|
||||||
> ⚠️ **關鍵限制**:`INPUT_*` 環境變數**只在 GitHub 官方 runner 保證自動注入**。若要跨環境(尤其 Gitea/`act`)可靠取值,官方建議**用 `args` 明確把 input 傳進容器**,或在 `runs.env` 自行對應一次(見下方 `runs.env`)。不要單靠 `INPUT_*` 而不驗證。
|
|
||||||
|
|
||||||
**呼叫端傳值**(用 `with`):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: ./
|
|
||||||
with:
|
|
||||||
message: 'Hi there'
|
|
||||||
```
|
|
||||||
|
|
||||||
> input 值一律是**字串**;數字、布林傳進來也會變字串(例如 `"true"`),比較時要留意。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `outputs`(輸出)
|
|
||||||
|
|
||||||
Docker action 的 output **只需宣告 `description`**,**不用**(也不能)像 composite 那樣寫 `value`:
|
|
||||||
|
|
||||||
| 欄位 | 必填 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `description` | ✅ | 輸出說明。 |
|
|
||||||
|
|
||||||
**在容器內設定 output** → 寫入 `$GITHUB_OUTPUT` 檔案(該檔案路徑由 runner 掛載進容器):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
outputs:
|
|
||||||
message:
|
|
||||||
description: '輸出訊息'
|
|
||||||
```
|
|
||||||
|
|
||||||
`entrypoint.sh` 內:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
echo "message=Hello from docker" >> "$GITHUB_OUTPUT"
|
|
||||||
```
|
|
||||||
|
|
||||||
**呼叫端取用 output**:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- id: docker-template
|
|
||||||
uses: ./
|
|
||||||
- run: echo "${{ steps.docker-template.outputs.message }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
> 📦 output 大小限制:單一 job 的 output 上限 **1 MB**,整個 workflow run 所有 output 合計上限 **50 MB**。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `runs`(執行設定)
|
|
||||||
|
|
||||||
Docker action 的核心。`using` 固定為 `docker`,其餘欄位:
|
|
||||||
|
|
||||||
| 欄位 | 必填 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `using` | ✅ | 固定為 `'docker'`。 |
|
|
||||||
| `image` | ✅ | 要跑的 image:本機 `Dockerfile`(檔名必須正好是 `Dockerfile`),或遠端 image 用 `docker://` 前綴(如 `docker://alpine:3.20`、`docker://gcr.io/...`)。 |
|
|
||||||
| `entrypoint` | ❌ | 覆寫 Dockerfile 的 `ENTRYPOINT`;Dockerfile 沒設時等於補上。建議用絕對路徑(如 `/entrypoint.sh`)。 |
|
|
||||||
| `pre-entrypoint` | ❌ | 在主 `entrypoint` **之前**執行的前置腳本。**會另開一個新容器**(同 base image),runtime 狀態與主容器不同——需保留的狀態要放進 workspace、`HOME`,或用 `STATE_` 變數傳遞。 |
|
|
||||||
| `post-entrypoint` | ❌ | 主 `entrypoint` 完成後執行的清理腳本,行為同 `pre-entrypoint`(另開容器)。 |
|
|
||||||
| `pre-if` | ❌ | 條件式,控制 `pre-entrypoint` 是否執行;預設一定跑。 |
|
|
||||||
| `post-if` | ❌ | 條件式,控制 `post-entrypoint` 是否執行;預設一定跑。 |
|
|
||||||
| `args` | ❌ | 字串陣列,啟動時傳給容器 `ENTRYPOINT` 的參數,**取代 Dockerfile 的 `CMD`**。 |
|
|
||||||
| `env` | ❌ | key/value map,容器啟動時設定的環境變數。 |
|
|
||||||
|
|
||||||
### `image`:Dockerfile vs 遠端 image
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
# 用本機 Dockerfile(每次執行前會 build)
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'Dockerfile'
|
|
||||||
|
|
||||||
# 直接拉遠端 image(不用自帶 Dockerfile,啟動快)
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'docker://alpine:3.20'
|
|
||||||
```
|
|
||||||
|
|
||||||
### `args`:怎麼把值送進容器
|
|
||||||
|
|
||||||
`args` 取代 `CMD`,會被當作參數接在 `ENTRYPOINT` 後面:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'Dockerfile'
|
|
||||||
args:
|
|
||||||
- ${{ inputs.message }} # → entrypoint.sh 的 $1
|
|
||||||
- 'foo' # → $2
|
|
||||||
- 'bar' # → $3
|
|
||||||
```
|
|
||||||
|
|
||||||
> ⚠️ 若 `args` 裡放的是**環境變數字串**(如 `- $GREETING`),在 exec-form 的 `ENTRYPOINT` **不會被展開**。要展開變數,讓 entrypoint 走一層 shell(`sh -c`),或改用 `runs.env` 傳值(見下)。
|
|
||||||
|
|
||||||
### `env`:明確傳環境變數(推薦)
|
|
||||||
|
|
||||||
比起依賴 `INPUT_*` 自動注入,用 `runs.env` 把 input 對應成自訂環境變數,跨環境最穩:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'Dockerfile'
|
|
||||||
env:
|
|
||||||
GREETING: ${{ inputs.message }}
|
|
||||||
```
|
|
||||||
|
|
||||||
容器內直接 `echo "$GREETING"`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `Dockerfile` 撰寫注意事項
|
|
||||||
|
|
||||||
Docker action 的 `Dockerfile` 有幾條**強制或強烈建議**的規則,踩到會直接失敗或讀不到檔案:
|
|
||||||
|
|
||||||
1. **`FROM` 必須是第一行**
|
|
||||||
建議用官方 image + 明確版本標籤(如 `python:3.12-slim`),別用 `latest`;Debian/Alpine 系列較穩。
|
|
||||||
|
|
||||||
2. **不要用 `USER`**
|
|
||||||
Docker action **必須以預設的 root 執行**。加了 `USER` 會導致**無法存取 `GITHUB_WORKSPACE`**(掛載進來的 repo 目錄)。
|
|
||||||
|
|
||||||
3. **不要用 `WORKDIR` 指定 entrypoint 位置**
|
|
||||||
runner 會自動把 `GITHUB_WORKSPACE` 掛載上來並設為工作目錄(路徑放在 `$GITHUB_WORKSPACE` 環境變數)。`entrypoint`/腳本一律用**絕對路徑**(如 `/entrypoint.sh`),不要依賴 `WORKDIR`。
|
|
||||||
|
|
||||||
4. **`ENTRYPOINT` 用 exec form(JSON 陣列)**
|
|
||||||
Docker 官方建議寫 `ENTRYPOINT ["/entrypoint.sh"]`。
|
|
||||||
- **exec form**:`args` 能正確以獨立參數傳入,但**不做環境變數展開**(`ENTRYPOINT ["echo", "$GITHUB_SHA"]` 印出的是字面字串)。
|
|
||||||
- **shell form**:`ENTRYPOINT /entrypoint.sh` 會走 shell,可展開變數,但 `args` 傳遞行為不同。
|
|
||||||
- 需要在 entrypoint 展開變數時,用 `ENTRYPOINT ["sh", "-c", "echo $GITHUB_SHA"]`,或寫一支 `entrypoint.sh` 腳本自行處理。
|
|
||||||
|
|
||||||
5. **`CMD` 會被 `args` 蓋掉**
|
|
||||||
`action.yml` 的 `args` 取代 `CMD`。若 action 允許不帶 `args` 也能跑,就在 `Dockerfile` 的 `CMD` 提供預設值,並在 README 說明必要參數。
|
|
||||||
|
|
||||||
6. **`entrypoint.sh` 腳本規範**
|
|
||||||
- 開頭要有 shebang:`#!/bin/sh`(或 `#!/bin/bash`,視 base image 而定)。
|
|
||||||
- 要可執行:`chmod +x entrypoint.sh`(並在 git 中保留執行權限)。
|
|
||||||
- 腳本會收到 `action.yml` 的 `args` 作為位置參數(`$1`, `$2`, …)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Docker action 的限制與注意事項
|
|
||||||
|
|
||||||
以下是實務上最容易踩雷的地方:
|
|
||||||
|
|
||||||
1. **只能跑在 Linux runner,且要有 Docker**
|
|
||||||
Windows / macOS runner 一律不支援 Docker container action。
|
|
||||||
|
|
||||||
2. **`INPUT_*` 不保證可靠,優先用 `args` / `env`**
|
|
||||||
GitHub 官方 runner 會自動注入 `INPUT_<NAME>`,但這在自架 / Gitea 環境未必成立。要穩定取 input,用 `args` 傳位置參數,或 `runs.env` 對應成自訂環境變數。
|
|
||||||
|
|
||||||
3. **`pre-entrypoint` / `post-entrypoint` 是「另開容器」**
|
|
||||||
它們**不共用主 entrypoint 容器的 runtime 狀態**(不是同一個容器內的前後腳本)。要跨階段保留狀態,寫進 `GITHUB_WORKSPACE`、`HOME`,或用 `STATE_` 變數。
|
|
||||||
|
|
||||||
4. **本機 `image: 'Dockerfile'` 每次會 build**
|
|
||||||
用本機 Dockerfile 時,執行前會先 build image,較慢;想加速可改用 `docker://` 拉預先建好的 image。
|
|
||||||
|
|
||||||
5. **只有 `GITHUB_WORKSPACE` 是持久且共用的**
|
|
||||||
容器內對檔案系統的改動,通常只有掛載進來的 `GITHUB_WORKSPACE` 會被後續 step 看到;其他路徑(如 `/tmp`)在跨 step / 跨 action 時不保證保留。
|
|
||||||
|
|
||||||
6. **exec-form `ENTRYPOINT` 不展開變數**
|
|
||||||
如上「Dockerfile 注意事項」第 4 點,需要展開就走 `sh -c` 或 entrypoint 腳本。
|
|
||||||
|
|
||||||
7. **`args` 是字串陣列,順序即位置參數**
|
|
||||||
`args` 的順序對應 entrypoint 的 `$1`, `$2`…;輸入值全是字串。
|
|
||||||
|
|
||||||
8. **不支援 `runs.steps`**
|
|
||||||
那是 composite action 專屬。Docker action 只有一個容器進入點(`entrypoint`)+選配的 pre/post。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Gitea vs GitHub Actions 差異
|
|
||||||
|
|
||||||
Gitea Actions **不是** GitHub Actions 的 100% 複製品。撰寫 Docker action 時特別注意:
|
|
||||||
|
|
||||||
| 項目 | Gitea 行為 |
|
|
||||||
|------|-----------|
|
|
||||||
| **runner 需求** | 執行 Docker action 的 `act_runner` 主機必須裝 Docker,且 runner label 對應到支援容器的環境。`runs-on` 只接受 `runs-on: xyz` 或 `runs-on: [xyz]`,不支援複雜表達式。 |
|
|
||||||
| **`pre-entrypoint` / `post-entrypoint`** | ⚠️ 早期 `act` 版本**完全不執行** pre/post-entrypoint(見 [`nektos/act#2363`](https://github.com/nektos/act/issues/2363),已於 PR #2394 修復)。Gitea 內建的 `act` 版本若較舊可能仍無效——**使用前務必在測試機驗證**。 |
|
|
||||||
| **`INPUT_*` 注入** | 是否自動注入 `INPUT_<NAME>` 取決於 runner 版本,別假設一定有;優先用 `args` / `env` 明確傳值。 |
|
|
||||||
| **表達式函式** | 依官方比較文件,**僅保證支援 `always()`**;`success()` / `failure()` / `cancelled()` / `hashFiles()` 等視 `act` runner 版本而定,寫 `if:`(含 `pre-if` / `post-if`)前先在測試機驗證。 |
|
|
||||||
| **`uses` 支援絕對 URL** | 可寫 `uses: https://github.com/owner/repo@v1` 或 `uses: http://your_gitea/owner/repo@branch`,不限同站 action。 |
|
|
||||||
| **Go actions** | Gitea 額外支援 `using: 'go'` 寫 Go action(GitHub 沒有);`docker` / `node` / `composite` 皆支援。 |
|
|
||||||
| **context 檢查較寬鬆** | Gitea 不檢查 context 可用性,`env` context 可用在比 GitHub 更多的位置(但不代表可攜,跨到 GitHub 會失敗)。 |
|
|
||||||
| **被忽略的 job 欄位** | `jobs.<job_id>.timeout-minutes`、`jobs.<job_id>.continue-on-error`、`jobs.<job_id>.environment` 會被忽略。 |
|
|
||||||
| **annotations / problem matchers** | 不支援,會被忽略。 |
|
|
||||||
| **`permissions` scope** | 支援 `permissions`,但沒有 GitHub 專屬的 `statuses` / `checks` / `deployments` / `id-token` / `security-events` / `pages`;Gitea 有自己的 `code` / `releases` / `wiki` / `projects`。 |
|
|
||||||
|
|
||||||
> 上表以 Gitea 官方文件為準;`act` runner 持續更新,部分限制(尤其表達式函式與 pre/post-entrypoint)可能隨版本放寬,仍以你環境的實測為準。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 本 repo 範例對照
|
|
||||||
|
|
||||||
一個典型 Docker container action 由三個檔案組成,放在 repo 根目錄:
|
|
||||||
|
|
||||||
**1. `action.yml`** — action 定義
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
name: 'Gitea Docker Template'
|
|
||||||
description: 'Gitea Docker 範本'
|
|
||||||
author: 'Jeffery'
|
|
||||||
inputs:
|
|
||||||
message:
|
|
||||||
description: '輸入訊息'
|
|
||||||
required: false
|
|
||||||
default: 'Hello, World!'
|
|
||||||
outputs:
|
|
||||||
message:
|
|
||||||
description: '輸出訊息'
|
|
||||||
runs:
|
|
||||||
using: 'docker'
|
|
||||||
image: 'Dockerfile'
|
|
||||||
args:
|
|
||||||
- ${{ inputs.message }}
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. `Dockerfile`** — 執行環境
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
FROM alpine:3.20
|
|
||||||
|
|
||||||
COPY entrypoint.sh /entrypoint.sh
|
|
||||||
RUN chmod +x /entrypoint.sh
|
|
||||||
|
|
||||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. `entrypoint.sh`** — 主程式
|
|
||||||
|
|
||||||
```sh
|
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
# $1 來自 action.yml 的 args(呼叫端 with.message)
|
|
||||||
MESSAGE="$1"
|
|
||||||
|
|
||||||
echo "Docker action 收到訊息:$MESSAGE"
|
|
||||||
|
|
||||||
# 設定 output 供後續 step 使用
|
|
||||||
echo "message=$MESSAGE" >> "$GITHUB_OUTPUT"
|
|
||||||
```
|
|
||||||
|
|
||||||
**呼叫端**(workflow)用法:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- name: 3. Testing
|
|
||||||
id: docker-template
|
|
||||||
uses: ./
|
|
||||||
with:
|
|
||||||
message: 'Hi there'
|
|
||||||
- name: 4. Feedback
|
|
||||||
run: echo "${{ steps.docker-template.outputs.message }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
> ℹ️ 本 repo 目前的 [`action.yml`](./action.yml) 仍為 composite 範本;要改為 Docker action,依上方三件套調整 `action.yml` 並新增 `Dockerfile`、`entrypoint.sh`。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 參考來源
|
|
||||||
|
|
||||||
- [GitHub Actions — Metadata syntax for actions](https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax)
|
|
||||||
- [GitHub Actions — Dockerfile support for GitHub Actions](https://docs.github.com/en/actions/sharing-automations/creating-actions/dockerfile-support-for-github-actions)
|
|
||||||
- [GitHub Actions — Creating a Docker container action](https://docs.github.com/en/actions/tutorials/creating-a-docker-container-action)
|
|
||||||
- [Gitea — Compared to GitHub Actions](https://docs.gitea.com/usage/actions/comparison)
|
|
||||||
- [Gitea — Actions FAQ](https://docs.gitea.com/usage/actions/faq)
|
|
||||||
- [nektos/act#2363 — pre/post-entrypoint of Docker actions not executed](https://github.com/nektos/act/issues/2363)
|
|
||||||
+162
-8
@@ -1,15 +1,169 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
|
const { execFileSync } = require('child_process');
|
||||||
|
const logger = require('./logger');
|
||||||
|
const { parse, stringify, compare, next } = require('./version');
|
||||||
|
|
||||||
function main() {
|
process.on('uncaughtException', (error) => {
|
||||||
const message = process.env.INPUT_MESSAGE || '';
|
logger.err(`未捕捉例外:${error.stack || error.message}`);
|
||||||
const outputPath = process.env.GITHUB_OUTPUT;
|
process.exit(1);
|
||||||
const line = `message=${message}\n`;
|
});
|
||||||
|
process.on('unhandledRejection', (reason) => {
|
||||||
|
logger.err(`未處理的 Promise 拒絕:${reason instanceof Error ? reason.stack : reason}`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
if (outputPath) {
|
/**
|
||||||
fs.appendFileSync(outputPath, line);
|
* 同步執行 git 指令並回傳標準輸出。
|
||||||
} else {
|
*
|
||||||
process.stdout.write(line);
|
* 先以 TRC 等級記錄實際執行的指令字串,再透過 execFileSync 同步執行,
|
||||||
|
* 回傳去除前後空白的 stdout 內容。
|
||||||
|
*
|
||||||
|
* @param {string[]} args - git 指令的引數陣列(不含 'git' 本身),例如 ['tag', '--list']
|
||||||
|
* @param {string} stage - 日誌用的流程階段名稱,例如 '讀取版號'
|
||||||
|
* @returns {string} git 指令的標準輸出(已 trim);輸出為空時回傳空字串
|
||||||
|
* @throws {Error} git 指令以非零 exit code 結束時,由 execFileSync 拋出(含 stderr 診斷資訊)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const output = git(['tag', '--list'], '讀取版號');
|
||||||
|
* // 日誌:[讀取版號][TRC][時間]: 執行指令:git tag --list
|
||||||
|
*/
|
||||||
|
function git(args, stage) {
|
||||||
|
logger.trc(`執行指令:git ${args.join(' ')}`, stage);
|
||||||
|
return execFileSync('git', args, { encoding: 'utf8' }).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 讀取並驗證 is_beta 輸入。
|
||||||
|
*
|
||||||
|
* 從 INPUT_IS_BETA 環境變數(action 的 is_beta 輸入)讀值,trim 並轉小寫後:
|
||||||
|
* 空字串或 'false' 視為 false、'true' 視為 true;
|
||||||
|
* 其他值輸出 ERR 訊息並以 process.exit(1) 結束程序(不拋出例外)。
|
||||||
|
*
|
||||||
|
* @returns {boolean} 是否以 beta 模式計算下一版號
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* // INPUT_IS_BETA=true 時
|
||||||
|
* const isBeta = readIsBeta(); // => true,並輸出 INF 日誌記錄收到的原始值
|
||||||
|
*/
|
||||||
|
function readIsBeta() {
|
||||||
|
const stage = '讀取輸入';
|
||||||
|
const raw = process.env.INPUT_IS_BETA ?? '';
|
||||||
|
logger.inf(`收到輸入 is_beta="${raw}"`, stage);
|
||||||
|
const normalized = raw.trim().toLowerCase();
|
||||||
|
if (normalized === '' || normalized === 'false') {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
if (normalized === 'true') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
logger.err(`輸入 is_beta 僅接受 true/false,收到:"${raw}"`, stage);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 從 git tag 找出目前最新(最大)的版號。
|
||||||
|
*
|
||||||
|
* 以 GITHUB_WORKSPACE(無則目前工作目錄)為目標 repo,先設定 git safe.directory
|
||||||
|
*(容器內以 root 執行而 workspace 屬於其他使用者時,git 會拒絕存取),
|
||||||
|
* 再列出全部 tag、逐一以 parse() 解析並以 compare() 取最大版號;
|
||||||
|
* 不符合版號格式的 tag 以 DBG 記錄後略過。
|
||||||
|
*
|
||||||
|
* @returns {?{major: number, minor: number, patch: number, beta: ?number}}
|
||||||
|
* 最新版號物件;找不到任何符合格式的 tag 時輸出 WRN 並回傳 null(呼叫端從起始版號計算)
|
||||||
|
* @throws {Error} git 指令執行失敗時,由 git() 拋出
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* const latest = findLatestVersion();
|
||||||
|
* // tag 有 1.2.3、1.2.4-beta.3 時 => { major: 1, minor: 2, patch: 4, beta: 3 }
|
||||||
|
*/
|
||||||
|
function findLatestVersion() {
|
||||||
|
const stage = '讀取版號';
|
||||||
|
const workspace = process.env.GITHUB_WORKSPACE || process.cwd();
|
||||||
|
|
||||||
|
git(['config', '--global', '--add', 'safe.directory', workspace], stage);
|
||||||
|
|
||||||
|
const output = git(['-C', workspace, 'tag', '--list'], stage);
|
||||||
|
const tags = output === '' ? [] : output.split('\n');
|
||||||
|
logger.inf(`git tag 共 ${tags.length} 筆`, stage);
|
||||||
|
|
||||||
|
let latest = null;
|
||||||
|
for (const tag of tags) {
|
||||||
|
const version = parse(tag);
|
||||||
|
if (version === null) {
|
||||||
|
logger.dbg(`略過不符合版號格式的 tag:${tag}`, stage);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
logger.trc(`解析到版號 tag:${tag}`, stage);
|
||||||
|
if (latest === null || compare(version, latest) > 0) {
|
||||||
|
latest = version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (latest === null) {
|
||||||
|
logger.wrn('找不到任何符合 X.Y.Z 或 X.Y.Z-beta.N 格式的 tag,將從起始版號計算', stage);
|
||||||
|
} else {
|
||||||
|
logger.inf(`目前最新版號:${stringify(latest)}`, stage);
|
||||||
|
}
|
||||||
|
return latest;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 將計算結果寫出為 action 的 value 輸出。
|
||||||
|
*
|
||||||
|
* 以 `value=<值>` 格式附加到 GITHUB_OUTPUT 環境變數指向的檔案,
|
||||||
|
* 供 workflow 後續步驟以 steps.<id>.outputs.value 讀取。
|
||||||
|
* GITHUB_OUTPUT 未設定(非 CI 容器環境)時輸出 ERR 並以 process.exit(1) 結束程序。
|
||||||
|
*
|
||||||
|
* @param {string} value - 下一版號字串,格式為 X.Y.Z 或 X.Y.Z-beta.N
|
||||||
|
* @returns {void}
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* writeOutput('1.2.4');
|
||||||
|
* // GITHUB_OUTPUT 檔案被附加一行:value=1.2.4
|
||||||
|
*/
|
||||||
|
function writeOutput(value) {
|
||||||
|
const stage = '寫出結果';
|
||||||
|
const outputPath = process.env.GITHUB_OUTPUT;
|
||||||
|
if (!outputPath) {
|
||||||
|
logger.err('找不到環境變數 GITHUB_OUTPUT,無法寫出輸出', stage);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
fs.appendFileSync(outputPath, `value=${value}\n`);
|
||||||
|
logger.inf(`已寫出輸出 value=${value}`, stage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主流程:計算並輸出下一版號。
|
||||||
|
*
|
||||||
|
* 依序執行:讀取 is_beta 輸入 → 從 git tag 找最新版號 → 以 next() 計算下一版
|
||||||
|
*(進位超過 9.9.9 時輸出 ERR 並 exit(1))→ 寫出 value 輸出。
|
||||||
|
* 檔案頂部的 process.on('uncaughtException') / process.on('unhandledRejection')
|
||||||
|
* 會攔截未捕捉例外,以 ERR 格式輸出後以非零 exit code 結束。
|
||||||
|
* 此函式為容器 entrypoint 啟動 node 後的進入點,於模組載入時直接呼叫。
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function main() {
|
||||||
|
logger.inf('開始計算下一版號');
|
||||||
|
|
||||||
|
const isBeta = readIsBeta();
|
||||||
|
const latest = findLatestVersion();
|
||||||
|
|
||||||
|
const stage = '計算版號';
|
||||||
|
let nextVersion;
|
||||||
|
try {
|
||||||
|
nextVersion = next(latest, isBeta);
|
||||||
|
} catch (error) {
|
||||||
|
logger.err(error.message, stage);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
const value = stringify(nextVersion);
|
||||||
|
logger.inf(`下一版號:${value}(is_beta=${isBeta})`, stage);
|
||||||
|
|
||||||
|
writeOutput(value);
|
||||||
|
logger.inf('計算完成');
|
||||||
}
|
}
|
||||||
|
|
||||||
main();
|
main();
|
||||||
|
|||||||
+147
@@ -0,0 +1,147 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const TIME_ZONE = 'Asia/Taipei';
|
||||||
|
|
||||||
|
const timeFormatter = new Intl.DateTimeFormat('zh-TW', {
|
||||||
|
timeZone: TIME_ZONE,
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 產生當前日期時間的格式化字串(Asia/Taipei 時區)。
|
||||||
|
*
|
||||||
|
* 使用 Intl.DateTimeFormat 將當前時間格式化為「yyyy/MM/dd HH:mm:ss」形式,
|
||||||
|
* 時區固定為台灣時間,確保在任何執行環境中都輸出一致的時間戳。
|
||||||
|
* 由 format() 於組合每一行日誌時呼叫;每次呼叫都取得當下系統時間,無快取。
|
||||||
|
*
|
||||||
|
* @returns {string} 格式化後的時間戳字串,例如 '2026/07/15 14:30:45'
|
||||||
|
*/
|
||||||
|
function timestamp() {
|
||||||
|
const parts = {};
|
||||||
|
for (const { type, value } of timeFormatter.formatToParts(new Date())) {
|
||||||
|
parts[type] = value;
|
||||||
|
}
|
||||||
|
return `${parts.year}/${parts.month}/${parts.day} ${parts.hour}:${parts.minute}:${parts.second}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 組合階段、等級與時間戳,產生統一格式的單行日誌字串。
|
||||||
|
*
|
||||||
|
* 輸出格式為 `[階段][等級][yyyy/MM/dd HH:mm:ss]: 訊息`(階段在前、等級居中、時間在後);
|
||||||
|
* 當 stage 為 falsy(省略、null、空字串)時省略整個 `[階段]` 區塊,
|
||||||
|
* 即 `[等級][yyyy/MM/dd HH:mm:ss]: 訊息`。純函式、無副作用(僅讀取當下系統時間),
|
||||||
|
* 不做參數型別驗證,由 write() 呼叫組出實際要輸出的一行文字。
|
||||||
|
*
|
||||||
|
* @param {string} level - 日誌等級標籤('INF'、'WRN'、'ERR'、'TRC'、'DBG'),不做合法性檢查
|
||||||
|
* @param {string|undefined} stage - 流程階段標籤(選填);falsy 時省略 `[階段]` 區塊
|
||||||
|
* @param {string} message - 日誌訊息內容,直接接在 `: ` 之後輸出,不做淨化或跳脫
|
||||||
|
* @returns {string} 格式化後的單行日誌字串,格式為 `[階段]?[等級][時間]: 訊息`
|
||||||
|
*/
|
||||||
|
function format(level, stage, message) {
|
||||||
|
const stageBlock = stage ? `[${stage}]` : '';
|
||||||
|
return `${stageBlock}[${level}][${timestamp()}]: ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化並輸出一則日誌訊息到主控台。
|
||||||
|
*
|
||||||
|
* 依等級選擇輸出串流:'ERR' 走 console.error(stderr),
|
||||||
|
* 其餘等級走 console.log(stdout)。一次呼叫輸出一行、一行一則訊息。
|
||||||
|
* 此為模組內部函式,外部請使用 module.exports 提供的 inf/wrn/err/trc/dbg。
|
||||||
|
*
|
||||||
|
* @param {string} level - 日誌等級('INF'、'WRN'、'ERR'、'TRC'、'DBG');非 'ERR' 一律輸出到 stdout
|
||||||
|
* @param {string|undefined} stage - 流程階段標籤(選填);falsy 時訊息不含 `[階段]` 區塊
|
||||||
|
* @param {string} message - 日誌訊息主體
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function write(level, stage, message) {
|
||||||
|
const line = format(level, stage, message);
|
||||||
|
if (level === 'ERR') {
|
||||||
|
console.error(line);
|
||||||
|
} else {
|
||||||
|
console.log(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
/**
|
||||||
|
* 輸出 INF(一般資訊)等級的日誌訊息到 stdout。
|
||||||
|
*
|
||||||
|
* 用於記錄流程中的正常進度,例如讀到的輸入、計算結果、寫出的輸出。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* logger.inf('下一版號:1.2.4', '計算版號');
|
||||||
|
* // [計算版號][INF][2026/07/15 14:30:45]: 下一版號:1.2.4
|
||||||
|
*
|
||||||
|
* @param {string} message - 日誌訊息內容
|
||||||
|
* @param {string} [stage] - 選填的流程階段標籤;省略時訊息不含 `[階段]` 區塊
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
inf: (message, stage) => write('INF', stage, message),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 輸出 WRN(警告)等級的日誌訊息到 stdout。
|
||||||
|
*
|
||||||
|
* 用於非致命的異常狀況,例如找不到任何版號 tag、需注意的邊界條件。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* logger.wrn('找不到任何符合格式的 tag,將從起始版號計算', '讀取版號');
|
||||||
|
* // [讀取版號][WRN][2026/07/15 14:30:45]: 找不到任何符合格式的 tag,將從起始版號計算
|
||||||
|
*
|
||||||
|
* @param {string} message - 警告訊息內容
|
||||||
|
* @param {string} [stage] - 選填的流程階段標籤;省略時訊息不含 `[階段]` 區塊
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
wrn: (message, stage) => write('WRN', stage, message),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 輸出 ERR(錯誤)等級的日誌訊息到 stderr(console.error)。
|
||||||
|
*
|
||||||
|
* 用於可預期錯誤與未捕捉例外的回報;呼叫端通常在輸出後以非零 exit code 結束。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* logger.err('找不到環境變數 GITHUB_OUTPUT,無法寫出輸出', '寫出結果');
|
||||||
|
* // [寫出結果][ERR][2026/07/15 14:30:45]: 找不到環境變數 GITHUB_OUTPUT,無法寫出輸出
|
||||||
|
*
|
||||||
|
* @param {string} message - 錯誤訊息內容
|
||||||
|
* @param {string} [stage] - 選填的流程階段標籤;省略時訊息不含 `[階段]` 區塊
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
err: (message, stage) => write('ERR', stage, message),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 輸出 TRC(細部追蹤)等級的日誌訊息到 stdout。
|
||||||
|
*
|
||||||
|
* 用於記錄低層次操作的細節,例如實際執行的外部指令、逐筆解析的中間結果。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* logger.trc('執行指令:git tag --list', '讀取版號');
|
||||||
|
* // [讀取版號][TRC][2026/07/15 14:30:45]: 執行指令:git tag --list
|
||||||
|
*
|
||||||
|
* @param {string} message - 追蹤訊息內容
|
||||||
|
* @param {string} [stage] - 選填的流程階段標籤;省略時訊息不含 `[階段]` 區塊
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
trc: (message, stage) => write('TRC', stage, message),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 輸出 DBG(除錯)等級的日誌訊息到 stdout。
|
||||||
|
*
|
||||||
|
* 用於協助開發者追蹤程式流程與詳細狀態,例如被略過的不符格式 tag。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* logger.dbg(`略過不符合版號格式的 tag:${tag}`, '讀取版號');
|
||||||
|
* // [讀取版號][DBG][2026/07/15 14:30:45]: 略過不符合版號格式的 tag:not-a-version
|
||||||
|
*
|
||||||
|
* @param {string} message - 除錯訊息內容
|
||||||
|
* @param {string} [stage] - 選填的流程階段標籤;省略時訊息不含 `[階段]` 區塊
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
dbg: (message, stage) => write('DBG', stage, message),
|
||||||
|
};
|
||||||
+148
@@ -0,0 +1,148 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
// 版號格式:X.Y.Z 或 X.Y.Z-beta.N(不帶前綴)
|
||||||
|
const VERSION_PATTERN = /^(\d+)\.(\d+)\.(\d+)(?:-beta\.(\d+))?$/;
|
||||||
|
|
||||||
|
// major / minor / patch 各位數上限;beta 號碼無上限
|
||||||
|
const MAX_DIGIT = 9;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析版號字串為版號物件。
|
||||||
|
*
|
||||||
|
* 支援正式版(X.Y.Z)與 beta 版(X.Y.Z-beta.N)兩種格式;
|
||||||
|
* 用於逐一解析 git tag,把 tag 字串轉成可比較、可計算的結構化物件。
|
||||||
|
* 不符合格式(含 null、非字串轉出的值)一律回傳 null,不拋出例外。
|
||||||
|
*
|
||||||
|
* @param {string} text - 待解析的版號字串,格式為 X.Y.Z 或 X.Y.Z-beta.N
|
||||||
|
* @returns {?{major: number, minor: number, patch: number, beta: ?number}}
|
||||||
|
* 版號物件;beta 為 null 表示正式版,為數字表示 beta 序號。
|
||||||
|
* 輸入不符合版號格式時回傳 null。
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* parse('1.2.3'); // => { major: 1, minor: 2, patch: 3, beta: null }
|
||||||
|
* parse('1.2.3-beta.5'); // => { major: 1, minor: 2, patch: 3, beta: 5 }
|
||||||
|
* parse('v1.2'); // => null(不符合格式)
|
||||||
|
*/
|
||||||
|
function parse(text) {
|
||||||
|
const match = VERSION_PATTERN.exec(text);
|
||||||
|
if (!match) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
major: Number(match[1]),
|
||||||
|
minor: Number(match[2]),
|
||||||
|
patch: Number(match[3]),
|
||||||
|
beta: match[4] === undefined ? null : Number(match[4]),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 將版號物件轉換為版號字串。
|
||||||
|
*
|
||||||
|
* beta 為 null 時輸出正式版格式 `X.Y.Z`,否則輸出 `X.Y.Z-beta.N`;
|
||||||
|
* 為 parse() 的反向操作,用於日誌輸出與最終寫出 action 的 value。
|
||||||
|
*
|
||||||
|
* @param {{major: number, minor: number, patch: number, beta: ?number}} version - 版號物件
|
||||||
|
* @returns {string} 版號字串,例如 '1.2.3' 或 '1.2.3-beta.5'
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* stringify({ major: 1, minor: 2, patch: 3, beta: null }); // => '1.2.3'
|
||||||
|
* stringify({ major: 1, minor: 2, patch: 3, beta: 5 }); // => '1.2.3-beta.5'
|
||||||
|
*/
|
||||||
|
function stringify(version) {
|
||||||
|
const core = `${version.major}.${version.minor}.${version.patch}`;
|
||||||
|
return version.beta === null ? core : `${core}-beta.${version.beta}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 比較兩個版號物件的大小。
|
||||||
|
*
|
||||||
|
* 依 major → minor → patch → beta 的順序比較;同號碼時正式版大於 beta 版
|
||||||
|
*(1.2.4 > 1.2.4-beta.3),beta 版之間依號碼比較。
|
||||||
|
* 用於在所有 git tag 中挑出最大(最新)的版號。
|
||||||
|
*
|
||||||
|
* @param {{major: number, minor: number, patch: number, beta: ?number}} a - 第一個版號物件
|
||||||
|
* @param {{major: number, minor: number, patch: number, beta: ?number}} b - 第二個版號物件
|
||||||
|
* @returns {number} 正數表示 a > b、0 表示相等、負數表示 a < b
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* compare(parse('1.2.4'), parse('1.2.4-beta.3')); // => 1(正式版大於同號 beta)
|
||||||
|
* compare(parse('1.2.4-beta.5'), parse('1.2.4-beta.3')); // => 2(beta 依號碼比較)
|
||||||
|
*/
|
||||||
|
function compare(a, b) {
|
||||||
|
if (a.major !== b.major) return a.major - b.major;
|
||||||
|
if (a.minor !== b.minor) return a.minor - b.minor;
|
||||||
|
if (a.patch !== b.patch) return a.patch - b.patch;
|
||||||
|
if (a.beta === b.beta) return 0;
|
||||||
|
if (a.beta === null) return 1;
|
||||||
|
if (b.beta === null) return -1;
|
||||||
|
return a.beta - b.beta;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 將版號的 patch +1,並處理各位數滿 9 的進位。
|
||||||
|
*
|
||||||
|
* patch 滿 9 進位到 minor、minor 滿 9 進位到 major(1.2.9 → 1.3.0、1.9.9 → 2.0.0);
|
||||||
|
* major 超過 9(即 9.9.9 再進位)時拋出 Error。回傳的是新物件(beta 一律為 null,
|
||||||
|
* 即轉為正式版基底),不修改傳入的 version。由 next() 在最新版為正式版時呼叫。
|
||||||
|
*
|
||||||
|
* @param {{major: number, minor: number, patch: number, beta: ?number}} version - 目前版號物件;beta 欄位會被忽略
|
||||||
|
* @returns {{major: number, minor: number, patch: number, beta: null}} 進位後的新版號物件
|
||||||
|
* @throws {Error} 版號已達 9.9.9 無法再進位時拋出
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* bumpPatch({ major: 1, minor: 2, patch: 3, beta: null }); // => 1.2.4
|
||||||
|
* bumpPatch({ major: 1, minor: 2, patch: 9, beta: null }); // => 1.3.0
|
||||||
|
* bumpPatch({ major: 9, minor: 9, patch: 9, beta: null }); // => throws Error
|
||||||
|
*/
|
||||||
|
function bumpPatch(version) {
|
||||||
|
let { major, minor, patch } = version;
|
||||||
|
patch += 1;
|
||||||
|
if (patch > MAX_DIGIT) {
|
||||||
|
patch = 0;
|
||||||
|
minor += 1;
|
||||||
|
}
|
||||||
|
if (minor > MAX_DIGIT) {
|
||||||
|
minor = 0;
|
||||||
|
major += 1;
|
||||||
|
}
|
||||||
|
if (major > MAX_DIGIT) {
|
||||||
|
throw new Error(`版號 ${stringify(version)} 已達上限 ${MAX_DIGIT}.${MAX_DIGIT}.${MAX_DIGIT},無法再進位`);
|
||||||
|
}
|
||||||
|
return { major, minor, patch, beta: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 依最新版號與 is_beta 旗標計算下一版號。
|
||||||
|
*
|
||||||
|
* 三種情境:
|
||||||
|
* 1. latest 為 null(repo 無任何版號 tag):起算 0.0.1,isBeta 時為 0.0.1-beta.1。
|
||||||
|
* 2. 最新是 beta 版:isBeta=false 去掉 beta 尾碼轉正式(1.2.4-beta.3 → 1.2.4);
|
||||||
|
* isBeta=true 則 beta 號 +1(beta.9 → beta.10,無上限)。
|
||||||
|
* 3. 最新是正式版:patch 進位(滿 9 進位),isBeta 時再加上 -beta.1。
|
||||||
|
* 為本 action 版號規則的核心,由 main() 於 CI 流程中呼叫以產生下一個 git tag。
|
||||||
|
*
|
||||||
|
* @param {?{major: number, minor: number, patch: number, beta: ?number}} latest - 最新版號物件(parse() 的結果),無先前版號時為 null
|
||||||
|
* @param {boolean} isBeta - 是否產生 beta 版號
|
||||||
|
* @returns {{major: number, minor: number, patch: number, beta: ?number}} 下一版號物件
|
||||||
|
* @throws {Error} 最新正式版已達 9.9.9 需再進位時拋出(來自 bumpPatch)
|
||||||
|
*
|
||||||
|
* @example
|
||||||
|
* next(parse('1.2.3'), false); // => 1.2.4
|
||||||
|
* next(parse('1.2.3'), true); // => 1.2.4-beta.1
|
||||||
|
* next(parse('1.2.4-beta.3'), false); // => 1.2.4(轉正式)
|
||||||
|
* next(null, false); // => 0.0.1
|
||||||
|
*/
|
||||||
|
function next(latest, isBeta) {
|
||||||
|
if (latest === null) {
|
||||||
|
return { major: 0, minor: 0, patch: 1, beta: isBeta ? 1 : null };
|
||||||
|
}
|
||||||
|
if (latest.beta !== null) {
|
||||||
|
// 最新是 beta:轉正式去尾碼;續 beta 則號碼 +1(無上限)
|
||||||
|
return { ...latest, beta: isBeta ? latest.beta + 1 : null };
|
||||||
|
}
|
||||||
|
const bumped = bumpPatch(latest);
|
||||||
|
return { ...bumped, beta: isBeta ? 1 : null };
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { parse, stringify, compare, next };
|
||||||
Reference in New Issue
Block a user