Merge pull request '版本 0.3.6' (#61) from develop into master

Reviewed-on: #61
This commit is contained in:
2026-05-15 17:20:37 +00:00
15 changed files with 865 additions and 82 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 @@
[]
+1 -1
View File
@@ -21,7 +21,7 @@ jobs:
- name: 發布專案
uses: akkuman/gitea-release-action@${{ vars.ACTION_RELEASE_VERSION }}
with:
tag_name: "v${{ needs.version.outputs.version }}"
tag_name: v${{ needs.version.outputs.version }}
- name: 清理成品
uses: https://gitea.jsc.idv.tw/actions/cleanup-release@${{ vars.ACTION_CLEANUP_RELEASE_VERSION }}
with:
+4
View File
@@ -1,6 +1,8 @@
name: AI
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
code-review:
@@ -10,6 +12,8 @@ jobs:
- 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 }}
+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`.
+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`.
+195 -81
View File
@@ -1,96 +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"
# 顯示 IS_BETA 參數,未設定時預設為 false
echo "IS_BETA=$IS_BETA" && ([ -z "$IS_BETA" ] || [ "$IS_BETA" = "null" ]) && IS_BETA="false"
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"
}
echo "--------------------------------------------------"
normalize_beta_flag() {
local value="${1:-false}"
# 組合 RELEASE_URL 參數,並顯示出來
RELEASE_URL="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases" && echo "RELEASE_URL=$RELEASE_URL"
if [ -z "$value" ] || [ "$value" = "null" ]; then
printf '%s\n' "false"
return
fi
# 檢查是否為空或 "null"
if [ -n "$RUNNER_TOKEN" ] && [ "$RUNNER_TOKEN" != "null" ]; then
# 組合 RELEASE_HEADER 參數,並顯示出來
RELEASE_HEADER="Authorization: token $RUNNER_TOKEN" && echo "RELEASE_HEADER=$RELEASE_HEADER"
printf '%s\n' "$value"
}
# 取得成品資訊
RELEASE_JSON="$(curl -s -H "$RELEASE_HEADER" "$RELEASE_URL")"
else
# 取得成品資訊
RELEASE_JSON="$(curl -s "$RELEASE_URL")"
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
# 從成品資訊取得最新的正式版版本號(排除 beta,去除 v 前綴)
LATEST_VERSION=$(echo "$RELEASE_JSON" | jq -r '[.[] | select(.tag_name | test("-beta\\.") | not)] | 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"
# 如果是 beta 版本,計算 beta 編號
if [ "$IS_BETA" = "true" ]; then
# 從所有 release 中找出符合 NEW_VERSION-beta.N 的最大 beta 編號
BETA=$(echo "$RELEASE_JSON" | jq -r --arg prefix "v$NEW_VERSION-beta." \
'[.[] | select(.tag_name | startswith($prefix)) | .tag_name | ltrimstr($prefix) | tonumber] | if length > 0 then max else 0 end')
BETA=$((BETA + 1))
NEW_VERSION="$NEW_VERSION-beta.$BETA" && echo "NEW_VERSION=$NEW_VERSION"
# 將版本輸出到環境變數
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT && exit 0
fi
echo "NEW_VERSION=$NEW_VERSION"
echo "=================================================="
# 將版本輸出到環境變數
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
+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'