From 7e181a8ab22fa1e3cd348a2dbafef7bc7626773d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 15 May 2026 16:13:15 +0000 Subject: [PATCH 1/9] chore: adjust workflow release and review settings --- .gitea/workflows/master.yaml | 2 +- .gitea/workflows/review.yaml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitea/workflows/master.yaml b/.gitea/workflows/master.yaml index 1be7a87..b37b091 100644 --- a/.gitea/workflows/master.yaml +++ b/.gitea/workflows/master.yaml @@ -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: diff --git a/.gitea/workflows/review.yaml b/.gitea/workflows/review.yaml index a24c7be..34ce86b 100644 --- a/.gitea/workflows/review.yaml +++ b/.gitea/workflows/review.yaml @@ -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 }} From 6b7f257664b590dc49a2fceab6a1ccbb992d764c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 15 May 2026 16:16:01 +0000 Subject: [PATCH 2/9] refactor: simplify version entrypoint output --- entrypoint.sh | 139 +++++++++++++++++++++++++++----------------------- 1 file changed, 76 insertions(+), 63 deletions(-) diff --git a/entrypoint.sh b/entrypoint.sh index 9e71d87..7ee7e92 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,96 +1,109 @@ #!/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 "--------------------------------------------------" +section "參數檢查" -# 組合 RELEASE_URL 參數,並顯示出來 -RELEASE_URL="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases" && echo "RELEASE_URL=$RELEASE_URL" +require_env "GITEA_SERVER_URL" "${GITEA_SERVER_URL:-}" +require_env "GITEA_REPOSITORY" "${GITEA_REPOSITORY:-}" -# 檢查是否為空或 "null" -if [ -n "$RUNNER_TOKEN" ] && [ "$RUNNER_TOKEN" != "null" ]; then - # 組合 RELEASE_HEADER 參數,並顯示出來 - RELEASE_HEADER="Authorization: token $RUNNER_TOKEN" && echo "RELEASE_HEADER=$RELEASE_HEADER" - - # 取得成品資訊 - RELEASE_JSON="$(curl -s -H "$RELEASE_HEADER" "$RELEASE_URL")" +if [ -n "${RUNNER_TOKEN:-}" ] && [ "${RUNNER_TOKEN:-}" != "null" ]; then + printf 'RUNNER_TOKEN=%s\n' '***' else - # 取得成品資訊 - RELEASE_JSON="$(curl -s "$RELEASE_URL")" + printf 'RUNNER_TOKEN=%s\n' '未提供' 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//') +IS_BETA="${IS_BETA:-false}" +if [ "$IS_BETA" = "null" ] || [ -z "$IS_BETA" ]; then + IS_BETA="false" +fi +printf 'IS_BETA=%s\n' "$IS_BETA" -# 並檢查是否為空或 "null" 後 -([ -z "$LATEST_VERSION" ] || [ "$LATEST_VERSION" = "null" ]) && LATEST_VERSION="0.0.0" +section "取得版本資料" -# 顯示 LATEST_VERSION 參數 -echo "LATEST_VERSION=$LATEST_VERSION" +RELEASE_URL="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/releases" +info "RELEASE_URL=$RELEASE_URL" -echo "==================================================" +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 -echo "計算版本號" +LATEST_VERSION="$( + printf '%s' "$RELEASE_JSON" | jq -r ' + [ .[] | select(.tag_name | test("-beta\\.") | not) | .tag_name ][0] // "v0.0.0" + ' | sed 's/^v//' +)" -echo "--------------------------------------------------" +if [ -z "$LATEST_VERSION" ] || [ "$LATEST_VERSION" = "null" ]; then + LATEST_VERSION="0.0.0" +fi + +info "LATEST_VERSION=$LATEST_VERSION" + +section "計算版本號" -# 分解版本號 IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_VERSION" +MAJOR="${MAJOR:-0}" +MINOR="${MINOR:-0}" +PATCH="${PATCH:-0}" -# 確保變數為數字 -MAJOR=${MAJOR:-0} -MINOR=${MINOR:-0} -PATCH=${PATCH:-0} - -# 增加 PATCH 版本 PATCH=$((PATCH + 1)) +if [ "$PATCH" -ge 10 ]; then + PATCH=0 + MINOR=$((MINOR + 1)) +fi -# 如果 PATCH >= 10,重置為 0 並增加 MINOR 版本 -[ $PATCH -ge 10 ] && { PATCH=0; MINOR=$((MINOR + 1)); } +if [ "$MINOR" -ge 10 ]; then + MINOR=0 + MAJOR=$((MAJOR + 1)) +fi -# 如果 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="$( + printf '%s' "$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 + NEW_VERSION="$NEW_VERSION-beta.$BETA" fi -echo "NEW_VERSION=$NEW_VERSION" - -echo "==================================================" - -# 將版本輸出到環境變數 -echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT \ No newline at end of file +info "NEW_VERSION=$NEW_VERSION" +write_output "$NEW_VERSION" From 8cef79455739ff98b5f79d143a930adbc963f751 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Fri, 15 May 2026 16:18:42 +0000 Subject: [PATCH 3/9] chore: update ai-review findings [ai-review-bot][failure] --- .amazonq/rules/triage-findings.md | 14 +++ .claude/skills/triage-findings/SKILL.md | 29 +++++ .codex/skills/triage-findings/SKILL.md | 45 ++++++++ .../skills/triage-findings/agents/openai.yaml | 4 + .gemini/skills/triage-findings/SKILL.md | 29 +++++ .gitea/ai-review/findings.json | 100 ++++++++++++++++++ .github/copilot-instructions.md | 14 +++ .github/skills/triage-findings/SKILL.md | 14 +++ CLAUDE.md | 16 +++ GEMINI.md | 14 +++ 10 files changed, 279 insertions(+) create mode 100644 .amazonq/rules/triage-findings.md create mode 100644 .claude/skills/triage-findings/SKILL.md create mode 100644 .codex/skills/triage-findings/SKILL.md create mode 100644 .codex/skills/triage-findings/agents/openai.yaml create mode 100644 .gemini/skills/triage-findings/SKILL.md create mode 100644 .gitea/ai-review/findings.json create mode 100644 .github/copilot-instructions.md create mode 100644 .github/skills/triage-findings/SKILL.md create mode 100644 CLAUDE.md create mode 100644 GEMINI.md diff --git a/.amazonq/rules/triage-findings.md b/.amazonq/rules/triage-findings.md new file mode 100644 index 0000000..4b65ce1 --- /dev/null +++ b/.amazonq/rules/triage-findings.md @@ -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. diff --git a/.claude/skills/triage-findings/SKILL.md b/.claude/skills/triage-findings/SKILL.md new file mode 100644 index 0000000..c4ab450 --- /dev/null +++ b/.claude/skills/triage-findings/SKILL.md @@ -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. diff --git a/.codex/skills/triage-findings/SKILL.md b/.codex/skills/triage-findings/SKILL.md new file mode 100644 index 0000000..21476cb --- /dev/null +++ b/.codex/skills/triage-findings/SKILL.md @@ -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. diff --git a/.codex/skills/triage-findings/agents/openai.yaml b/.codex/skills/triage-findings/agents/openai.yaml new file mode 100644 index 0000000..6f59e2c --- /dev/null +++ b/.codex/skills/triage-findings/agents/openai.yaml @@ -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." diff --git a/.gemini/skills/triage-findings/SKILL.md b/.gemini/skills/triage-findings/SKILL.md new file mode 100644 index 0000000..c4ab450 --- /dev/null +++ b/.gemini/skills/triage-findings/SKILL.md @@ -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. diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json new file mode 100644 index 0000000..88de3f8 --- /dev/null +++ b/.gitea/ai-review/findings.json @@ -0,0 +1,100 @@ +[ + { + "level": "critical", + "role": "Aria", + "location": "entrypoint.sh", + "suggestion": "檔案結尾缺少一個換行符號。根據 POSIX 規範,所有文字檔案都應以換行符號結束。請在檔案的最後一行後新增一個換行符號。", + "is_new": true + }, + { + "level": "critical", + "role": "Maya", + "location": "entrypoint.sh", + "suggestion": "此腳本包含複雜的邏輯,特別是版本解析與遞增,並依賴外部服務(如 `curl` 和 `jq`)。目前完全沒有提供任何測試,這使得任何修改都存在高風險。請務必實作一套健全的測試套件,例如使用 `bats` 或 `shunit2` 等 shell 測試框架。測試應包含單元測試(針對輔助函數)和整合測試(模擬外部依賴,如 `curl` 和 `jq` 的回應),以涵蓋各種情境(例如不同的發布歷史、網路錯誤、格式錯誤的 JSON)。", + "is_new": true + }, + { + "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`,以驗證進位行為是否符合預期。", + "is_new": true + }, + { + "level": "warning", + "role": "Zara", + "location": "entrypoint.sh:65, entrypoint.sh:90", + "suggestion": "腳本多次對完整的 `RELEASE_JSON` 字串執行 `jq` 命令來提取不同資訊。儘管 `RELEASE_JSON` 已載入記憶體,但每次 `jq` 呼叫都會啟動新程序並重新解析字串。對於擁有大量發行版本的儲存庫,這可能導致顯著的效能開銷。建議考慮將這些 `jq` 操作合併為單次處理,以一次性提取所有所需資料(最新的穩定版本和計算出的新版本最高 beta 編號),從而減少 CPU 週期和程序啟動的開銷。", + "is_new": true + }, + { + "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 是否符合預期的格式和網域,並確保儲存庫名稱不包含惡意字元(如 `../` 或特殊編碼字元)。", + "is_new": true + }, + { + "level": "warning", + "role": "Aria", + "location": "entrypoint.sh:44", + "suggestion": "`require_env` 函數中的 `printf '%s=%s\n' \"$name\" \"$value\"` 會直接印出環境變數的值。對於 `RUNNER_TOKEN` 這類敏感資訊,這可能導致安全風險。建議修改此函數,使其僅印出變數名稱,或對敏感變數的值進行遮蔽處理。例如,可以改為 `info \"$name 已設定\"`,或在呼叫 `require_env` 之前處理敏感變數的輸出。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "entrypoint.sh:70-73", + "suggestion": "用於提取 `LATEST_VERSION` 和 `BETA` 的 `jq` 指令相當複雜,且依賴特定的 JSON 結構。儘管 `curl -fsS` 有助於處理網路錯誤,但 `jq` 在面對格式錯誤或邊界條件的 JSON 回應時,仍可能產生非預期的結果。請新增專門的測試案例,為 `jq` 提供各種模擬的 `RELEASE_JSON` 輸入,包括:空的發布陣列 `[]`、僅包含 beta 版本的陣列、沒有任何發布的陣列(例如 `curl` 返回 `null` 或空字串)、具有非標準 `tag_name` 格式的發布,以及格式錯誤的 JSON。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "entrypoint.sh:54-57", + "suggestion": "`IS_BETA` 變數的初始化邏輯 (`IS_BETA=\"${IS_BETA:-false}\"` 接著 `if [ \"$IS_BETA\" = \"null\" ] || [ -z \"$IS_BETA\" ]; then IS_BETA=\"false\"; fi`) 略顯冗餘。如果 `IS_BETA` 確實是字串 \"null\",則第一個賦值不會改變它,而第二個 `if` 條件會處理。如果 `IS_BETA` 未設定或為空,第一個賦值會將其設為 \"false\",使得 `if` 條件中的 `[ -z \"$IS_BETA\" ]` 為假。請簡化 `IS_BETA` 的初始化邏輯,以提高清晰度和穩健性。例如,可以考慮使用 `IS_BETA=$(echo \"${IS_BETA:-false}\" | sed 's/null/false/')` 或更清晰的條件區塊。同時,請新增測試案例,涵蓋 `IS_BETA` 未設定、空字串、字串 \"null\"、\"true\"、\"false\" 以及其他非預期字串值的情況,以確保行為一致。", + "is_new": true + }, + { + "level": "info", + "role": "Zara", + "location": "entrypoint.sh:58", + "suggestion": "腳本從 Gitea API 獲取所有發行版本。如果儲存庫中的發行版本數量非常龐大,這可能會消耗大量的網路頻寬和記憶體。儘管目前的邏輯可能需要檢查多個發行版本(例如,尋找最新的穩定版或特定的 beta 版本),但值得研究 Gitea API 是否提供更精細的過濾(例如,按標籤名稱模式或排除 beta 標籤)或分頁功能,以減少獲取的資料量,特別是當腳本只需要部分發行版本資訊時。", + "is_new": true + }, + { + "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 建議的多行輸出語法以確保安全。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "entrypoint.sh:5", + "suggestion": "在 `readonly` 變數定義和第一個函數定義之間增加一個空行,可以提高程式碼區塊之間的視覺分隔,增強可讀性。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "entrypoint.sh:53", + "suggestion": "為了保持日誌輸出的一致性,建議將 `RUNNER_TOKEN` 的狀態輸出也透過 `info` 函數處理,例如 `info \"RUNNER_TOKEN=***\"` 或 `info \"RUNNER_TOKEN=未提供\"`。這有助於統一日誌格式。", + "is_new": true + }, + { + "level": "info", + "role": "Aria", + "location": "entrypoint.sh:70", + "suggestion": "多行 `jq` 查詢字串的縮排方式可以考慮調整。為了提高可讀性,可以將 `jq` 腳本內容使用 heredoc 語法,或將其縮排與 `jq -r` 對齊,而不是與 `printf` 的內容對齊。目前的縮排方式雖然功能上無誤,但可能在視覺上造成混淆。", + "is_new": true + }, + { + "level": "info", + "role": "Maya", + "location": "entrypoint.sh:109", + "suggestion": "此腳本直接寫入 `$GITHUB_OUTPUT`,這是 GitHub Actions 環境特有的行為。雖然這對於其預期用途是正確的,但這使得在沒有完整 GitHub Actions 環境的情況下,難以獨立測試輸出機制。在建立測試套件時,請確保 `write_output` 函數可以透過將 `$GITHUB_OUTPUT` 重定向到臨時文件,或透過模擬 `write_output` 函數本身來進行測試,以便在不執行完整 GitHub Actions 的情況下驗證生成的輸出。", + "is_new": true + } +] diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..f1a77ef --- /dev/null +++ b/.github/copilot-instructions.md @@ -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`. diff --git a/.github/skills/triage-findings/SKILL.md b/.github/skills/triage-findings/SKILL.md new file mode 100644 index 0000000..8ca4117 --- /dev/null +++ b/.github/skills/triage-findings/SKILL.md @@ -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`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..fa2403d --- /dev/null +++ b/CLAUDE.md @@ -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`. diff --git a/GEMINI.md b/GEMINI.md new file mode 100644 index 0000000..8ca4117 --- /dev/null +++ b/GEMINI.md @@ -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`. From 4bdde4f7cebb80101367edcc52647e10c27cc71e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 15 May 2026 16:55:04 +0000 Subject: [PATCH 4/9] test: harden version entrypoint --- .gitea/ai-review/exclusions.json | 32 +++ entrypoint.sh | 251 +++++++++++++++++------- tests/entrypoint_test.sh | 321 +++++++++++++++++++++++++++++++ 3 files changed, 533 insertions(+), 71 deletions(-) create mode 100644 .gitea/ai-review/exclusions.json create mode 100755 tests/entrypoint_test.sh diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json new file mode 100644 index 0000000..7006a6a --- /dev/null +++ b/.gitea/ai-review/exclusions.json @@ -0,0 +1,32 @@ +[ + { + "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 建議的多行輸出語法以確保安全。" + } +] diff --git a/entrypoint.sh b/entrypoint.sh index 7ee7e92..e02472d 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -33,77 +33,186 @@ write_output() { printf 'version=%s\n' "$1" >> "$GITHUB_OUTPUT" } -section "參數檢查" +normalize_beta_flag() { + local value="${1:-false}" -require_env "GITEA_SERVER_URL" "${GITEA_SERVER_URL:-}" -require_env "GITEA_REPOSITORY" "${GITEA_REPOSITORY:-}" + if [ -z "$value" ] || [ "$value" = "null" ]; then + printf '%s\n' "false" + return + fi -if [ -n "${RUNNER_TOKEN:-}" ] && [ "${RUNNER_TOKEN:-}" != "null" ]; then - printf 'RUNNER_TOKEN=%s\n' '***' -else - printf 'RUNNER_TOKEN=%s\n' '未提供' + 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" + + if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then + LATEST_TAG="0.0.0" + fi + + info "LATEST_VERSION=$LATEST_TAG" + + section "計算版本號" + + if [ -z "$NEW_VERSION" ] || [ "$NEW_VERSION" = "null" ]; then + NEW_VERSION="0.0.1" + fi + + info "NEW_VERSION=$NEW_VERSION" + write_output "$NEW_VERSION" +} + +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + main "$@" fi - -IS_BETA="${IS_BETA:-false}" -if [ "$IS_BETA" = "null" ] || [ -z "$IS_BETA" ]; then - IS_BETA="false" -fi -printf 'IS_BETA=%s\n' "$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 - -LATEST_VERSION="$( - printf '%s' "$RELEASE_JSON" | jq -r ' - [ .[] | select(.tag_name | test("-beta\\.") | not) | .tag_name ][0] // "v0.0.0" - ' | sed 's/^v//' -)" - -if [ -z "$LATEST_VERSION" ] || [ "$LATEST_VERSION" = "null" ]; then - LATEST_VERSION="0.0.0" -fi - -info "LATEST_VERSION=$LATEST_VERSION" - -section "計算版本號" - -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 - -NEW_VERSION="$MAJOR.$MINOR.$PATCH" - -if [ "$IS_BETA" = "true" ]; then - BETA="$( - printf '%s' "$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" -fi - -info "NEW_VERSION=$NEW_VERSION" -write_output "$NEW_VERSION" diff --git a/tests/entrypoint_test.sh b/tests/entrypoint_test.sh new file mode 100755 index 0000000..14dadf6 --- /dev/null +++ b/tests/entrypoint_test.sh @@ -0,0 +1,321 @@ +#!/bin/bash + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +source "$ROOT_DIR/entrypoint.sh" + +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 [ "${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)" + 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" \ + 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" \ + 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" +} + +test_stable_release_flow() { + local response_file + local output_file + + response_file="$(mktemp)" + 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 response_file + local output_file + + response_file="$(mktemp)" + 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 response_file + local output_file + + response_file="$(mktemp)" + 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 response_file + local output_file + + response_file="$(mktemp)" + 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 response_file + local output_file + + response_file="$(mktemp)" + 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_malformed_release_payload() { + local response_file + local workdir + local bin_dir + local output_file + local stdout_file + local stderr_file + + response_file="$(mktemp)" + printf '%s\n' '{' >"$response_file" + workdir="$(mktemp -d)" + 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 response_file + local workdir + local bin_dir + local output_file + local stdout_file + local stderr_file + + response_file="$(mktemp)" + printf '%s\n' '[]' >"$response_file" + workdir="$(mktemp -d)" + 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_malformed_release_payload +test_curl_failure + +printf '[info] all tests passed\n' From fadcf9b14c4b8473066dcc5b5d2fa60005c96581 Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Fri, 15 May 2026 16:57:14 +0000 Subject: [PATCH 5/9] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/findings.json | 82 +++++++++++----------------------- 1 file changed, 27 insertions(+), 55 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 88de3f8..858d294 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,23 +1,16 @@ [ { "level": "critical", - "role": "Aria", - "location": "entrypoint.sh", - "suggestion": "檔案結尾缺少一個換行符號。根據 POSIX 規範,所有文字檔案都應以換行符號結束。請在檔案的最後一行後新增一個換行符號。", + "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` 的職責,使其僅用於資料提取而非複雜的邏輯運算。如果必須使用單一 `jq` 腳本,請考慮將其拆分為多行並使用 `read -r -d '' JQ_SCRIPT << 'EOF'` 語法,以提高可讀性。", "is_new": true }, { "level": "critical", - "role": "Maya", - "location": "entrypoint.sh", - "suggestion": "此腳本包含複雜的邏輯,特別是版本解析與遞增,並依賴外部服務(如 `curl` 和 `jq`)。目前完全沒有提供任何測試,這使得任何修改都存在高風險。請務必實作一套健全的測試套件,例如使用 `bats` 或 `shunit2` 等 shell 測試框架。測試應包含單元測試(針對輔助函數)和整合測試(模擬外部依賴,如 `curl` 和 `jq` 的回應),以涵蓋各種情境(例如不同的發布歷史、網路錯誤、格式錯誤的 JSON)。", - "is_new": true - }, - { - "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`,以驗證進位行為是否符合預期。", + "role": "Leo", + "location": "tests/entrypoint_test.sh:50-109", + "suggestion": "在 `make_mock_jq` 函式中,為了測試 `entrypoint.sh`,您在 Python 中重新實作了核心的版本計算邏輯。這導致了業務邏輯的嚴重重複(Python 與 `entrypoint.sh` 中的 `jq` 腳本),使得測試本身變得複雜且脆弱。如果 `entrypoint.sh` 中的版本計算邏輯發生變化,此 Mock 也需要同步更新,這極易引入錯誤。建議簡化 `entrypoint.sh` 中的 `jq` 邏輯(如前一個建議所述),這樣 Mock `jq` 就可以更簡單,例如僅根據輸入返回預定義的輸出,而不是重新計算邏輯。", "is_new": true }, { @@ -25,34 +18,34 @@ "role": "Zara", "location": "entrypoint.sh:65, entrypoint.sh:90", "suggestion": "腳本多次對完整的 `RELEASE_JSON` 字串執行 `jq` 命令來提取不同資訊。儘管 `RELEASE_JSON` 已載入記憶體,但每次 `jq` 呼叫都會啟動新程序並重新解析字串。對於擁有大量發行版本的儲存庫,這可能導致顯著的效能開銷。建議考慮將這些 `jq` 操作合併為單次處理,以一次性提取所有所需資料(最新的穩定版本和計算出的新版本最高 beta 編號),從而減少 CPU 週期和程序啟動的開銷。", - "is_new": true + "is_new": false }, { "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 是否符合預期的格式和網域,並確保儲存庫名稱不包含惡意字元(如 `../` 或特殊編碼字元)。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "entrypoint.sh:44", - "suggestion": "`require_env` 函數中的 `printf '%s=%s\n' \"$name\" \"$value\"` 會直接印出環境變數的值。對於 `RUNNER_TOKEN` 這類敏感資訊,這可能導致安全風險。建議修改此函數,使其僅印出變數名稱,或對敏感變數的值進行遮蔽處理。例如,可以改為 `info \"$name 已設定\"`,或在呼叫 `require_env` 之前處理敏感變數的輸出。", + "role": "Leo", + "location": "entrypoint.sh:159", + "suggestion": "在 `main` 函式中,`curl -fsS` 會抑制所有進度與錯誤訊息。雖然 `set -e` 會在 `curl` 失敗時終止腳本,但缺乏詳細的錯誤輸出會使問題診斷變得困難。建議在 `curl` 失敗時,能將其錯誤訊息導向到標準錯誤輸出,或在 `fail` 函式中提供更具體的錯誤上下文,例如 `RELEASE_JSON=\"$(curl -fsS -H ... \"$RELEASE_URL\" || fail \"無法從 Gitea 伺服器取得 Release 資訊\")\"`。", "is_new": true }, { "level": "warning", "role": "Maya", - "location": "entrypoint.sh:70-73", - "suggestion": "用於提取 `LATEST_VERSION` 和 `BETA` 的 `jq` 指令相當複雜,且依賴特定的 JSON 結構。儘管 `curl -fsS` 有助於處理網路錯誤,但 `jq` 在面對格式錯誤或邊界條件的 JSON 回應時,仍可能產生非預期的結果。請新增專門的測試案例,為 `jq` 提供各種模擬的 `RELEASE_JSON` 輸入,包括:空的發布陣列 `[]`、僅包含 beta 版本的陣列、沒有任何發布的陣列(例如 `curl` 返回 `null` 或空字串)、具有非標準 `tag_name` 格式的發布,以及格式錯誤的 JSON。", + "location": "tests/entrypoint_test.sh:L209", + "suggestion": "在 `test_unit_helpers` 中,`next_release_version` 函數的測試案例涵蓋了 patch 和 minor 版本的進位,但缺少對 major 版本進位的測試 (例如,從 '9.9.9' 進位到 '10.0.0')。請添加此邊界條件的測試,以確保版本號計算的完整性。", "is_new": true }, { "level": "warning", "role": "Maya", - "location": "entrypoint.sh:54-57", - "suggestion": "`IS_BETA` 變數的初始化邏輯 (`IS_BETA=\"${IS_BETA:-false}\"` 接著 `if [ \"$IS_BETA\" = \"null\" ] || [ -z \"$IS_BETA\" ]; then IS_BETA=\"false\"; fi`) 略顯冗餘。如果 `IS_BETA` 確實是字串 \"null\",則第一個賦值不會改變它,而第二個 `if` 條件會處理。如果 `IS_BETA` 未設定或為空,第一個賦值會將其設為 \"false\",使得 `if` 條件中的 `[ -z \"$IS_BETA\" ]` 為假。請簡化 `IS_BETA` 的初始化邏輯,以提高清晰度和穩健性。例如,可以考慮使用 `IS_BETA=$(echo \"${IS_BETA:-false}\" | sed 's/null/false/')` 或更清晰的條件區塊。同時,請新增測試案例,涵蓋 `IS_BETA` 未設定、空字串、字串 \"null\"、\"true\"、\"false\" 以及其他非預期字串值的情況,以確保行為一致。", + "location": "tests/entrypoint_test.sh:L209", + "suggestion": "目前測試案例中,雖然 `run_entrypoint` 函數接受 `token` 參數,但沒有明確的測試案例來驗證當 `RUNNER_TOKEN` 存在時,`curl` 是否正確地使用了授權標頭。請添加一個測試案例,明確地傳遞 `RUNNER_TOKEN` 並驗證其行為。", + "is_new": true + }, + { + "level": "warning", + "role": "Maya", + "location": "tests/entrypoint_test.sh:L209", + "suggestion": "測試腳本在執行過程中會創建臨時文件和目錄(例如 `response_file` 和 `workdir`),但目前沒有明確的清理機制來移除這些臨時資源。這可能導致測試環境中累積不必要的檔案。建議在測試腳本的開頭或結尾添加 `trap 'rm -rf \"$workdir\" \"$response_file\"' EXIT` 等清理命令,確保測試完成後所有臨時資源都被妥善移除。", "is_new": true }, { @@ -60,41 +53,20 @@ "role": "Zara", "location": "entrypoint.sh:58", "suggestion": "腳本從 Gitea API 獲取所有發行版本。如果儲存庫中的發行版本數量非常龐大,這可能會消耗大量的網路頻寬和記憶體。儘管目前的邏輯可能需要檢查多個發行版本(例如,尋找最新的穩定版或特定的 beta 版本),但值得研究 Gitea API 是否提供更精細的過濾(例如,按標籤名稱模式或排除 beta 標籤)或分頁功能,以減少獲取的資料量,特別是當腳本只需要部分發行版本資訊時。", - "is_new": true + "is_new": false }, { "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 建議的多行輸出語法以確保安全。", + "role": "Leo", + "location": "entrypoint.sh:110", + "suggestion": "在 `calculate_version` 函式內的 `jq` 腳本中,版本號進位邏輯使用了「10」這個魔術數字(例如,`patch >= 10`)。雖然這是常見的版本控制慣例,但為了提高程式碼的清晰度和未來可配置性,建議將其定義為一個具名的 `jq` 變數或 Bash `readonly` 常數。", "is_new": true }, { "level": "info", "role": "Aria", - "location": "entrypoint.sh:5", - "suggestion": "在 `readonly` 變數定義和第一個函數定義之間增加一個空行,可以提高程式碼區塊之間的視覺分隔,增強可讀性。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "entrypoint.sh:53", - "suggestion": "為了保持日誌輸出的一致性,建議將 `RUNNER_TOKEN` 的狀態輸出也透過 `info` 函數處理,例如 `info \"RUNNER_TOKEN=***\"` 或 `info \"RUNNER_TOKEN=未提供\"`。這有助於統一日誌格式。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "entrypoint.sh:70", - "suggestion": "多行 `jq` 查詢字串的縮排方式可以考慮調整。為了提高可讀性,可以將 `jq` 腳本內容使用 heredoc 語法,或將其縮排與 `jq -r` 對齊,而不是與 `printf` 的內容對齊。目前的縮排方式雖然功能上無誤,但可能在視覺上造成混淆。", - "is_new": true - }, - { - "level": "info", - "role": "Maya", - "location": "entrypoint.sh:109", - "suggestion": "此腳本直接寫入 `$GITHUB_OUTPUT`,這是 GitHub Actions 環境特有的行為。雖然這對於其預期用途是正確的,但這使得在沒有完整 GitHub Actions 環境的情況下,難以獨立測試輸出機制。在建立測試套件時,請確保 `write_output` 函數可以透過將 `$GITHUB_OUTPUT` 重定向到臨時文件,或透過模擬 `write_output` 函數本身來進行測試,以便在不執行完整 GitHub Actions 的情況下驗證生成的輸出。", + "location": "b/entrypoint.sh:176,182", + "suggestion": "在 `main` 函數中,`LATEST_TAG` 和 `NEW_VERSION` 的空值或 `null` 檢查可能是多餘的。`calculate_version` 函數已經處理了這些情況並返回了預設值。建議移除這些檢查以簡化程式碼,除非有特殊情況需要再次驗證 `calculate_version` 的輸出。", "is_new": true } ] From 9e0253f74dbdbeb26f8499f6c3fb4ab33d0a1f83 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 15 May 2026 17:07:00 +0000 Subject: [PATCH 6/9] chore: triage review findings --- .gitea/ai-review/exclusions.json | 36 +++++++++++ entrypoint.sh | 8 --- tests/entrypoint_test.sh | 102 +++++++++++++++++++++++++++---- 3 files changed, 126 insertions(+), 20 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 7006a6a..13c41c3 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -28,5 +28,41 @@ "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` 常數。" } ] diff --git a/entrypoint.sh b/entrypoint.sh index e02472d..cd75246 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -197,18 +197,10 @@ main() { VERSION_INFO="$(calculate_version "$RELEASE_JSON" "$IS_BETA")" IFS=$'\t' read -r LATEST_TAG NEW_VERSION <<< "$VERSION_INFO" - if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then - LATEST_TAG="0.0.0" - fi - info "LATEST_VERSION=$LATEST_TAG" section "計算版本號" - if [ -z "$NEW_VERSION" ] || [ "$NEW_VERSION" = "null" ]; then - NEW_VERSION="0.0.1" - fi - info "NEW_VERSION=$NEW_VERSION" write_output "$NEW_VERSION" } diff --git a/tests/entrypoint_test.sh b/tests/entrypoint_test.sh index 14dadf6..2bf5859 100755 --- a/tests/entrypoint_test.sh +++ b/tests/entrypoint_test.sh @@ -6,6 +6,16 @@ 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 @@ -27,6 +37,10 @@ make_mock_curl() { 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 @@ -135,6 +149,7 @@ run_entrypoint() { local stderr_file workdir="$(mktemp -d)" + CLEANUP_PATHS+=("$workdir") bin_dir="$workdir/bin" mkdir -p "$bin_dir" make_mock_curl "$bin_dir" "$response_file" @@ -147,6 +162,7 @@ run_entrypoint() { 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" \ @@ -157,6 +173,7 @@ run_entrypoint() { 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" \ @@ -174,13 +191,17 @@ test_unit_helpers() { 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 - response_file="$(mktemp)" + workdir="$(mktemp -d)" + CLEANUP_PATHS+=("$workdir") + response_file="$workdir/releases.json" cat >"$response_file" <<'EOF' [ {"tag_name":"v1.2.3-beta.1"}, @@ -194,10 +215,13 @@ EOF } test_beta_release_flow() { + local workdir local response_file local output_file - response_file="$(mktemp)" + workdir="$(mktemp -d)" + CLEANUP_PATHS+=("$workdir") + response_file="$workdir/releases.json" cat >"$response_file" <<'EOF' [ {"tag_name":"v1.2.3"}, @@ -211,10 +235,13 @@ EOF } test_empty_release_list() { + local workdir local response_file local output_file - response_file="$(mktemp)" + workdir="$(mktemp -d)" + CLEANUP_PATHS+=("$workdir") + response_file="$workdir/releases.json" printf '%s\n' '[]' >"$response_file" output_file="$(run_entrypoint "$response_file" "false")" @@ -222,10 +249,13 @@ test_empty_release_list() { } test_only_beta_releases() { + local workdir local response_file local output_file - response_file="$(mktemp)" + workdir="$(mktemp -d)" + CLEANUP_PATHS+=("$workdir") + response_file="$workdir/releases.json" cat >"$response_file" <<'EOF' [ {"tag_name":"v2.4.6-beta.1"}, @@ -238,27 +268,73 @@ EOF } test_null_release_payload() { + local workdir local response_file local output_file - response_file="$(mktemp)" + 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_malformed_release_payload() { - local response_file +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 - response_file="$(mktemp)" - printf '%s\n' '{' >"$response_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" @@ -280,16 +356,17 @@ test_malformed_release_payload() { } test_curl_failure() { - local response_file local workdir + local response_file local bin_dir local output_file local stdout_file local stderr_file - response_file="$(mktemp)" - printf '%s\n' '[]' >"$response_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" @@ -315,6 +392,7 @@ 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 From be954f4cb11164dcddd3919a9f218728ce71e3bd Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Fri, 15 May 2026 17:08:18 +0000 Subject: [PATCH 7/9] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/findings.json | 53 ++-------------------------------- 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 858d294..4966955 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,72 +1,23 @@ [ - { - "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` 的職責,使其僅用於資料提取而非複雜的邏輯運算。如果必須使用單一 `jq` 腳本,請考慮將其拆分為多行並使用 `read -r -d '' JQ_SCRIPT << 'EOF'` 語法,以提高可讀性。", - "is_new": true - }, - { - "level": "critical", - "role": "Leo", - "location": "tests/entrypoint_test.sh:50-109", - "suggestion": "在 `make_mock_jq` 函式中,為了測試 `entrypoint.sh`,您在 Python 中重新實作了核心的版本計算邏輯。這導致了業務邏輯的嚴重重複(Python 與 `entrypoint.sh` 中的 `jq` 腳本),使得測試本身變得複雜且脆弱。如果 `entrypoint.sh` 中的版本計算邏輯發生變化,此 Mock 也需要同步更新,這極易引入錯誤。建議簡化 `entrypoint.sh` 中的 `jq` 邏輯(如前一個建議所述),這樣 Mock `jq` 就可以更簡單,例如僅根據輸入返回預定義的輸出,而不是重新計算邏輯。", - "is_new": true - }, - { - "level": "warning", - "role": "Zara", - "location": "entrypoint.sh:65, entrypoint.sh:90", - "suggestion": "腳本多次對完整的 `RELEASE_JSON` 字串執行 `jq` 命令來提取不同資訊。儘管 `RELEASE_JSON` 已載入記憶體,但每次 `jq` 呼叫都會啟動新程序並重新解析字串。對於擁有大量發行版本的儲存庫,這可能導致顯著的效能開銷。建議考慮將這些 `jq` 操作合併為單次處理,以一次性提取所有所需資料(最新的穩定版本和計算出的新版本最高 beta 編號),從而減少 CPU 週期和程序啟動的開銷。", - "is_new": false - }, - { - "level": "warning", - "role": "Leo", - "location": "entrypoint.sh:159", - "suggestion": "在 `main` 函式中,`curl -fsS` 會抑制所有進度與錯誤訊息。雖然 `set -e` 會在 `curl` 失敗時終止腳本,但缺乏詳細的錯誤輸出會使問題診斷變得困難。建議在 `curl` 失敗時,能將其錯誤訊息導向到標準錯誤輸出,或在 `fail` 函式中提供更具體的錯誤上下文,例如 `RELEASE_JSON=\"$(curl -fsS -H ... \"$RELEASE_URL\" || fail \"無法從 Gitea 伺服器取得 Release 資訊\")\"`。", - "is_new": true - }, { "level": "warning", "role": "Maya", "location": "tests/entrypoint_test.sh:L209", "suggestion": "在 `test_unit_helpers` 中,`next_release_version` 函數的測試案例涵蓋了 patch 和 minor 版本的進位,但缺少對 major 版本進位的測試 (例如,從 '9.9.9' 進位到 '10.0.0')。請添加此邊界條件的測試,以確保版本號計算的完整性。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Maya", "location": "tests/entrypoint_test.sh:L209", "suggestion": "目前測試案例中,雖然 `run_entrypoint` 函數接受 `token` 參數,但沒有明確的測試案例來驗證當 `RUNNER_TOKEN` 存在時,`curl` 是否正確地使用了授權標頭。請添加一個測試案例,明確地傳遞 `RUNNER_TOKEN` 並驗證其行為。", - "is_new": true + "is_new": false }, { "level": "warning", "role": "Maya", "location": "tests/entrypoint_test.sh:L209", "suggestion": "測試腳本在執行過程中會創建臨時文件和目錄(例如 `response_file` 和 `workdir`),但目前沒有明確的清理機制來移除這些臨時資源。這可能導致測試環境中累積不必要的檔案。建議在測試腳本的開頭或結尾添加 `trap 'rm -rf \"$workdir\" \"$response_file\"' EXIT` 等清理命令,確保測試完成後所有臨時資源都被妥善移除。", - "is_new": true - }, - { - "level": "info", - "role": "Zara", - "location": "entrypoint.sh:58", - "suggestion": "腳本從 Gitea API 獲取所有發行版本。如果儲存庫中的發行版本數量非常龐大,這可能會消耗大量的網路頻寬和記憶體。儘管目前的邏輯可能需要檢查多個發行版本(例如,尋找最新的穩定版或特定的 beta 版本),但值得研究 Gitea API 是否提供更精細的過濾(例如,按標籤名稱模式或排除 beta 標籤)或分頁功能,以減少獲取的資料量,特別是當腳本只需要部分發行版本資訊時。", "is_new": false - }, - { - "level": "info", - "role": "Leo", - "location": "entrypoint.sh:110", - "suggestion": "在 `calculate_version` 函式內的 `jq` 腳本中,版本號進位邏輯使用了「10」這個魔術數字(例如,`patch >= 10`)。雖然這是常見的版本控制慣例,但為了提高程式碼的清晰度和未來可配置性,建議將其定義為一個具名的 `jq` 變數或 Bash `readonly` 常數。", - "is_new": true - }, - { - "level": "info", - "role": "Aria", - "location": "b/entrypoint.sh:176,182", - "suggestion": "在 `main` 函數中,`LATEST_TAG` 和 `NEW_VERSION` 的空值或 `null` 檢查可能是多餘的。`calculate_version` 函數已經處理了這些情況並返回了預設值。建議移除這些檢查以簡化程式碼,除非有特殊情況需要再次驗證 `calculate_version` 的輸出。", - "is_new": true } ] From 74c544d4e4d5aa44d8f6d1b68c70b6d35a6c4d6f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 15 May 2026 17:11:41 +0000 Subject: [PATCH 8/9] chore: update review exclusions --- .gitea/ai-review/exclusions.json | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 13c41c3..f1f6c46 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -64,5 +64,23 @@ "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`,以確保測試完成後所有暫存資源皆被妥善移除。" } ] From 9caf84432f1f380c0d3bf716ae86437894d9a9ad Mon Sep 17 00:00:00 2001 From: AI Review Bot Date: Fri, 15 May 2026 17:13:07 +0000 Subject: [PATCH 9/9] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/findings.json | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index 4966955..fe51488 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -1,23 +1 @@ -[ - { - "level": "warning", - "role": "Maya", - "location": "tests/entrypoint_test.sh:L209", - "suggestion": "在 `test_unit_helpers` 中,`next_release_version` 函數的測試案例涵蓋了 patch 和 minor 版本的進位,但缺少對 major 版本進位的測試 (例如,從 '9.9.9' 進位到 '10.0.0')。請添加此邊界條件的測試,以確保版本號計算的完整性。", - "is_new": false - }, - { - "level": "warning", - "role": "Maya", - "location": "tests/entrypoint_test.sh:L209", - "suggestion": "目前測試案例中,雖然 `run_entrypoint` 函數接受 `token` 參數,但沒有明確的測試案例來驗證當 `RUNNER_TOKEN` 存在時,`curl` 是否正確地使用了授權標頭。請添加一個測試案例,明確地傳遞 `RUNNER_TOKEN` 並驗證其行為。", - "is_new": false - }, - { - "level": "warning", - "role": "Maya", - "location": "tests/entrypoint_test.sh:L209", - "suggestion": "測試腳本在執行過程中會創建臨時文件和目錄(例如 `response_file` 和 `workdir`),但目前沒有明確的清理機制來移除這些臨時資源。這可能導致測試環境中累積不必要的檔案。建議在測試腳本的開頭或結尾添加 `trap 'rm -rf \"$workdir\" \"$response_file\"' EXIT` 等清理命令,確保測試完成後所有臨時資源都被妥善移除。", - "is_new": false - } -] +[]