diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/.gitea/ai-review/findings.json @@ -0,0 +1 @@ +[] diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index ea69542..e6d542f 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,9 +1,14 @@ +# -------------------------------------------------- +# 用途:定義 CI 工作流程,處理 pull request 的版本發佈與測試。 +# 更新日期:2026/07/11 17:44:33(Asia/Taipei) +# -------------------------------------------------- + name: CI on: pull_request: branches: - - master - - develop + - master + - develop types: [opened, synchronize] jobs: build: @@ -16,13 +21,13 @@ jobs: version: ${{ env.VERSION }} is_beta: ${{ env.IS_BETA }} steps: - - name: Publishing Release - uses: akkuman/gitea-release-action@${{ vars.ACTION_GITEA_RELEASE_VERSION }} - with: - name: "${{ gitea.event.repository.name }} v${{ env.VERSION }}" - tag_name: "v${{ env.VERSION }}" - target_commitish: ${{ gitea.sha }} - prerelease: ${{ env.IS_BETA }} + - name: Publishing Release + uses: akkuman/gitea-release-action@${{ vars.ACTION_GITEA_RELEASE_VERSION }} + with: + name: "${{ gitea.event.repository.name }} v${{ env.VERSION }}" + tag_name: "v${{ env.VERSION }}" + target_commitish: ${{ gitea.sha }} + prerelease: ${{ env.IS_BETA }} test: name: 2. TEST runs-on: ubuntu @@ -33,23 +38,36 @@ jobs: outputs: version: ${{ steps.calculate-version.outputs.version }} steps: - - name: Run Calculate Version - id: calculate-version - uses: https://gitea.jsc.idv.tw/actions/calculate-version@v${{ env.VERSION }} - with: - is_beta: ${{ env.IS_BETA == "true" }} + - name: Run Calculate Version + id: calculate-version + uses: https://gitea.jsc.idv.tw/actions/calculate-version@v${{ env.VERSION }} + with: + is_beta: ${{ env.IS_BETA == "true" }} + - name: Setup LLM CLI + uses: https://gitea.jsc.idv.tw/actions/setup-${{ vars.ACTION_SETUP_LLM_CLI }} + if: ${{ env.IS_BETA == 'true' }} + with: + oauth: ${{ secrets.LLM_OAUTH }} + - name: Run AI Code Review + uses: https://gitea.jsc.idv.tw/actions/ai-code-review@${{ vars.ACTION_AI_CODE_REVIEW_VERSION }} + id: ai-code-review + if: ${{ env.IS_BETA == 'true' }} + with: + token: ${{ secrets.TOKEN }} + model: ${{ vars.LLM_NAME }} result: name: 3. RESULT runs-on: ubuntu - needs: [build,test] + needs: [build, test] + if: ${{ needs.build.outputs.is_beta == 'false' }} env: VERSION: ${{ needs.test.outputs.version }} IS_BETA: ${{ needs.build.outputs.is_beta }} steps: - - name: Publishing Release - uses: akkuman/gitea-release-action@${{ vars.ACTION_GITEA_RELEASE_VERSION }} - with: - name: "${{ gitea.event.repository.name }} v${{ env.VERSION }}" - tag_name: "v${{ env.VERSION }}" - target_commitish: ${{ gitea.sha }} - prerelease: ${{ env.IS_BETA == "true" }} + - name: Publishing Release + uses: akkuman/gitea-release-action@${{ vars.ACTION_GITEA_RELEASE_VERSION }} + with: + name: "${{ gitea.event.repository.name }} v${{ env.VERSION }}" + tag_name: "v${{ env.VERSION }}" + target_commitish: ${{ gitea.sha }} + prerelease: ${{ env.IS_BETA == "true" }} diff --git a/.gitea/workflows/master.yaml b/.gitea/workflows/master.yaml index 01831cc..00522ef 100644 --- a/.gitea/workflows/master.yaml +++ b/.gitea/workflows/master.yaml @@ -1,8 +1,13 @@ +# -------------------------------------------------- +# 用途:定義 CD 工作流程,於 master push 後部署並顯示相關 tag。 +# 更新日期:2026/07/11 17:44:33(Asia/Taipei) +# -------------------------------------------------- + name: CD on: push: branches: - - master + - master jobs: deploy: name: DEPLOY @@ -10,12 +15,14 @@ jobs: env: COMMIT_SHA: ${{ gitea.event.commits[1].id }} steps: - - name: Source Code Checkout - uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} - with: - 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 - - name: Show Tag - run: echo "${{ steps.commit.outputs.tag }}" + - name: Source Code Checkout + uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} + with: + 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 + - name: Show Tag + run: >- + echo "[DEPLOY][INF][$(TZ='Asia/Taipei' date +'%Y/%m/%d %H:%M:%S')]: ${{ steps.commit.outputs.tag }}" diff --git a/.gitea/workflows/readme.md b/.gitea/workflows/readme.md index fd64672..99db59a 100644 --- a/.gitea/workflows/readme.md +++ b/.gitea/workflows/readme.md @@ -1,9 +1,41 @@ -# GITEA COMPOSITE ACTION 的工作流列表 +# 工作流程說明 -- CI - - BUILD - - TEST - - RESULT -- CD - - BUILD - - DEPLOY \ No newline at end of file +以下內容整理 `.gitea/workflows/` 目前的工作流程與用途,作為後續覆蓋 `.gitea/workflows/readme.md` 的說明文件。 + +## 總覽 + +| Workflow 名稱 | 檔案位置 | 觸發條件 | 主要用途 | 相關參數 | +| ------------- | ------------------------------ | --------------------------------------------------------------------------- | ------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| CI | `.gitea/workflows/ci.yaml` | `pull_request` 到 `master` 與 `develop`,事件型別為 `opened`、`synchronize` | 在 PR 階段建立 release、計算版本,beta 分支還會執行 AI code review | `VERSION`、`IS_BETA`、`vars.ACTION_GITEA_RELEASE_VERSION`、`vars.ACTION_SETUP_LLM_CLI`、`vars.ACTION_AI_CODE_REVIEW_VERSION`、`secrets.LLM_OAUTH`、`secrets.TOKEN`、`vars.LLM_NAME` | +| CD | `.gitea/workflows/master.yaml` | `push` 到 `master` | 根據 commit 找出對應 tag,供部署流程或後續檢查使用 | `COMMIT_SHA`、`vars.ACTION_CHECKOUT_VERSION` | + +## CI + +- 檔案位置:`.gitea/workflows/ci.yaml` +- 觸發條件:`pull_request` 到 `master` 與 `develop`,事件型別包含 `opened` 與 `synchronize` +- 用途說明:先發佈 release 草稿,再呼叫 `calculate-version` action 計算版本;若是 beta 分支,還會設定 LLM CLI 並執行 AI code review。 +- 主要輸入 / 環境參數: + - `VERSION`:預設為 `0.0.0-beta.${{ gitea.run_number }}`,供後續 job 使用。 + - `IS_BETA`:由 `gitea.base_ref == 'develop'` 推得,控制 beta 與正式版流程。 + - `vars.ACTION_GITEA_RELEASE_VERSION`:`gitea-release-action` 版本。 + - `vars.ACTION_SETUP_LLM_CLI`:LLM CLI 安裝 action 版本。 + - `vars.ACTION_AI_CODE_REVIEW_VERSION`:AI code review action 版本。 + - `secrets.LLM_OAUTH`、`secrets.TOKEN`:beta 流程所需憑證。 + - `vars.LLM_NAME`:AI code review 使用的模型名稱。 +- 重要注意事項: + - `result` job 只在 `IS_BETA == 'false'` 時執行。 + - `test` job 的 action 來源是 `https://gitea.jsc.idv.tw/actions/calculate-version@v${{ env.VERSION }}`,版本號需與 release 流程對齊。 + - 若 `develop` 與 `master` 的 release 節奏不同,需確認 `VERSION` 的預設生成邏輯是否符合預期。 + +## CD + +- 檔案位置:`.gitea/workflows/master.yaml` +- 觸發條件:`push` 到 `master` +- 用途說明:檢出原始碼後,根據提交 SHA 找出對應 tag,並輸出查得的結果。 +- 主要輸入 / 環境參數: + - `COMMIT_SHA`:目前流程中使用 `gitea.event.commits[1].id`,需確認該索引在所有 push 事件下都存在。 + - `vars.ACTION_CHECKOUT_VERSION`:`actions/checkout` 版本。 +- 重要注意事項: + - `fetch-depth: 0` 是必要條件,否則 `git for-each-ref --contains` 可能找不到完整 tag。 + - `Get Commit Tag` 的 `run` 指令假設 tag 存在;若找不到,輸出會是空字串,需人工確認下游是否能接受。 + - `Show Tag` 目前採用統一輸出格式草稿,正式實作時仍需確認 shell 內的時間取得方式。 diff --git a/Dockerfile b/Dockerfile index e4751be..f2d8aca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,3 +1,8 @@ +# -------------------------------------------------- +# 用途:定義 calculate-version action 的 Docker 執行環境。 +# 更新日期:2026/07/11 17:44:33(Asia/Taipei) +# -------------------------------------------------- + FROM node:lts-alpine WORKDIR /action diff --git a/README.md b/README.md new file mode 100644 index 0000000..bedaeba --- /dev/null +++ b/README.md @@ -0,0 +1,151 @@ +# calculate-version + +更新時間:2026/07/11 17:53:32(Asia/Taipei) + +這個專案是一個 Gitea Docker action,會讀取 repository 的 release 清單,計算下一個正式版或 beta 版本號,並將結果輸出成 action output。 + +## 專案列表 + +### 專案描述表 + +| 專案名稱 | 專案描述 | +| --- | --- | +| [calculate-version](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/) | 這是一個以 Node.js 實作的 Gitea Docker action,提供 release 擷取、版本解析、beta 推算與 action 主流程輸出。 | + +### 參考專案表 + +| 專案名稱 | 參考專案列表 | +| --- | --- | +| [calculate-version](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/) | 無 | + +### NuGet 套件表 + +| 專案名稱 | NuGet 套件列表 | +| --- | --- | +| [calculate-version](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/) | 無 | + +## 功能列表 + +### calculate-version + +| 功能名稱 | 功能描述 | +| --- | --- | +| [calculate-version.normalizeBetaFlag](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L72) | [把空值或 `null` 正規化成 beta 旗標字串](#calculateversionnormalizebetaflag) | +| [calculate-version.parseVersionParts](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L107) | [把版本字串拆成數字片段](#calculateversionparseversionparts) | +| [calculate-version.compareVersionParts](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L125) | [逐段比較版本片段,供排序使用](#calculateversioncompareversionparts) | +| [calculate-version.stableVersionFromTag](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L148) | [解析穩定版 tag 為版本片段](#calculateversionstableversionfromtag) | +| [calculate-version.latestStableVersion](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L169) | [從 release 清單找出最新穩定版](#calculateversionlateststableversion) | +| [calculate-version.nextReleaseVersion](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L195) | [將正式版版本號遞增一版](#calculateversionnextreleaseversion) | +| [calculate-version.nextBetaNumber](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L224) | [推算下一個 beta 編號](#calculateversionnextbetanumber) | +| [calculate-version.calculateVersion](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L252) | [回傳目前最新版本與下一個版本](#calculateversioncalculateversion) | +| [calculate-version.fetchReleases](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L334) | [分頁抓取 repository release 清單](#calculateversionfetchreleases) | +| [calculate-version.main](https://gitea.jsc.idv.tw/actions/calculate-version/src/branch/ai-review-resolve/develop-20260711-150530/src/index.js#L373) | [執行 action 主流程並輸出結果](#calculateversionmain) | + +## 使用範例 + +### calculate-version.normalizeBetaFlag +把空值、`null` 或未定義值正規化成 `false`,讓後續版本判斷只處理字串。 + +```js +const { normalizeBetaFlag } = require('./src/index.js'); + +const isBeta = normalizeBetaFlag(process.env.IS_BETA); +``` + +### calculate-version.parseVersionParts +將版本字串拆成數字陣列,非數字片段會保守視為 `0`。 + +```js +const { parseVersionParts } = require('./src/index.js'); + +const parts = parseVersionParts('1.2.3'); +``` + +### calculate-version.compareVersionParts +逐段比較兩組版本片段,適合搭配 `Array.prototype.sort()` 使用。 + +```js +const { compareVersionParts } = require('./src/index.js'); + +const sorted = [[1, 2, 10], [1, 2, 3]].sort(compareVersionParts); +``` + +### calculate-version.stableVersionFromTag +從 tag 名稱擷取穩定版版本資訊,像 `v1.2.3` 會被解析成版本片段。 + +```js +const { stableVersionFromTag } = require('./src/index.js'); + +const parts = stableVersionFromTag('v1.2.3'); +``` + +### calculate-version.latestStableVersion +從 release 清單中找出最新的穩定版版本號,找不到時回傳 `0.0.0`。 + +```js +const { latestStableVersion } = require('./src/index.js'); + +const latest = latestStableVersion([ + { tag_name: 'v1.2.2' }, + { tag_name: 'v1.2.3-beta.1' }, + { tag_name: 'v1.2.3' }, +]); +``` + +### calculate-version.nextReleaseVersion +將正式版版本號遞增一版,`patch` 與 `minor` 都會以 10 為進位界線。 + +```js +const { nextReleaseVersion } = require('./src/index.js'); + +const nextVersion = nextReleaseVersion('1.2.9'); +``` + +### calculate-version.nextBetaNumber +推算下一個 beta 編號,會找出目前版本對應的最大 `beta.N` 再加一。 + +```js +const { nextBetaNumber } = require('./src/index.js'); + +const betaNumber = nextBetaNumber([ + { tag_name: 'v1.2.4-beta.1' }, + { tag_name: 'v1.2.4-beta.3' }, +], '1.2.4'); +``` + +### calculate-version.calculateVersion +回傳目前最新版本與下一個版本,beta 模式時會產生對應的 `-beta.N` 結果。 + +```js +const { calculateVersion } = require('./src/index.js'); + +const [latest, nextVersion] = calculateVersion([ + { tag_name: 'v1.2.3' }, + { tag_name: 'v1.2.4-beta.1' }, +], 'false'); +``` + +### calculate-version.fetchReleases +分頁抓取 repository 的 release 清單,直到 API 回傳空資料或最後一頁不足頁面上限。 + +```js +const { fetchReleases } = require('./src/index.js'); + +// 需人工確認:baseUrl 與 repository 必須對應實際 Gitea 環境。 +(async () => { + const releases = await fetchReleases('https://gitea.example.com', 'owner/repo', 'token-value'); + console.log(releases); +})(); +``` + +### calculate-version.main +執行 action 主流程,會讀取環境變數、抓取 release、計算版本並輸出結果。 + +```js +const { main } = require('./src/index.js'); + +// 需人工確認:這個入口通常應由 Gitea action 啟動,且需要完整環境變數。 +(async () => { + await main(); +})(); +``` diff --git a/action.yml b/action.yml index 009edd3..513435b 100644 --- a/action.yml +++ b/action.yml @@ -1,19 +1,24 @@ -name: 'Calculate Version' -description: '計算版本號' -author: 'Jeffery' +# -------------------------------------------------- +# 用途:宣告 Calculate Version action 的輸入、輸出與 Docker 執行方式。 +# 更新日期:2026/07/11 17:44:33(Asia/Taipei) +# -------------------------------------------------- + +name: "Calculate Version" +description: "計算版本號" +author: "Jeffery" inputs: is_beta: - description: '是否為 beta 版本' - default: 'false' + description: "是否為 beta 版本" + default: "false" runner_token: - description: '用於讀取 release 的 token' + description: "用於讀取 release 的 token" required: false outputs: version: - description: '計算出的版本號' + description: "計算出的版本號" runs: - using: 'docker' - image: 'Dockerfile' + using: "docker" + image: "Dockerfile" env: GITEA_SERVER_URL: ${{ gitea.server_url }} GITEA_REPOSITORY: ${{ gitea.repository }} diff --git a/entrypoint.sh b/entrypoint.sh index 14d6d5a..10ee879 100755 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,4 +1,9 @@ #!/bin/sh +# -------------------------------------------------- +# 用途:啟動 calculate-version action,將所有參數交給 Node 主程式。 +# 更新日期:2026/07/11 17:44:33(Asia/Taipei) +# -------------------------------------------------- + set -eu exec node /action/src/index.js "$@" diff --git a/src/index.js b/src/index.js index 87f9840..092c0f1 100644 --- a/src/index.js +++ b/src/index.js @@ -4,19 +4,78 @@ const https = require('https'); const { URL } = require('url'); const RELEASES_PER_PAGE = 10; +let currentStage = null; +/** + * 設定目前訊息輸出的階段名稱。 + * + * @param {string} title 階段名稱,通常是流程區塊名稱。 + * @remarks + * 供 `info()` 與錯誤輸出使用,讓訊息可以帶上統一前綴。 + */ function section(title) { - process.stdout.write(`\n==================================================\n${title}\n--------------------------------------------------\n`); + currentStage = title; } +/** + * 產生統一格式的時間戳記字串。 + * + * @returns {string} Asia/Taipei 時區格式化後的時間戳記。 + * @remarks + * 由 `info()` 與錯誤輸出共用,避免不同輸出路徑各自拼出不同格式。 + */ +function formatTimestamp() { + return new Date() + .toLocaleString('sv-SE', { timeZone: 'Asia/Taipei', hour12: false }) + .replace(/-/g, '/'); +} + +/** + * 以統一格式組裝單行 log。 + * + * @param {string} level 訊息等級。 + * @param {string} message 要輸出的訊息內容。 + * @returns {string} 已格式化完成且含換行的 log 字串。 + * @remarks + * 讓一般資訊與錯誤訊息共用同一套前綴格式,降低未來調整格式時的維護成本。 + */ +function formatLogLine(level, message) { + const stagePrefix = currentStage ? `[${currentStage}]` : ''; + + return `${stagePrefix}[${level}][${formatTimestamp()}]: ${message}\n`; +} + +/** + * 以統一格式輸出資訊訊息。 + * + * @param {string} message 要輸出的訊息內容。 + * @remarks + * 會附加目前階段與 Asia/Taipei 時區時間戳記,適合用於流程進度與狀態輸出。 + */ function info(message) { - process.stdout.write(`[info] ${message}\n`); + process.stdout.write(formatLogLine('INF', message)); } +/** + * 以例外方式終止流程。 + * + * @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} 未設定`); @@ -25,6 +84,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'; @@ -33,6 +100,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; @@ -45,6 +119,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('.') @@ -54,6 +136,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); @@ -69,6 +160,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; @@ -82,6 +181,14 @@ function stableVersionFromTag(tagName) { return parseVersionParts(versionText); } +/** + * 取得最新的穩定版版本號。 + * + * @param {Array} releaseJson release API 回傳資料。 + * @returns {string} 最新穩定版版本號;找不到時回傳 `0.0.0`。 + * @remarks + * 主要用於計算下一版版本號的基礎值,會忽略 beta 標籤與不合法版本字串。 + */ function latestStableVersion(releaseJson) { if (!Array.isArray(releaseJson) || releaseJson.length === 0) { return '0.0.0'; @@ -100,6 +207,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; @@ -120,6 +235,15 @@ function nextReleaseVersion(latestVersion) { return `${major}.${minor}.${patch}`; } +/** + * 計算下一個 beta 編號。 + * + * @param {Array} 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; @@ -139,6 +263,15 @@ function nextBetaNumber(releaseJson, version) { return Math.max(...values) + 1; } +/** + * 計算目前最新版本與下一個版本。 + * + * @param {Array} 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' @@ -157,6 +290,15 @@ function calculateVersion(releaseJson, isBeta) { return [baseVersion, nextVersion]; } +/** + * 以 HTTP/HTTPS 取得 JSON 回應。 + * + * @param {URL} url 請求目標。 + * @param {string} token 驗證 token;空值或字串 `null` 會被忽略。 + * @returns {Promise} 成功時解析為 JSON 物件或空字串。 + * @remarks + * 失敗時會拋出錯誤,適合拿來包裝 release API 的低階請求。 + */ function requestJson(url, token) { return new Promise((resolve, reject) => { const client = url.protocol === 'http:' ? http : https; @@ -202,6 +344,16 @@ function requestJson(url, token) { }); } +/** + * 逐頁抓取 repository 的 release 清單。 + * + * @param {string|URL} baseUrl Gitea API 基底網址。 + * @param {string} repository repository 路徑。 + * @param {string} token API 存取 token。 + * @returns {Promise>} 合併後的 release 清單。 + * @remarks + * 會依 `RELEASES_PER_PAGE` 分頁,當 API 回傳空資料或最後一頁不足數量時停止。 + */ async function fetchReleases(baseUrl, repository, token) { const combined = []; let page = 1; @@ -234,6 +386,13 @@ async function fetchReleases(baseUrl, repository, token) { return combined; } +/** + * 執行 calculate-version action 的主流程。 + * + * @returns {Promise} 完成時不回傳值,失敗時會拋出例外。 + * @remarks + * 需要在 Gitea action 環境中執行,並依賴 `GITEA_SERVER_URL`、`GITEA_REPOSITORY` 與 token 相關環境變數。 + */ async function main() { section('參數檢查'); @@ -262,7 +421,7 @@ async function main() { if (require.main === module) { main().catch((error) => { - process.stderr.write(`[error] ${error.message}\n`); + process.stderr.write(formatLogLine('ERR', error.message)); process.exit(1); }); } diff --git a/src/index.test.js b/src/index.test.js new file mode 100644 index 0000000..e655888 --- /dev/null +++ b/src/index.test.js @@ -0,0 +1,26 @@ +const assert = require('node:assert/strict'); +const { spawnSync } = require('node:child_process'); +const path = require('node:path'); +const test = require('node:test'); + +test('主流程失敗時會輸出階段前綴與錯誤碼', () => { + const result = spawnSync( + process.execPath, + [path.join(__dirname, 'index.js')], + { + cwd: path.join(__dirname, '..'), + env: { + ...process.env, + GITEA_SERVER_URL: '', + GITEA_REPOSITORY: 'owner/repo', + RUNNER_TOKEN: 'token', + IS_BETA: 'false', + }, + encoding: 'utf8', + }, + ); + + assert.equal(result.status, 1); + assert.match(result.stderr, /^\[參數檢查\]\[ERR\]\[\d{4}\/\d{2}\/\d{2} /); + assert.match(result.stderr, /GITEA_SERVER_URL 未設定/); +});