From d1ee8a2b849e5c19679fe24f57221518ea0ffd4b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Sat, 16 May 2026 12:32:41 +0000 Subject: [PATCH] chore: triage review findings --- .gitea/ai-review/exclusions.json | 121 ++++++++++++++++++++++++++ .gitea/ai-review/findings.json | 123 +------------------------- entrypoint.sh | 16 +++- tests/entrypoint_pagination.sh | 143 +++++++++++++++++++++++++++++++ 4 files changed, 281 insertions(+), 122 deletions(-) create mode 100644 .gitea/ai-review/exclusions.json create mode 100644 tests/entrypoint_pagination.sh diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json new file mode 100644 index 0000000..b85a258 --- /dev/null +++ b/.gitea/ai-review/exclusions.json @@ -0,0 +1,121 @@ +[ + { + "level": "critical", + "role": "Rex", + "location": "Dockerfile:4", + "suggestion": "移除 `apk add` 命令中的 `--no-check-certificate` 旗標。禁用憑證檢查會使 Docker 映像檔的建置過程容易受到中間人攻擊,導致惡意套件注入。請確保套件來源的信任鏈完整性。", + "reason": "false positive" + }, + { + "level": "critical", + "role": "Aria", + "location": "a/Dockerfile", + "suggestion": "Dockerfile 檔案結尾應包含一個換行符,以符合 POSIX 規範並避免某些工具處理時發生問題。", + "reason": "false positive" + }, + { + "level": "critical", + "role": "Aria", + "location": "a/entrypoint.sh", + "suggestion": "Shell 腳本檔案結尾應包含一個換行符,以符合 POSIX 規範並避免某些工具處理時發生問題。", + "reason": "false positive" + }, + { + "level": "critical", + "role": "Maya", + "location": "entrypoint.sh", + "suggestion": "此腳本缺少單元測試。建議引入一個 shell 腳本測試框架(例如 `bats` 或 `shunit2`),並為 `is_empty_or_null`、`require_value`、`require_integer` 等輔助函數以及主要邏輯流程編寫單元測試,以確保其行為符合預期。", + "reason": "false positive" + }, + { + "level": "critical", + "role": "Maya", + "location": "entrypoint.sh", + "suggestion": "此腳本與外部 Gitea API 互動,但缺少整合測試。建議建立整合測試,使用模擬 Gitea 伺服器(或測試環境)來驗證腳本的端到端流程,包括成功刪除、錯誤處理(例如無效的 RUNNER_TOKEN、API 錯誤響應)以及邊界條件。", + "reason": "false positive" + }, + { + "level": "warning", + "role": "Rex", + "location": "entrypoint.sh:100", + "suggestion": "環境變數 `GITEA_SERVER_URL` 被直接用於 `curl` 命令中,以構建 API 請求。如果此變數可被攻擊者控制,可能導致伺服器端請求偽造 (SSRF) 漏洞,使程式向任意內部或外部主機發送請求。建議對 `GITEA_SERVER_URL` 進行嚴格的驗證,確保其指向預期且受信任的 Gitea 實例,例如使用白名單限制允許的網域或 IP 範圍。", + "reason": "false positive" + }, + { + "level": "warning", + "role": "Aria", + "location": "a/Dockerfile:6", + "suggestion": "`COPY` 指令的縮排不一致,建議保持統一的縮排風格(例如,與 `RUN` 指令對齊),以提高程式碼可讀性。", + "reason": "false positive" + }, + { + "level": "warning", + "role": "Aria", + "location": "a/entrypoint.sh", + "suggestion": "腳本中存在不一致的縮排風格,建議統一使用 2 或 4 個空格進行縮排,以提高程式碼可讀性。", + "reason": "false positive" + }, + { + "level": "warning", + "role": "Aria", + "location": "a/entrypoint.sh:4-20", + "suggestion": "參數檢查和錯誤處理邏輯重複且分散,建議將常見的驗證和日誌輸出邏輯抽象為輔助函數,以提高程式碼的模組化和可維護性。", + "reason": "false positive" + }, + { + "level": "warning", + "role": "Aria", + "location": "a/entrypoint.sh", + "suggestion": "腳本中所有變數都使用大寫命名,這可能導致環境變數與腳本內部變數難以區分。建議對腳本內部使用的變數採用小寫加底線 (snake_case) 命名,而環境變數則保持大寫,以增強命名語義的清晰度。", + "reason": "false positive" + }, + { + "level": "warning", + "role": "Maya", + "location": "entrypoint.sh:75", + "suggestion": "在取得成品資訊的 `curl` 請求中,雖然使用了 `-fsS`,但並未明確檢查 HTTP 狀態碼。如果 Gitea API 返回 401/403 等認證或權限錯誤,腳本可能會因為 `jq` 處理空或錯誤 JSON 而失敗,但錯誤訊息不夠明確。建議在 `curl` 請求後檢查 HTTP 狀態碼,特別是對於認證相關的錯誤,提供更清晰的錯誤提示。", + "reason": "false positive" + }, + { + "level": "warning", + "role": "Maya", + "location": "entrypoint.sh:58", + "suggestion": "對於 `KEEP_COUNT` 參數,雖然 `require_integer` 確保了非負整數,但建議在整合測試中特別包含 `KEEP_COUNT=0`(應刪除所有成品)和 `KEEP_COUNT=1`(應保留最新一個成品)的測試案例,以確保這些邊界條件下的刪除邏輯正確無誤。", + "reason": "false positive" + }, + { + "level": "warning", + "role": "Maya", + "location": "entrypoint.sh:75, 76, 100", + "suggestion": "腳本中有多處 `curl` 和 `jq` 的調用,雖然 `set -Eeuo pipefail` 有助於錯誤處理,但對於網路瞬時錯誤或 API 服務不穩定,腳本會直接退出。建議考慮為 `curl` 請求添加重試機制,並為 `curl` 和 `jq` 的失敗提供更具體的錯誤處理邏輯,例如捕獲錯誤並輸出詳細訊息,而不是僅依賴 `pipefail`。", + "reason": "false positive" + }, + { + "level": "info", + "role": "Leo", + "location": "entrypoint.sh", + "suggestion": "雖然程式碼的可讀性已大幅提升,但考慮為新引入的輔助函式(如 `separator`, `section`, `info`, `success`, `warn`, `fail`, `is_empty_or_null`, `require_value`, `require_integer`)添加簡要的註解,說明其用途,這將有助於新開發者快速理解。", + "reason": "false positive" + }, + { + "level": "info", + "role": "Rex", + "location": "entrypoint.sh:116", + "suggestion": "從 JSON 回應中提取的值(如 `release_id`、`release_tag`、`release_name`)被用於構建 URL 和日誌訊息。儘管 `jq` 和 shell 引用提供了一定保護,但如果 JSON 回應本身不可信或被惡意篡改,這些值仍可能被用於注入惡意資料。建議對這些提取的值進行額外的驗證或淨化(例如,確保 `release_id` 是整數,`release_tag` 和 `release_name` 符合預期模式),尤其是在將它們用於關鍵操作或日誌記錄之前。", + "reason": "false positive" + }, + { + "level": "info", + "role": "Maya", + "location": "entrypoint.sh:54", + "suggestion": "對於 `GITEA_SERVER_URL` 參數,除了檢查是否為空或 'null',建議增加基本的 URL 格式驗證,以確保其為有效的 URL 格式,避免 `curl` 在無效 URL 時產生非預期的行為。", + "reason": "false positive" + }, + { + "level": "info", + "role": "Maya", + "location": "entrypoint.sh:55", + "suggestion": "對於 `GITEA_REPOSITORY` 參數,建議增加格式驗證,確保其符合 `owner/repo` 的預期格式,避免因格式錯誤導致 API 請求失敗。", + "reason": "false positive" + } +] diff --git a/.gitea/ai-review/findings.json b/.gitea/ai-review/findings.json index e4c5001..afecb28 100644 --- a/.gitea/ai-review/findings.json +++ b/.gitea/ai-review/findings.json @@ -2,127 +2,8 @@ { "level": "critical", "role": "Zara", - "location": "entrypoint.sh:91", - "suggestion": "目前的 `curl` 呼叫 (`release_json=\"$(curl -fsS \"${auth_header[@]}\" \"$release_api_url\")\"`) 沒有處理 Gitea API 的分頁機制。Gitea 的發布 API (`/api/v1/repos/{owner}/{repo}/releases`) 通常會限制單次請求返回的發布數量(例如,預設可能只返回 30 個)。這會導致腳本無法取得所有發布資訊,進而無法正確計算總發布數量 (`release_count`),也無法刪除超出第一頁限制的舊版本成品。這是一個嚴重的正確性問題,會導致清理功能失效。建議修改腳本,透過迴圈多次呼叫 API,每次增加 `page` 參數,並將所有頁面的發布資訊合併成一個完整的 JSON 陣列,直到 API 返回空列表為止。這將確保腳本能夠全面且正確地執行清理任務。", - "is_new": true - }, - { - "level": "critical", - "role": "Rex", - "location": "Dockerfile:4", - "suggestion": "移除 `apk add` 命令中的 `--no-check-certificate` 旗標。禁用憑證檢查會使 Docker 映像檔的建置過程容易受到中間人攻擊,導致惡意套件注入。請確保套件來源的信任鏈完整性。", - "is_new": true - }, - { - "level": "critical", - "role": "Aria", - "location": "a/Dockerfile", - "suggestion": "Dockerfile 檔案結尾應包含一個換行符,以符合 POSIX 規範並避免某些工具處理時發生問題。", - "is_new": true - }, - { - "level": "critical", - "role": "Aria", - "location": "a/entrypoint.sh", - "suggestion": "Shell 腳本檔案結尾應包含一個換行符,以符合 POSIX 規範並避免某些工具處理時發生問題。", - "is_new": true - }, - { - "level": "critical", - "role": "Maya", - "location": "entrypoint.sh", - "suggestion": "此腳本缺少單元測試。建議引入一個 shell 腳本測試框架(例如 `bats` 或 `shunit2`),並為 `is_empty_or_null`、`require_value`、`require_integer` 等輔助函數以及主要邏輯流程編寫單元測試,以確保其行為符合預期。", - "is_new": true - }, - { - "level": "critical", - "role": "Maya", - "location": "entrypoint.sh", - "suggestion": "此腳本與外部 Gitea API 互動,但缺少整合測試。建議建立整合測試,使用模擬 Gitea 伺服器(或測試環境)來驗證腳本的端到端流程,包括成功刪除、錯誤處理(例如無效的 RUNNER_TOKEN、API 錯誤響應)以及邊界條件。", - "is_new": true - }, - { - "level": "warning", - "role": "Rex", - "location": "entrypoint.sh:100", - "suggestion": "環境變數 `GITEA_SERVER_URL` 被直接用於 `curl` 命令中,以構建 API 請求。如果此變數可被攻擊者控制,可能導致伺服器端請求偽造 (SSRF) 漏洞,使程式向任意內部或外部主機發送請求。建議對 `GITEA_SERVER_URL` 進行嚴格的驗證,確保其指向預期且受信任的 Gitea 實例,例如使用白名單限制允許的網域或 IP 範圍。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "a/Dockerfile:6", - "suggestion": "`COPY` 指令的縮排不一致,建議保持統一的縮排風格(例如,與 `RUN` 指令對齊),以提高程式碼可讀性。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "a/entrypoint.sh", - "suggestion": "腳本中存在不一致的縮排風格,建議統一使用 2 或 4 個空格進行縮排,以提高程式碼可讀性。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "a/entrypoint.sh:4-20", - "suggestion": "參數檢查和錯誤處理邏輯重複且分散,建議將常見的驗證和日誌輸出邏輯抽象為輔助函數,以提高程式碼的模組化和可維護性。", - "is_new": true - }, - { - "level": "warning", - "role": "Aria", - "location": "a/entrypoint.sh", - "suggestion": "腳本中所有變數都使用大寫命名,這可能導致環境變數與腳本內部變數難以區分。建議對腳本內部使用的變數採用小寫加底線 (snake_case) 命名,而環境變數則保持大寫,以增強命名語義的清晰度。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "entrypoint.sh:75", - "suggestion": "在取得成品資訊的 `curl` 請求中,雖然使用了 `-fsS`,但並未明確檢查 HTTP 狀態碼。如果 Gitea API 返回 401/403 等認證或權限錯誤,腳本可能會因為 `jq` 處理空或錯誤 JSON 而失敗,但錯誤訊息不夠明確。建議在 `curl` 請求後檢查 HTTP 狀態碼,特別是對於認證相關的錯誤,提供更清晰的錯誤提示。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "entrypoint.sh:58", - "suggestion": "對於 `KEEP_COUNT` 參數,雖然 `require_integer` 確保了非負整數,但建議在整合測試中特別包含 `KEEP_COUNT=0`(應刪除所有成品)和 `KEEP_COUNT=1`(應保留最新一個成品)的測試案例,以確保這些邊界條件下的刪除邏輯正確無誤。", - "is_new": true - }, - { - "level": "warning", - "role": "Maya", - "location": "entrypoint.sh:75, 76, 100", - "suggestion": "腳本中有多處 `curl` 和 `jq` 的調用,雖然 `set -Eeuo pipefail` 有助於錯誤處理,但對於網路瞬時錯誤或 API 服務不穩定,腳本會直接退出。建議考慮為 `curl` 請求添加重試機制,並為 `curl` 和 `jq` 的失敗提供更具體的錯誤處理邏輯,例如捕獲錯誤並輸出詳細訊息,而不是僅依賴 `pipefail`。", - "is_new": true - }, - { - "level": "info", - "role": "Leo", - "location": "entrypoint.sh", - "suggestion": "雖然程式碼的可讀性已大幅提升,但考慮為新引入的輔助函式(如 `separator`, `section`, `info`, `success`, `warn`, `fail`, `is_empty_or_null`, `require_value`, `require_integer`)添加簡要的註解,說明其用途,這將有助於新開發者快速理解。", - "is_new": true - }, - { - "level": "info", - "role": "Rex", - "location": "entrypoint.sh:116", - "suggestion": "從 JSON 回應中提取的值(如 `release_id`、`release_tag`、`release_name`)被用於構建 URL 和日誌訊息。儘管 `jq` 和 shell 引用提供了一定保護,但如果 JSON 回應本身不可信或被惡意篡改,這些值仍可能被用於注入惡意資料。建議對這些提取的值進行額外的驗證或淨化(例如,確保 `release_id` 是整數,`release_tag` 和 `release_name` 符合預期模式),尤其是在將它們用於關鍵操作或日誌記錄之前。", - "is_new": true - }, - { - "level": "info", - "role": "Maya", - "location": "entrypoint.sh:54", - "suggestion": "對於 `GITEA_SERVER_URL` 參數,除了檢查是否為空或 'null',建議增加基本的 URL 格式驗證,以確保其為有效的 URL 格式,避免 `curl` 在無效 URL 時產生非預期的行為。", - "is_new": true - }, - { - "level": "info", - "role": "Maya", - "location": "entrypoint.sh:55", - "suggestion": "對於 `GITEA_REPOSITORY` 參數,建議增加格式驗證,確保其符合 `owner/repo` 的預期格式,避免因格式錯誤導致 API 請求失敗。", + "location": "entrypoint.sh:77-91", + "suggestion": "`curl` only fetched the first releases page. Gitea paginates this API, so the cleanup can miss older releases beyond page 1 and leave stale artifacts behind. Fetch pages until an empty page is returned, merge the results, and then sort/filter the full list.", "is_new": true } ] diff --git a/entrypoint.sh b/entrypoint.sh index 6c4c9fb..b7599b8 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -74,7 +74,21 @@ fi section "取得成品資訊" info "GET $release_api_url" -release_json="$(curl -fsS "${auth_header[@]}" "$release_api_url")" +release_json='[]' +page=1 + +while :; do + page_url="$release_api_url?page=$page" + page_json="$(curl -fsS "${auth_header[@]}" "$page_url")" + + if [ "$(jq 'length' <<<"$page_json")" -eq 0 ]; then + break + fi + + release_json="$(jq -s 'add' <<<"$release_json"$'\n'"$page_json")" + page=$((page + 1)) +done + release_json="$(jq -e 'sort_by(.created_at) | reverse' <<<"$release_json")" release_count="$(jq 'length' <<<"$release_json")" diff --git a/tests/entrypoint_pagination.sh b/tests/entrypoint_pagination.sh new file mode 100644 index 0000000..a13ebf3 --- /dev/null +++ b/tests/entrypoint_pagination.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -Eeuo pipefail + +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +curl_log="$tmpdir/curl.log" +bin_dir="$tmpdir/bin" +mkdir -p "$bin_dir" + +cat >"$bin_dir/curl" <<'EOF' +#!/usr/bin/env bash +set -Eeuo pipefail + +log_file="${CURL_LOG:?}" +printf '%s\n' "$*" >>"$log_file" + +last_arg="${!#}" + +if [[ " $* " == *" -X DELETE "* ]]; then + printf '204' + exit 0 +fi + +case "$last_arg" in + *page=1) + printf '%s' '[{"id":4,"tag_name":"v4","name":"release-4","created_at":"2024-04-01T00:00:00Z"},{"id":3,"tag_name":"v3","name":"release-3","created_at":"2024-03-01T00:00:00Z"}]' + ;; + *page=2) + printf '%s' '[{"id":2,"tag_name":"v2","name":"release-2","created_at":"2024-02-01T00:00:00Z"},{"id":1,"tag_name":"v1","name":"release-1","created_at":"2024-01-01T00:00:00Z"}]' + ;; + *page=3) + printf '%s' '[]' + ;; + *) + printf 'unexpected request: %s\n' "$last_arg" >&2 + exit 1 + ;; +esac +EOF +chmod +x "$bin_dir/curl" + +cat >"$bin_dir/jq.py" <<'PY' +#!/usr/bin/env python3 +import json +import re +import sys + +expr = "" +flags = set() +args = sys.argv[1:] + +while args: + current = args.pop(0) + if current in {"-e", "-c", "-r", "-s"}: + flags.add(current) + continue + if current == "--": + if not args: + raise SystemExit("missing jq expression") + expr = args.pop(0) + break + if current.startswith("-"): + flags.add(current) + continue + expr = current + break + +raw = sys.stdin.read() + +def dump(value): + if "-r" in flags and isinstance(value, (str, int, float)) and not isinstance(value, bool): + sys.stdout.write(str(value)) + else: + sys.stdout.write(json.dumps(value, separators=(",", ":"))) + +if expr == "length": + data = json.loads(raw or "null") + print(len(data)) + raise SystemExit(0) + +if expr == "add": + arrays = [json.loads(line) for line in raw.splitlines() if line.strip()] + merged = [] + for item in arrays: + merged.extend(item) + dump(merged) + raise SystemExit(0) + +if expr == "sort_by(.created_at) | reverse": + data = json.loads(raw or "[]") + dump(sorted(data, key=lambda item: item["created_at"], reverse=True)) + raise SystemExit(0) + +match = re.fullmatch(r"\.\[(\d+):\]", expr) +if match: + data = json.loads(raw or "[]") + dump(data[int(match.group(1)):]) + raise SystemExit(0) + +if expr == ".[]": + data = json.loads(raw or "[]") + for item in data: + dump(item) + sys.stdout.write("\n") + raise SystemExit(0) + +match = re.fullmatch(r"\.(id|tag_name|name)", expr) +if match: + data = json.loads(raw or "null") + value = data.get(match.group(1)) + dump(value) + raise SystemExit(0) + +raise SystemExit(f"unsupported jq expression: {expr}") +PY +chmod +x "$bin_dir/jq.py" + +cat >"$bin_dir/jq" <<'EOF' +#!/usr/bin/env bash +set -Eeuo pipefail +exec python3 "$0.py" "$@" +EOF +chmod +x "$bin_dir/jq" + +PATH="$bin_dir:$PATH" \ +CURL_LOG="$curl_log" \ +GITEA_SERVER_URL="https://gitea.example.test" \ +GITEA_REPOSITORY="owner/repo" \ +KEEP_COUNT="2" \ +RUNNER_TOKEN="" \ +bash ./entrypoint.sh >/dev/null + +grep -q 'page=1' "$curl_log" +grep -q 'page=2' "$curl_log" +grep -q 'page=3' "$curl_log" +grep -q '/releases/2' "$curl_log" +grep -q '/releases/1' "$curl_log" + +if grep -q '/releases/3' "$curl_log" || grep -q '/releases/4' "$curl_log"; then + printf 'unexpected deletion request in %s\n' "$curl_log" >&2 + exit 1 +fi