Compare commits

...

38 Commits

Author SHA1 Message Date
jiantw83 9ffdf2241a Merge pull request 'feat/ai_code_review' (#60) from feat/ai_code_review into develop
Reviewed-on: #60
2026-05-15 17:17:13 +00:00
AI Review Bot 9caf84432f chore: update ai-review findings [ai-review-bot][success] 2026-05-15 17:13:07 +00:00
jiantw83 74c544d4e4 chore: update review exclusions 2026-05-15 17:11:52 +00:00
AI Review Bot be954f4cb1 chore: update ai-review findings [ai-review-bot][success] 2026-05-15 17:08:18 +00:00
jiantw83 9e0253f74d chore: triage review findings 2026-05-15 17:07:00 +00:00
AI Review Bot fadcf9b14c chore: update ai-review findings [ai-review-bot][failure] 2026-05-15 16:57:14 +00:00
jiantw83 4bdde4f7ce test: harden version entrypoint 2026-05-15 16:55:04 +00:00
AI Review Bot 8cef794557 chore: update ai-review findings [ai-review-bot][failure] 2026-05-15 16:18:42 +00:00
jiantw83 6b7f257664 refactor: simplify version entrypoint output 2026-05-15 16:16:01 +00:00
jiantw83 7e181a8ab2 chore: adjust workflow release and review settings 2026-05-15 16:13:15 +00:00
jiantw83 43cddbd2c4 feat: update workflow names for clarity and add AI code review workflow 2026-05-13 03:28:58 +00:00
Jeffery d407899afb feat: 將參數輸出移到最後一個步驟 2026-03-26 14:51:28 +08:00
jiantw83 54cba9b783 更新 action.yaml 2026-03-25 06:40:59 +00:00
jiantw83 e6708957b0 更新 Dockerfile 2026-03-25 05:42:30 +00:00
jiantw83 4e2af3ffe5 更新 action.yaml 2026-03-25 02:46:44 +00:00
Jeffery 11c39bc75a test: 修正基本版本號的過濾方法 2026-03-24 18:34:55 +08:00
Jeffery 76006da109 Merge branch 'develop' of https://gitea.jsc.idv.tw/actions/calculate-version into develop 2026-03-24 18:17:22 +08:00
Jeffery 16d2afca48 feat: 加入 beta 版本計算 2026-03-24 18:17:18 +08:00
jiantw83 28aed39d80 Merge branch 'develop' of https://gitea.jsc.idv.tw/actions/calculate-version into develop 2026-03-21 19:47:28 +08:00
jiantw83 5c70769ed3 feat: 將清理成品的行為合併到發布專案 2026-03-21 19:47:23 +08:00
jiantw83 9a79e99f42 更新 .gitea/workflows/master.yaml 2026-03-21 07:21:41 +00:00
jiantw83 535e2fb2c2 feat: 加上 Gitea Runner Token 2026-03-21 10:58:38 +08:00
jiantw83 ea5f9e2399 feat: 清理成品 2026-03-21 10:53:09 +08:00
Jeffery 0ccffa8b1c feat: 變更工作流名稱 2026-03-20 18:14:16 +08:00
Jeffery bbf48129b4 fix: 修正工作流相依 2026-03-20 18:09:37 +08:00
Jeffery 3a04e691db feat: 重構 CD 工作流 2026-03-20 18:07:23 +08:00
Jeffery fb052a75c4 test: 使用不同的執行器 2026-03-20 18:03:42 +08:00
Jeffery 697311db2d feat: 移除無用的部分 2026-03-20 17:47:26 +08:00
Jeffery 559cb45312 feat: 改回檔案的方式繼續想怎麼處理 2026-03-20 17:38:00 +08:00
Jeffery 8cf6a41f3c test: 相信 AI 再試一次 GITHUB_OUTPUT 2026-03-20 17:31:29 +08:00
Jeffery b418abbbac feat: 修正工作流 2026-03-20 17:26:04 +08:00
Jeffery 94f0e99392 feat: 顯示另一個工作目錄 2026-03-20 17:22:49 +08:00
Jeffery e382432c7b feat: 顯示工作目錄的內容 2026-03-20 17:22:20 +08:00
Jeffery dfaa8530e1 feat: 從不同的工作目錄取得版本檔案 2026-03-20 17:20:48 +08:00
Jeffery 3a95e3e96b test: 從共用資料夾讀取版本檔案 2026-03-20 17:13:34 +08:00
Jeffery 6041e730fa feat: 更新版本號 2026-03-20 17:12:10 +08:00
Jeffery 20d104ab9b test: 將版本檔案放到共用資料夾 2026-03-20 17:11:41 +08:00
Jeffery f63edd5214 fix: 修正指令 2026-03-20 17:09:35 +08:00
17 changed files with 912 additions and 91 deletions
+14
View File
@@ -0,0 +1,14 @@
# Triage Findings
When the task is to triage review findings, follow this workflow:
1. Merge all findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1 after sorting.
5. Fix real issues with the smallest safe change.
6. Add false positives to `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check the issue after each fix.
Use the repo-local `triage-findings` skill for the same workflow when running in Codex.
+29
View File
@@ -0,0 +1,29 @@
---
name: triage-findings
description: Triage findings, fix real issues, and exclude false positives.
---
# Triage Findings
## Use
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
## Workflow
1. Merge all findings.
2. Sort by severity:
- critical
- warning
- info
3. Renumber from 1.
4. Fix real issues.
5. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
6. Add tests when behavior changes.
## Output Rules
- Keep the final list short.
- Keep numbering contiguous.
- Preserve file path, location, and fix.
- When writing exclusions, prefer the original issue text over paraphrased rewrites.
+45
View File
@@ -0,0 +1,45 @@
---
name: triage-findings
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
---
# Triage Findings
## When To Use
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
It is also used when some findings are false positives and should be moved into the exclusions list.
## Workflow
1. Collect all findings into one list.
2. Merge duplicates into a single finding when they describe the same issue.
3. Sort the final list by severity:
- critical
- warning
- info
4. Renumber the sorted list from 1 upward.
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
6. If a finding is a false positive, do not keep it in the final list.
7. Add false positives to the exclusions list using the existing schema in the repo or task context, and preserve the original finding wording as much as possible, including language and semantics.
## Resolution Flow
After the list is merged and ordered, resolve the remaining findings one by one.
1. Start from the highest severity item.
2. Identify the root cause in the relevant file or context.
3. Apply the smallest safe change that fixes the issue.
4. Add or update tests when behavior changes.
5. Re-check the issue after the change.
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
7. Continue until the list is either fixed or explicitly excluded.
## Output Rules
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
- Keep numbering contiguous after filtering and merging.
- Preserve useful details like file path, location, and suggested fix.
- Keep exclusions entries minimal and consistent with the project schema.
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
@@ -0,0 +1,4 @@
interface:
display_name: "Triage Findings"
short_description: "Triage, sort, fix, and exclude review findings"
default_prompt: "Use $triage-findings to merge review findings, sort and renumber them by severity, resolve real issues one by one, and add false positives to exclusions."
+29
View File
@@ -0,0 +1,29 @@
---
name: triage-findings
description: Triage findings, fix real issues, and exclude false positives.
---
# Triage Findings
## Use
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
## Workflow
1. Merge all findings.
2. Sort by severity:
- critical
- warning
- info
3. Renumber from 1.
4. Fix real issues.
5. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
6. Add tests when behavior changes.
## Output Rules
- Keep the final list short.
- Keep numbering contiguous.
- Preserve file path, location, and fix.
- When writing exclusions, prefer the original issue text over paraphrased rewrites.
+86
View File
@@ -0,0 +1,86 @@
[
{
"level": "critical",
"role": "Aria",
"location": "entrypoint.sh",
"suggestion": "檔案結尾缺少一個換行符號。根據 POSIX 規範,所有文字檔案都應以換行符號結束。請在檔案的最後一行後新增一個換行符號。"
},
{
"level": "critical",
"role": "Maya",
"location": "entrypoint.sh:86-91",
"suggestion": "版本號遞增邏輯中,當 `PATCH` 達到 `10` 時會遞增 `MINOR`,當 `MINOR` 達到 `10` 時會遞增 `MAJOR`。這與標準的語義化版本控制(Semantic Versioning)慣例不同(例如,通常 `1.9.9` 之後是 `1.9.10`,而非 `2.0.0`)。請釐清預期的版本控制方案。如果目標是標準語義版本,則應移除 `MINOR` 和 `MAJOR` 在 `10` 時的進位邏輯。如果這是一個自訂的十進位版本系統,請務必清楚文件化此行為,並新增特定測試案例,例如 `v0.0.9`、`v0.9.9`、`v0.9.0`、`v1.9.9`,以驗證進位行為是否符合預期。"
},
{
"level": "warning",
"role": "Rex",
"location": "entrypoint.sh:42, entrypoint.sh:43, entrypoint.sh:55",
"suggestion": "GITEA_SERVER_URL 和 GITEA_REPOSITORY 變數直接用於構建 URL。雖然在 CI/CD 環境中這些變數通常被視為受信任的輸入,但若這些輸入來源不可信,可能導致伺服器端請求偽造(SSRF)或路徑遍歷攻擊。建議對這些數據進行更嚴格的輸入驗證,例如檢查 URL 是否符合預期的格式和網域,並確保儲存庫名稱不包含惡意字元(如 `../` 或特殊編碼字元)。"
},
{
"level": "warning",
"role": "Aria",
"location": "entrypoint.sh:44",
"suggestion": "`require_env` 函數中的 `printf '%s=%s\n' \"$name\" \"$value\"` 會直接回顯變數數值。對於 `RUNNER_TOKEN` 這類敏感資訊,這可能導致安全風險。建議修改此函數,使其僅回顯變數名稱,或對敏感變數的值進行遮蔽處理。例如,可以改為 `info \"$name 已設定\"`,或在呼叫 `require_env` 之前處理敏感變數的輸出。"
},
{
"level": "info",
"role": "Rex",
"location": "entrypoint.sh:109",
"suggestion": "將 NEW_VERSION 輸出到 GITHUB_OUTPUT 時,雖然目前 NEW_VERSION 的格式(如 `X.Y.Z` 或 `X.Y.Z-beta.N`)相對安全,不太可能包含換行符或其他特殊字元,但一般而言,應確保輸出到環境變數或檔案的內容不包含可能導致指令注入的特殊字元。對於複雜或多行內容,應使用 GitHub Actions 建議的多行輸出語法以確保安全。"
},
{
"level": "critical",
"role": "Leo",
"location": "entrypoint.sh:98-142",
"suggestion": "在 `calculate_version` 函式中,`jq` 腳本的邏輯過於複雜且龐大。重新實作了 `latest_stable_version`、`next_release_version`、`next_beta_number` 等邏輯。Bash 與 jq 的跨語言邏輯重複,增加維護難度與錯誤風險。建議:將 `calculate_version` 改寫成純 Bash。使用外部 `jq` 篩選器而非 inline jq。若必須使用 jq,建議拆分成多個函式並使用:`read -r -d '' JQ_SCRIPT << 'EOF'` 以提高可讀性。"
},
{
"level": "critical",
"role": "Leo",
"location": "tests/entrypoint_test.sh:50-109",
"suggestion": "在 `make_mock_jq` 函式中,為了測試 `entrypoint.sh`,於 Python 中重新實作核心版本計算邏輯。測試程式重複了正式程式的 jq 腳本。若 `entrypoint.sh` 的版本計算邏輯變更,Mock 也需同步更新,容易造成錯誤。建議:簡化 `entrypoint.sh` 的 jq 邏輯(如前一建議)。Mock jq 僅根據輸入回傳預定義輸出,而非重新計算邏輯。"
},
{
"level": "warning",
"role": "Zara",
"location": "entrypoint.sh:65, entrypoint.sh:90",
"suggestion": "腳本多次對完整的 `RELEASE_JSON` 字串執行 `jq` 命令來提取不同資訊。儘管 `RELEASE_JSON` 已載入記憶體,但每次 `jq` 呼叫都會啟動新程序並重新解析字串。對於擁有大量發行版本的儲存庫,這可能導致顯著的效能開銷。建議考慮將這些 `jq` 操作合併單次處理,以一次性提取所有所需資料(最新的穩定版本和首個出的新版本最高 beta 編號),從而減少 CPU 週期和程序啟動的開銷。"
},
{
"level": "warning",
"role": "Zara",
"location": "entrypoint.sh:58",
"suggestion": "腳本從 Gitea API 獲取所有發行版本。如果儲存庫中的發行版本數量非常龐大,這可能會消耗大量的網路頻寬和記憶體。儘管目前的邏輯可能需要檢查多個發行版本(例如,尋找最新的穩定版或特定的 beta 版本),但值得研究 Gitea API 是否提供更精細的過濾(例如,按標籤名稱模式或排除 beta 標籤)或分頁功能,以減少獲取的資料量,特別是當腳本只需要部分發行版本資訊時。"
},
{
"level": "warning",
"role": "Leo",
"location": "entrypoint.sh:159",
"suggestion": "在 `main` 函式中,`curl -fsS` 會抑制進度與錯誤細節。雖然 `set -e` 會在 curl 失敗時終止,但 stderr 的輸出資訊不足。建議在 `fail` 區塊中提供更明確錯誤內容,或包裝 `curl -fsS -H ... \"$RELEASE_URL\"` 以便記錄更具體的失敗原因。"
},
{
"level": "warning",
"role": "Leo",
"location": "entrypoint.sh:110",
"suggestion": "`calculate_version` 中的 jq 腳本使用魔術數字 `10` 用於 patch >= 10 判斷。建議提取為具名常數,或改用 Bash `readonly` 常數。"
},
{
"level": "warning",
"role": "Maya",
"location": "tests/entrypoint_test.sh:209",
"suggestion": "`next_release_version` 函數的測試案例涵蓋了:patch、minor 版本進位,但缺少 major 版本進位測試。例如:`99.9.9 -> 100.0.0`。建議新增此邊界條件測試,以確保版本號計算完整性。"
},
{
"level": "warning",
"role": "Maya",
"location": "tests/entrypoint_test.sh:209",
"suggestion": "目前測試案例中,雖然 `run_entrypoint` 函數接受 `token` 參數,但沒有明確測試驗證 `RUNNER_TOKEN` 存在時,`curl` 是否正確使用授權標頭。建議新增測試案例驗證 `RUNNER_TOKEN` 行為。"
},
{
"level": "warning",
"role": "Maya",
"location": "tests/entrypoint_test.sh:209",
"suggestion": "測試腳本執行過程中會建立暫存檔案與目錄:`response_file`、`workdir`,但目前沒有明確清理機制。可能導致測試環境累積不必要檔案。建議在測試腳本開頭或結尾加入:`trap 'rm -rf \"$workdir\" \"$response_file\"' EXIT`,以確保測試完成後所有暫存資源皆被妥善移除。"
}
]
+1
View File
@@ -0,0 +1 @@
[]
+17 -27
View File
@@ -1,38 +1,28 @@
name: CD
on:
push:
branches:
- master
jobs:
cd:
name: "CD > 發布專案"
runs-on: docker
steps:
- name: 發布專案
uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }}
with:
tag_name: "v0.1.8"
- name: 清理舊版本 (保留最新2個)
uses: https://gitea.jsc.idv.tw/actions/cleanup-release@${{ vars.ACTION_CLEANUP_RELEASE_VERSION }}
with:
gitea-server: ${{ gitea.server_url }}
repository: ${{ gitea.repository }}
token: ${{ secrets.GITEA_TOKEN }}
test-calculate:
name: "TEST > 計算版本號"
runs-on: docker
needs: cd
version:
name: 計算版本號
runs-on: ubuntu
outputs:
version: ${{ steps.version.outputs.version }}
steps:
- name: 計算版本號
uses: https://gitea.jsc.idv.tw/actions/calculate-version@v0.1.8
- name: 輸出版本號
id: version
run: echo VERSION.txt >> $GITHUB_OUTPUT
test:
name: "TEST > 測試專案"
runs-on: docker
needs: test-calculate
uses: https://gitea.jsc.idv.tw/actions/calculate-version@${{ vars.ACTION_CALCULATE_VERSION }}
release:
name: 發布專案
runs-on: ubuntu
needs: version
steps:
- name: 顯示版本號
run: echo ${{ needs.test-calculate.outputs.version }}
- name: 發布專案
uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }}
with:
tag_name: v${{ needs.version.outputs.version }}
- name: 清理成品
uses: https://gitea.jsc.idv.tw/actions/cleanup-release@${{ vars.ACTION_CLEANUP_RELEASE_VERSION }}
with:
RUNNER_TOKEN: ${{ secrets.RUNNER_TOKEN }}
+23
View File
@@ -0,0 +1,23 @@
name: AI
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
code-review:
name: Code Review
runs-on: ubuntu
steps:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }},${{ secrets.GEMINI_API_KEY_1 }},${{ secrets.GEMINI_API_KEY_2 }},${{ secrets.GEMINI_API_KEY_3 }},${{ secrets.GEMINI_API_KEY_4 }},${{ secrets.GEMINI_API_KEY_5 }},${{ secrets.GEMINI_API_KEY_6 }},${{ secrets.GEMINI_API_KEY_7 }},${{ secrets.GEMINI_API_KEY_8 }},${{ secrets.GEMINI_API_KEY_9 }},${{ secrets.GEMINI_API_KEY_10 }},${{ secrets.GEMINI_API_KEY_11 }},${{ secrets.GEMINI_API_KEY_12 }},${{ secrets.GEMINI_API_KEY_13 }},${{ secrets.GEMINI_API_KEY_14 }},${{ secrets.GEMINI_API_KEY_15 }},${{ secrets.GEMINI_API_KEY_16 }},${{ secrets.GEMINI_API_KEY_17 }},${{ secrets.GEMINI_API_KEY_18 }},${{ secrets.GEMINI_API_KEY_19 }}
GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
permissions:
contents: write
pull-requests: write
issues: write
+14
View File
@@ -0,0 +1,14 @@
# Triage Findings
Use the triage-finding workflow for review issue lists:
1. Merge findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1.
5. Fix real issues with the smallest safe change.
6. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check after each fix.
The full reusable skill lives in `.claude/skills/triage-findings/SKILL.md`.
+14
View File
@@ -0,0 +1,14 @@
# Triage Findings
Use the triage-finding workflow for review issue lists:
1. Merge findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1.
5. Fix real issues with the smallest safe change.
6. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check after each fix.
The reusable skill lives in `.gemini/skills/triage-findings/SKILL.md`.
+16
View File
@@ -0,0 +1,16 @@
# Triage Findings
When the task is to triage review findings, follow this workflow:
1. Merge all findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1 after sorting.
5. Fix real issues with the smallest safe change.
6. Add false positives to `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check the issue after each fix.
Use the repo-local `triage-findings` skill for the same workflow when running in Codex.
Trigger it with `/triage-findings`.
+2 -3
View File
@@ -1,8 +1,7 @@
FROM alpine:latest
# 更新並安裝必要的工具
RUN apk update \
&& apk add --no-cache bash curl jq
# 安裝必要的工具
RUN apk add --no-cache --no-check-certificate bash curl jq
COPY entrypoint.sh /entrypoint.sh
+14
View File
@@ -0,0 +1,14 @@
# Triage Findings
Use the triage-finding workflow for review issue lists:
1. Merge findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1.
5. Fix real issues with the smallest safe change.
6. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check after each fix.
The reusable skill lives in `.gemini/skills/triage-findings/SKILL.md`.
+9 -1
View File
@@ -1,10 +1,18 @@
name: 'CALCULATE VERSION'
description: '計算版本號'
author: 'Jeffery'
inputs:
IS_BETA:
description: '是否為 beta 版本'
default: "false"
outputs:
version:
description: '計算出的版本號'
runs:
using: 'docker'
image: 'Dockerfile'
env:
GITEA_SERVER_URL: ${{ gitea.server_url }}
GITEA_REPOSITORY: ${{ gitea.repository }}
RUNNER_TOKEN: ${{ secrets.RUNNER_TOKEN }}
RUNNER_TOKEN: ${{ inputs.RUNNER_TOKEN || secrets.GITEA_TOKEN || secrets.RUNNER_TOKEN }}
IS_BETA: ${{ inputs.IS_BETA }}
+196 -60
View File
@@ -1,74 +1,210 @@
#!/bin/bash
echo "=================================================="
set -euo pipefail
echo "參數檢查"
readonly LINE="=================================================="
readonly SUBLINE="--------------------------------------------------"
echo "--------------------------------------------------"
section() {
printf '\n%s\n%s\n%s\n' "$LINE" "$1" "$SUBLINE"
}
# 顯示 GITEA_SERVER_URL 參數,並檢查是否為空或 "null",如果是則輸出錯誤訊息並退出
echo "GITEA_SERVER_URL=$GITEA_SERVER_URL" && ([ -z "$GITEA_SERVER_URL" ] || [ "$GITEA_SERVER_URL" = "null" ]) && exit 1
info() {
printf '[info] %s\n' "$1"
}
# 顯示 GITEA_REPOSITORY 參數,並檢查是否為空或 "null",如果是則輸出錯誤訊息並退出
echo "GITEA_REPOSITORY=$GITEA_REPOSITORY" && ([ -z "$GITEA_REPOSITORY" ] || [ "$GITEA_REPOSITORY" = "null" ]) && exit 1
fail() {
printf '[error] %s\n' "$1" >&2
exit 1
}
# 顯示 RUNNER_TOKEN 參數
echo "RUNNER_TOKEN=$RUNNER_TOKEN"
require_env() {
local name="$1"
local value="$2"
echo "=================================================="
if [ -z "$value" ] || [ "$value" = "null" ]; then
fail "$name 未設定"
fi
echo "取得最新的版本號"
printf '%s=%s\n' "$name" "$value"
}
echo "--------------------------------------------------"
write_output() {
printf 'version=%s\n' "$1" >> "$GITHUB_OUTPUT"
}
# 組合 RELEASE_URL 參數,並顯示出來
RELEASE_URL="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases" && echo "RELEASE_URL=$RELEASE_URL"
normalize_beta_flag() {
local value="${1:-false}"
if [ -n "$RUNNER_TOKEN" ] && [ "$RUNNER_TOKEN" != "null" ]; then
# 組合 RELEASE_HEADER 參數,並顯示出來
RELEASE_HEADER="Authorization: token $RUNNER_TOKEN" && echo "RELEASE_HEADER=$RELEASE_HEADER"
if [ -z "$value" ] || [ "$value" = "null" ]; then
printf '%s\n' "false"
return
fi
# 取得成品資訊
RELEASE_JSON="$(curl -s -H "$RELEASE_HEADER" "$RELEASE_URL")"
else
# 取得成品資訊
RELEASE_JSON="$(curl -s "$RELEASE_URL")"
printf '%s\n' "$value"
}
latest_stable_version() {
local release_json="$1"
if [ -z "$release_json" ] || [ "$release_json" = "null" ]; then
printf '%s\n' "0.0.0"
return
fi
printf '%s' "$release_json" | jq -r '
if type == "array" then
[ .[] | select(.tag_name? and (.tag_name | test("-beta\\.") | not)) | .tag_name ][0]
// "v0.0.0"
else
"v0.0.0"
end
| sub("^v"; "")
'
}
next_release_version() {
local latest_version="$1"
local major minor patch
IFS='.' read -r major minor patch <<< "$latest_version"
major="${major:-0}"
minor="${minor:-0}"
patch="${patch:-0}"
patch=$((patch + 1))
if [ "$patch" -ge 10 ]; then
patch=0
minor=$((minor + 1))
fi
if [ "$minor" -ge 10 ]; then
minor=0
major=$((major + 1))
fi
printf '%s.%s.%s\n' "$major" "$minor" "$patch"
}
next_beta_number() {
local release_json="$1"
local version="$2"
if [ -z "$release_json" ] || [ "$release_json" = "null" ]; then
printf '%s\n' "1"
return
fi
printf '%s' "$release_json" | jq -r --arg prefix "v${version}-beta." '
if type == "array" then
[ .[]
| select(.tag_name? and (.tag_name | startswith($prefix)))
| .tag_name
| ltrimstr($prefix)
| try tonumber catch empty
] | if length > 0 then max else 0 end
else
0
end
' | awk '{print $1 + 1}'
}
calculate_version() {
local release_json="$1"
local is_beta="$2"
if [ -z "$release_json" ] || [ "$release_json" = "null" ]; then
if [ "$is_beta" = "true" ]; then
printf '%s\t%s\n' "0.0.0" "0.0.1-beta.1"
else
printf '%s\t%s\n' "0.0.0" "0.0.1"
fi
return
fi
printf '%s' "$release_json" | jq -r --arg is_beta "$is_beta" '
def stable_version:
if type == "array" then
[ .[] | select(.tag_name? and (.tag_name | test("-beta\\.") | not)) | .tag_name ][0]
// "v0.0.0"
else
"v0.0.0"
end
| sub("^v"; "");
def next_release($latest):
($latest | split(".") | map(tonumber? // 0)) as $parts
| ($parts[0] // 0) as $major
| ($parts[1] // 0) as $minor
| ($parts[2] // 0) as $patch
| ($patch + 1) as $next_patch
| if $next_patch >= 10 then
if ($minor + 1) >= 10 then
"\(($major + 1)).0.0"
else
"\($major).\($minor + 1).0"
end
else
"\($major).\($minor).\($next_patch)"
end;
def beta_max($prefix):
[ .[]
| select(.tag_name? and (.tag_name | startswith($prefix)))
| .tag_name
| ltrimstr($prefix)
| try tonumber catch empty
] | if length > 0 then max else 0 end;
(stable_version) as $base
| (next_release($base)) as $next
| if $is_beta == "true" then
$base + "\t" + ($next + "-beta." + ((beta_max("v" + $next + "-beta.") + 1) | tostring))
else
$base + "\t" + $next
end
'
}
main() {
section "參數檢查"
require_env "GITEA_SERVER_URL" "${GITEA_SERVER_URL:-}"
require_env "GITEA_REPOSITORY" "${GITEA_REPOSITORY:-}"
if [ -n "${RUNNER_TOKEN:-}" ] && [ "${RUNNER_TOKEN:-}" != "null" ]; then
info "RUNNER_TOKEN=***"
else
info "RUNNER_TOKEN=未提供"
fi
IS_BETA="$(normalize_beta_flag "${IS_BETA:-false}")"
info "IS_BETA=$IS_BETA"
section "取得版本資料"
RELEASE_URL="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases"
info "RELEASE_URL=$RELEASE_URL"
if [ -n "${RUNNER_TOKEN:-}" ] && [ "${RUNNER_TOKEN:-}" != "null" ]; then
info "使用授權 token 取得 release"
RELEASE_JSON="$(curl -fsS -H "Authorization: token $RUNNER_TOKEN" "$RELEASE_URL")"
else
info "使用匿名請求取得 release"
RELEASE_JSON="$(curl -fsS "$RELEASE_URL")"
fi
VERSION_INFO="$(calculate_version "$RELEASE_JSON" "$IS_BETA")"
IFS=$'\t' read -r LATEST_TAG NEW_VERSION <<< "$VERSION_INFO"
info "LATEST_VERSION=$LATEST_TAG"
section "計算版本號"
info "NEW_VERSION=$NEW_VERSION"
write_output "$NEW_VERSION"
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
main "$@"
fi
# 從成品資訊取得最新的版本號
LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r 'if length > 0 then .[0].tag_name else "v0.0.0" end' | sed 's/^v//')
# 並檢查是否為空或 "null" 後
([ -z "$LATEST_VERSION" ] || [ "$LATEST_VERSION" = "null" ]) && LATEST_VERSION="0.0.0"
# 顯示 LATEST_VERSION 參數
echo "LATEST_VERSION=$LATEST_VERSION"
echo "=================================================="
echo "計算版本號"
echo "--------------------------------------------------"
# 分解版本號
IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_VERSION"
# 確保變數為數字
MAJOR=${MAJOR:-0}
MINOR=${MINOR:-0}
PATCH=${PATCH:-0}
# 增加 PATCH 版本
PATCH=$((PATCH + 1))
# 如果 PATCH >= 10,重置為 0 並增加 MINOR 版本
[ $PATCH -ge 10 ] && { PATCH=0; MINOR=$((MINOR + 1)); }
# 如果 MINOR >= 10,重置為 0 並增加 MAJOR 版本
[ $MINOR -ge 10 ] && { MINOR=0; MAJOR=$((MAJOR + 1)); }
# 組合新的版本號,並顯示
NEW_VERSION="$MAJOR.$MINOR.$PATCH" && echo "NEW_VERSION=$NEW_VERSION"
# 將版本輸出到環境變數
echo "version=$NEW_VERSION" > VERSION.txt
+399
View File
@@ -0,0 +1,399 @@
#!/bin/bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT_DIR/entrypoint.sh"
declare -a CLEANUP_PATHS=()
cleanup() {
if [ "${#CLEANUP_PATHS[@]}" -gt 0 ]; then
rm -rf "${CLEANUP_PATHS[@]}"
fi
}
trap cleanup EXIT
fail() {
printf '[error] %s\n' "$1" >&2
exit 1
}
assert_eq() {
local expected="$1"
local actual="$2"
local label="$3"
if [ "$expected" != "$actual" ]; then
fail "$label: expected '$expected', got '$actual'"
fi
}
make_mock_curl() {
local bin_dir="$1"
local response_file="$2"
cat >"$bin_dir/curl" <<'EOF'
#!/bin/sh
if [ -n "${FAKE_CURL_LOG_FILE:-}" ]; then
printf '%s\n' "$*" >> "$FAKE_CURL_LOG_FILE"
fi
if [ "${FAKE_CURL_STATUS:-0}" != "0" ]; then
exit "$FAKE_CURL_STATUS"
fi
cat "${FAKE_CURL_RESPONSE_FILE:?}"
EOF
chmod +x "$bin_dir/curl"
}
make_mock_jq() {
local bin_dir="$1"
cat >"$bin_dir/jq" <<'EOF'
#!/bin/sh
is_beta=""
query=""
while [ "$#" -gt 0 ]; do
case "$1" in
-r)
shift
;;
--arg)
if [ "$2" = "is_beta" ]; then
is_beta="$3"
fi
shift 3
;;
*)
query="$1"
shift
break
;;
esac
done
python3 -c 'import json, sys
query = sys.argv[1]
is_beta = sys.argv[2]
payload = sys.stdin.read()
try:
data = json.loads(payload)
except Exception:
sys.exit(4)
def next_version(latest):
parts = [int(p or 0) for p in latest.split(".")]
while len(parts) < 3:
parts.append(0)
major, minor, patch = parts[:3]
patch += 1
if patch >= 10:
patch = 0
minor += 1
if minor >= 10:
minor = 0
major += 1
return f"{major}.{minor}.{patch}"
def beta_max(data, prefix):
values = []
for item in data:
if not isinstance(item, dict):
continue
tag = item.get("tag_name")
if isinstance(tag, str) and tag.startswith(prefix):
suffix = tag[len(prefix):]
try:
values.append(int(suffix))
except Exception:
pass
return max(values) if values else 0
if not isinstance(data, list):
base = "0.0.0"
else:
base = "0.0.0"
for item in data:
if not isinstance(item, dict):
continue
tag = item.get("tag_name")
if isinstance(tag, str) and "-beta." not in tag:
base = tag[1:] if tag.startswith("v") else tag
break
next_ver = next_version(base)
if is_beta == "true":
beta = beta_max(data, f"v{next_ver}-beta.") + 1
sys.stdout.write(f"{base}\t{next_ver}-beta.{beta}")
else:
sys.stdout.write(f"{base}\t{next_ver}")
' "$query" "$is_beta"
EOF
chmod +x "$bin_dir/jq"
}
run_entrypoint() {
local response_file="$1"
local is_beta="$2"
local token="${3:-}"
local fake_status="${4:-0}"
local workdir
local output_file
local bin_dir
local stdout_file
local stderr_file
workdir="$(mktemp -d)"
CLEANUP_PATHS+=("$workdir")
bin_dir="$workdir/bin"
mkdir -p "$bin_dir"
make_mock_curl "$bin_dir" "$response_file"
make_mock_jq "$bin_dir"
output_file="$workdir/github_output"
stdout_file="$workdir/stdout"
stderr_file="$workdir/stderr"
if [ -n "$token" ]; then
FAKE_CURL_STATUS="$fake_status" \
FAKE_CURL_RESPONSE_FILE="$response_file" \
FAKE_CURL_LOG_FILE="$workdir/curl_args" \
PATH="$bin_dir:$PATH" \
GITEA_SERVER_URL="https://gitea.example.com" \
GITEA_REPOSITORY="org/repo" \
RUNNER_TOKEN="$token" \
IS_BETA="$is_beta" \
GITHUB_OUTPUT="$output_file" \
bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file"
else
FAKE_CURL_STATUS="$fake_status" \
FAKE_CURL_RESPONSE_FILE="$response_file" \
FAKE_CURL_LOG_FILE="$workdir/curl_args" \
PATH="$bin_dir:$PATH" \
GITEA_SERVER_URL="https://gitea.example.com" \
GITEA_REPOSITORY="org/repo" \
IS_BETA="$is_beta" \
GITHUB_OUTPUT="$output_file" \
bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file"
fi
printf '%s\n' "$output_file"
}
test_unit_helpers() {
assert_eq "false" "$(normalize_beta_flag "")" "normalize_beta_flag empty"
assert_eq "false" "$(normalize_beta_flag "null")" "normalize_beta_flag null"
assert_eq "true" "$(normalize_beta_flag "true")" "normalize_beta_flag true"
assert_eq "0.1.0" "$(next_release_version "0.0.9")" "next_release_version carry patch"
assert_eq "2.0.0" "$(next_release_version "1.9.9")" "next_release_version carry minor"
assert_eq "100.0.0" "$(next_release_version "99.9.9")" "next_release_version carry major"
}
test_stable_release_flow() {
local workdir
local response_file
local output_file
workdir="$(mktemp -d)"
CLEANUP_PATHS+=("$workdir")
response_file="$workdir/releases.json"
cat >"$response_file" <<'EOF'
[
{"tag_name":"v1.2.3-beta.1"},
{"tag_name":"v1.2.3"},
{"tag_name":"v1.2.4-beta.1"}
]
EOF
output_file="$(run_entrypoint "$response_file" "false")"
assert_eq "version=1.2.4" "$(cat "$output_file")" "stable release output"
}
test_beta_release_flow() {
local workdir
local response_file
local output_file
workdir="$(mktemp -d)"
CLEANUP_PATHS+=("$workdir")
response_file="$workdir/releases.json"
cat >"$response_file" <<'EOF'
[
{"tag_name":"v1.2.3"},
{"tag_name":"v1.2.4-beta.1"},
{"tag_name":"v1.2.4-beta.3"}
]
EOF
output_file="$(run_entrypoint "$response_file" "true")"
assert_eq "version=1.2.4-beta.4" "$(cat "$output_file")" "beta release output"
}
test_empty_release_list() {
local workdir
local response_file
local output_file
workdir="$(mktemp -d)"
CLEANUP_PATHS+=("$workdir")
response_file="$workdir/releases.json"
printf '%s\n' '[]' >"$response_file"
output_file="$(run_entrypoint "$response_file" "false")"
assert_eq "version=0.0.1" "$(cat "$output_file")" "empty release list"
}
test_only_beta_releases() {
local workdir
local response_file
local output_file
workdir="$(mktemp -d)"
CLEANUP_PATHS+=("$workdir")
response_file="$workdir/releases.json"
cat >"$response_file" <<'EOF'
[
{"tag_name":"v2.4.6-beta.1"},
{"tag_name":"v2.4.6-beta.2"}
]
EOF
output_file="$(run_entrypoint "$response_file" "false")"
assert_eq "version=0.0.1" "$(cat "$output_file")" "only beta releases"
}
test_null_release_payload() {
local workdir
local response_file
local output_file
workdir="$(mktemp -d)"
CLEANUP_PATHS+=("$workdir")
response_file="$workdir/releases.json"
printf '%s\n' 'null' >"$response_file"
output_file="$(run_entrypoint "$response_file" "false")"
assert_eq "version=0.0.1" "$(cat "$output_file")" "null release payload"
}
test_token_auth_header() {
local workdir
local response_file
local bin_dir
local output_file
local curl_args
local stdout_file
local stderr_file
workdir="$(mktemp -d)"
CLEANUP_PATHS+=("$workdir")
response_file="$workdir/releases.json"
printf '%s\n' '[]' >"$response_file"
bin_dir="$workdir/bin"
mkdir -p "$bin_dir"
make_mock_curl "$bin_dir" "$response_file"
make_mock_jq "$bin_dir"
output_file="$workdir/github_output"
stdout_file="$workdir/stdout"
stderr_file="$workdir/stderr"
FAKE_CURL_STATUS=0 \
FAKE_CURL_RESPONSE_FILE="$response_file" \
FAKE_CURL_LOG_FILE="$workdir/curl_args" \
PATH="$bin_dir:$PATH" \
GITEA_SERVER_URL="https://gitea.example.com" \
GITEA_REPOSITORY="org/repo" \
RUNNER_TOKEN="secret-token" \
IS_BETA="false" \
GITHUB_OUTPUT="$output_file" \
bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file"
curl_args="$workdir/curl_args"
if ! grep -q 'Authorization: token secret-token' "$curl_args"; then
fail "token auth header: missing Authorization header"
fi
assert_eq "version=0.0.1" "$(cat "$output_file")" "token auth output"
}
test_malformed_release_payload() {
local workdir
local response_file
local bin_dir
local output_file
local stdout_file
local stderr_file
workdir="$(mktemp -d)"
CLEANUP_PATHS+=("$workdir")
response_file="$workdir/releases.json"
printf '%s\n' '{' >"$response_file"
bin_dir="$workdir/bin"
mkdir -p "$bin_dir"
make_mock_curl "$bin_dir" "$response_file"
make_mock_jq "$bin_dir"
output_file="$workdir/github_output"
stdout_file="$workdir/stdout"
stderr_file="$workdir/stderr"
if FAKE_CURL_STATUS=0 \
FAKE_CURL_RESPONSE_FILE="$response_file" \
PATH="$bin_dir:$PATH" \
GITEA_SERVER_URL="https://gitea.example.com" \
GITEA_REPOSITORY="org/repo" \
IS_BETA="false" \
GITHUB_OUTPUT="$output_file" \
bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file"; then
fail "malformed release payload: expected failure"
fi
}
test_curl_failure() {
local workdir
local response_file
local bin_dir
local output_file
local stdout_file
local stderr_file
workdir="$(mktemp -d)"
CLEANUP_PATHS+=("$workdir")
response_file="$workdir/releases.json"
printf '%s\n' '[]' >"$response_file"
bin_dir="$workdir/bin"
mkdir -p "$bin_dir"
make_mock_curl "$bin_dir" "$response_file"
output_file="$workdir/github_output"
stdout_file="$workdir/stdout"
stderr_file="$workdir/stderr"
if FAKE_CURL_STATUS=22 \
FAKE_CURL_RESPONSE_FILE="$response_file" \
PATH="$bin_dir:$PATH" \
GITEA_SERVER_URL="https://gitea.example.com" \
GITEA_REPOSITORY="org/repo" \
IS_BETA="false" \
GITHUB_OUTPUT="$output_file" \
bash "$ROOT_DIR/entrypoint.sh" >"$stdout_file" 2>"$stderr_file"; then
fail "curl failure: expected failure"
fi
}
test_unit_helpers
test_stable_release_flow
test_beta_release_flow
test_empty_release_list
test_only_beta_releases
test_null_release_payload
test_token_auth_header
test_malformed_release_payload
test_curl_failure
printf '[info] all tests passed\n'