From 3fe643c00375fc3be29e19b22c487dca432bca21 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 17 Jul 2026 18:54:00 +0800 Subject: [PATCH 01/79] =?UTF-8?q?docs(doc-funcs):=20=E8=A3=9C=E9=BD=8A=20a?= =?UTF-8?q?ction=20=E8=88=87=20workflow=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yaml | 47 ++++++++++++++++ .gitea/workflows/readme.md | 41 ++++++++++++++ action.yml | 50 ++++++++--------- readme.md | 108 ++++++++++++++++++------------------- src/index.js | 12 ++++- 5 files changed, 178 insertions(+), 80 deletions(-) create mode 100644 .gitea/workflows/readme.md diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 05a4103..e973722 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,45 +1,92 @@ +# ============================================================================ +# 用途:在 pull request 針對 develop 分支開啟或同步時,分別以 Antigravity、Codex、Claude 環境執行本機 AI Code Review action。 +# 更新時間:2026/07/17 18:49:58 +# ============================================================================ +# Workflow 顯示名稱。 name: CI +# Workflow 觸發條件區塊。 on: + # 以 pull request 事件觸發。 pull_request: + # 限定 pull request 目標分支。 branches: + # 僅在目標分支為 develop 時執行。 - develop + # 限定 pull request 開啟與同步更新時執行。 types: [opened, synchronize] +# Job 定義區塊。 jobs: + # Antigravity 工具環境測試 job。 test-antigravity: + # Job 在 workflow UI 顯示的名稱。 name: TEST (Antigravity) + # 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。 runs-on: ubuntu + # Job 執行步驟。 steps: + # 取得存取庫內容供後續 action 使用。 - name: 取得存取庫資訊 + # 使用呼叫端 vars 指定的 checkout action 版本。 uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} + # 安裝 Antigravity 工具環境。 - name: 安裝工具 + # 使用 Gitea composite action 安裝 Antigravity,版本由 vars 指定。 uses: https://gitea.jsc.idv.tw/composite-actions/setup-antigravity@${{ vars.ACTION_SETUP_ANTIGRAVITY_VERSION }} + # 執行目前存取庫的 AI Code Review action。 - name: 程式碼審查 + # 以目前存取庫根目錄的 action.yml 作為 action 來源。 uses: ./ + # 傳入 action inputs。 with: + # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # Codex 工具環境測試 job。 test-codex: + # Job 在 workflow UI 顯示的名稱。 name: TEST (Codex) + # 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。 runs-on: ubuntu + # Job 執行步驟。 steps: + # 取得存取庫內容供後續 action 使用。 - name: 取得存取庫資訊 + # 使用呼叫端 vars 指定的 checkout action 版本。 uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} + # 安裝 Codex 工具環境。 - name: 安裝工具 + # 使用 Gitea composite action 安裝 Codex,版本由 vars 指定。 uses: https://gitea.jsc.idv.tw/composite-actions/setup-codex@${{ vars.ACTION_SETUP_CODEX_VERSION }} + # 執行目前存取庫的 AI Code Review action。 - name: 程式碼審查 + # 以目前存取庫根目錄的 action.yml 作為 action 來源。 uses: ./ + # 傳入 action inputs。 with: + # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # Claude 工具環境測試 job。 test-claude: + # Job 在 workflow UI 顯示的名稱。 name: TEST (Claude) + # 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。 runs-on: ubuntu + # Job 執行步驟。 steps: + # 取得存取庫內容供後續 action 使用。 - name: 取得存取庫資訊 + # 使用呼叫端 vars 指定的 checkout action 版本。 uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} + # 安裝 Claude 工具環境。 - name: 安裝工具 + # 使用 Gitea composite action 安裝 Claude,版本由 vars 指定。 uses: https://gitea.jsc.idv.tw/composite-actions/setup-claude@${{ vars.ACTION_SETUP_CLAUDE_VERSION }} + # 執行目前存取庫的 AI Code Review action。 - name: 程式碼審查 + # 以目前存取庫根目錄的 action.yml 作為 action 來源。 uses: ./ + # 傳入 action inputs。 with: + # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitea/workflows/readme.md b/.gitea/workflows/readme.md new file mode 100644 index 0000000..6b2e3c8 --- /dev/null +++ b/.gitea/workflows/readme.md @@ -0,0 +1,41 @@ +# Workflow 文件草稿 + +更新時間:2026/07/17 18:49:58 + +## Workflow 總覽 + +| 名稱 | 檔案位置 | 用途 | 觸發條件 | +| --- | --- | --- | --- | +| CI | `.gitea/workflows/ci.yaml` | 在 PR 事件中分別以 Antigravity、Codex、Claude 工具環境執行本存取庫的 AI Code Review action,驗證 action 可在不同 AI 工具環境下運作。 | `pull_request` 目標分支為 `develop`,事件類型為 `opened` 或 `synchronize`。 | + +## CI + +- Workflow 名稱:`CI` +- 檔案位置:`.gitea/workflows/ci.yaml` +- 用途:針對送往 `develop` 的 pull request 執行三組測試 job,分別安裝 Antigravity、Codex、Claude 環境後呼叫 `uses: ./` 執行目前 action。 +- 觸發條件:`pull_request.branches` 為 `develop`;`pull_request.types` 為 `opened`、`synchronize`。 + +## Job + +| Job ID | 顯示名稱 | Runner | 主要流程 | +| --- | --- | --- | --- | +| `test-antigravity` | `TEST (Antigravity)` | `ubuntu` | checkout 存取庫、安裝 Antigravity、執行本地 action。 | +| `test-codex` | `TEST (Codex)` | `ubuntu` | checkout 存取庫、安裝 Codex、執行本地 action。 | +| `test-claude` | `TEST (Claude)` | `ubuntu` | checkout 存取庫、安裝 Claude、執行本地 action。 | + +## 主要輸入 / 環境參數 + +| 名稱 | 來源 | 使用位置 | 說明 | +| --- | --- | --- | --- | +| `vars.ACTION_CHECKOUT_VERSION` | Gitea / GitHub repository 或 organization vars | `actions/checkout@...` | 指定 checkout action 版本。 | +| `vars.ACTION_SETUP_ANTIGRAVITY_VERSION` | Gitea / GitHub vars | `setup-antigravity@...` | 指定 Antigravity 安裝 action 版本。 | +| `vars.ACTION_SETUP_CODEX_VERSION` | Gitea / GitHub vars | `setup-codex@...` | 指定 Codex 安裝 action 版本。 | +| `vars.ACTION_SETUP_CLAUDE_VERSION` | Gitea / GitHub vars | `setup-claude@...` | 指定 Claude 安裝 action 版本。 | +| `secrets.GITHUB_TOKEN` | Gitea / GitHub secrets | 本地 action input `token` | 提供 action 呼叫 Gitea API 留言與寫回 findings 所需 token。 | + +## 注意事項 + +- `.gitea/workflows/readme.md` 目前不存在;本檔為新增 workflow README 的草稿。 +- `runs-on: ubuntu` 是否符合實際 Gitea runner 標籤需人工確認。 +- `vars.*` 與 `secrets.GITHUB_TOKEN` 必須由呼叫端環境提供,否則 checkout、工具安裝或程式碼審查步驟可能失敗。 +- 三個 job 均以 `uses: ./` 呼叫目前存取庫根目錄的 `action.yml`;因此 `action.yml` 的 `runs.main` 需保持可被 runner 直接執行。 diff --git a/action.yml b/action.yml index c952c50..08eb38f 100644 --- a/action.yml +++ b/action.yml @@ -1,53 +1,53 @@ -# ===================================================== -# 用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings -# 更新時間: 2026/07/17 16:49:21 -# ===================================================== +# ============================================================================ +# 用途:定義 AI Code Review Node action 的名稱、輸入參數與 Node.js 24 進入點,供 Gitea / GitHub workflow 以 uses 引用。 +# 更新時間:2026/07/17 18:49:58 +# ============================================================================ # Gitea / GitHub node action 的 manifest(action.yml): # 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs), # 供呼叫端 workflow 以 `uses:` 引用;runner 讀取此檔後以 node24 執行 src/index.js。 -# action 顯示名稱:呼叫端 workflow log 與 marketplace 列表上看到的名稱 +# action 顯示名稱:呼叫端 workflow log 與 marketplace 列表上看到的名稱。 name: 'AI Code Review' # action 用途說明:多角色 AI code review 流程(攻擊方找問題、防守方裁決誤報), -# 審查結果會留言到 PR 並保存 findings(.gitea/ai-review/findings/) +# 審查結果會留言到 PR 並保存 findings(.gitea/ai-review/findings/)。 description: 'AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings' -# action 作者資訊(僅供辨識,不影響執行) +# action 作者資訊(僅供辨識,不影響執行)。 author: 'Jeffery' # 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入, -# runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取 +# runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取。 inputs: - # Gitea API token:用於對 PR 留言審查結果、以及 push 審查結果檔回 repo + # Gitea API token:用於對 PR 留言審查結果、以及 push 審查結果檔回 repo。 token: # 參數用途說明:secrets/vars context 在 action 內不可用, - # 故由呼叫端 workflow 以 secrets.GITHUB_TOKEN 傳入 + # 故由呼叫端 workflow 以 secrets.GITHUB_TOKEN 傳入。 description: 'Gitea API token(PR 留言與 push findings 用;呼叫端以 secrets.GITHUB_TOKEN 傳入)' - # 必填:缺少 token 無法呼叫 Gitea API,action 無法運作 + # 必填:缺少 token 無法呼叫 Gitea API,action 無法運作。 required: true - # 指定 AI 工具使用的模型名稱 + # 指定 AI 工具使用的模型名稱。 model: - # 參數用途說明:留空表示使用各 AI 工具自身的預設模型 + # 參數用途說明:留空表示使用各 AI 工具自身的預設模型。 description: '指定 AI 工具使用的模型(空值=各工具預設)' - # 選填:未指定時採用預設值 + # 選填:未指定時採用預設值。 required: false - # 預設為空字串,代表不覆寫各工具的預設模型 + # 預設為空字串,代表不覆寫各工具的預設模型。 default: '' - # 建問題模式開關:是否把審查保留的問題另建 issue 追蹤 + # 建問題模式開關:是否把審查保留的問題另建 issue 追蹤。 create-issue: - # 參數用途說明:字串 'true' 時建立 issue(標題=PR 標題、描述=PR 描述、AI 挑標籤) + # 參數用途說明:字串 'true' 時建立 issue(標題=PR 標題、描述=PR 描述、AI 挑標籤), # 並逐條留言問題明細,findings 檔不進版控、收尾只 commit exclusions.json; - # 預設 'false' 走原流程(findings 檔與 exclusions.json 一併 commit 回 PR 來源分支) + # 預設 'false' 走原流程(findings 檔與 exclusions.json 一併 commit 回 PR 來源分支)。 description: '是否將問題建到存取庫的問題追蹤(true 時建立 issue 逐條留言問題明細,最後只 commit exclusions.json;預設 false 走原流程)' - # 選填:未指定時採用預設值 + # 選填:未指定時採用預設值。 required: false - # 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用) + # 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用)。 default: 'false' -# 執行方式區塊:宣告本 action 為 node action 及其進入點 +# 執行方式區塊:宣告本 action 為 node action 及其進入點。 runs: - # 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite) + # 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite)。 using: 'node24' - # 主程式進入點:直接指向 src/index.js(entry point) + # 主程式進入點:直接指向 src/index.js(entry point)。 # 主程式為零外部相依(package.json 無 dependencies,src 僅 require Node 內建模組與本地 lib), # runner 不會自動 npm install,零相依時依 node action 慣例 main 直接指向 src/index.js 即正確; - # 日後若新增外部相依,需改以 @vercel/ncc 打包(package.json 已備有 build script) - # 並將 main 改指 dist/index.js、把 dist/ commit 進 repo + # 日後若新增外部相依,需改以 @vercel/ncc 打包(package.json 已備有 build script), + # 並將 main 改指 dist/index.js、把 dist/ commit 進 repo。 main: 'src/index.js' diff --git a/readme.md b/readme.md index 760617f..244c223 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # AI Code Review -> 更新時間:2026/07/17 16:49:21 +> 更新時間:2026/07/17 18:49:58 AI 多角色 code review 的 Gitea **node action**(`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]`/`[failure]`)供下次觸發快速回報。 @@ -56,19 +56,19 @@ flowchart TD | 專案名稱 | 專案描述 | | --- | --- | -| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | Gitea node action:提供台北時區日誌工具、runner 上下文載入、git diff/commit 操作、Gitea REST API 客戶端(留言/review/issue/標籤)、AI CLI 工具偵測與 sub agent 執行、角色提示載入、固定留言模板,以及多角色審查編排(攻擊方找問題、防守方裁決、findings 保存、誤判回寫、建問題模式) | +| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | Gitea node action:提供台北時區日誌工具、runner 上下文載入、git diff/commit 操作、Gitea REST API 客戶端(留言/review/issue/標籤)、AI CLI 工具偵測與 sub agent 執行、角色提示載入、固定留言模板,以及多角色審查編排(攻擊方找問題、防守方裁決、findings 保存、誤判回寫、建問題模式) | ### 參考專案表 | 專案名稱 | 參考專案列表 | | --- | --- | -| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | 無 | +| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | 無 | ### NuGet 套件表 | 專案名稱 | NuGet 套件列表 | | --- | --- | -| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | 無 | +| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | 無 | ## 功能列表 @@ -76,56 +76,56 @@ flowchart TD | 功能名稱 | 功能描述 | | --- | --- | -| [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L20) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) | -| [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L41) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) | -| [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | -| [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) | -| [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | -| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | -| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | -| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | -| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | -| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | -| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | -| [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | -| [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | -| [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | -| [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) | -| [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) | -| [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) | -| [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) | -| [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) | -| [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) | -| [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) | -| [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) | -| [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) | -| [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) | -| [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) | -| [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) | -| [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) | -| [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) | -| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L88) | [產生步驟 2 審查工具留言](#templatestoolcomment) | -| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L132) | [產生步驟 3 變更摘要留言](#templatesdiffcomment) | -| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L173) | [產生步驟 4/6 角色登場留言](#templatesrolescomment) | -| [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) | -| [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) | -| [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) | -| [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) | -| [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) | -| [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) | -| [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) | -| [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) | -| [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) | -| [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) | -| [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) | -| [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) | -| [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) | -| [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) | -| [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) | -| [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) | -| [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) | -| [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) | -| [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) | +| [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L20) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) | +| [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L41) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) | +| [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | +| [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) | +| [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | +| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | +| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | +| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | +| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | +| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | +| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | +| [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | +| [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | +| [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | +| [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) | +| [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) | +| [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) | +| [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) | +| [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) | +| [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) | +| [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) | +| [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) | +| [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) | +| [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) | +| [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) | +| [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) | +| [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) | +| [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) | +| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L88) | [產生步驟 2 審查工具留言](#templatestoolcomment) | +| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L132) | [產生步驟 3 變更摘要留言](#templatesdiffcomment) | +| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L173) | [產生步驟 4/6 角色登場留言](#templatesrolescomment) | +| [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) | +| [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) | +| [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) | +| [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) | +| [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) | +| [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) | +| [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) | +| [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) | +| [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) | +| [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) | +| [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) | +| [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) | +| [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) | +| [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) | +| [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) | +| [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) | +| [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) | +| [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) | +| [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) | ## 使用範例 diff --git a/src/index.js b/src/index.js index b8680b9..90ab897 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ console.log('================================================'); console.log('Action : AI Code Review'); console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings'); -console.log('更新時間: 2026/07/17 16:49:21'); +console.log('更新時間: 2026/07/17 18:49:58'); console.log('================================================'); const fs = require('fs'); @@ -169,6 +169,16 @@ async function main() { // 本回合發出的一般留言 id:步驟 8 標註過時時要跳過這些。 const currentRunCommentIds = new Set(); + /** + * 建立本回合 PR 一般留言並記錄留言 id,供後續舊留言處理排除。 + * + * @param {string} body 要發布到 PR 的 Markdown 留言內容。 + * @returns {Promise} Gitea API 建立的留言物件;至少預期包含 `id`。 + * @remarks + * 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與 + * 警告/建議彙整等一般留言。若 Gitea API 失敗,例外會往上拋出並由 + * 主流程頂層 catch 收斂。 + */ const postComment = async (body) => { const created = await gitea.createIssueComment(ctx, body); currentRunCommentIds.add(created.id); -- 2.53.0 From c4c23d45314bb260d7ec350db4f968021076ee17 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 17 Jul 2026 19:02:42 +0800 Subject: [PATCH 02/79] =?UTF-8?q?fix(gitrepo):=20=E8=A3=9C=E6=8A=93?= =?UTF-8?q?=E6=B7=BA=E5=B1=A4=20checkout=20=E7=9A=84=20base=20=E6=AD=B7?= =?UTF-8?q?=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.md | 12 ++++++------ src/lib/gitrepo.js | 48 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index 244c223..35df151 100644 --- a/readme.md +++ b/readme.md @@ -81,12 +81,12 @@ flowchart TD | [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | | [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) | | [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | -| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | -| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | -| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | -| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | -| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | -| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | +| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L79) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | +| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L100) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | +| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L136) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | +| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L157) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | +| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L175) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | +| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L213) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | | [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | | [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | | [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index 3ecc5ee..b7c2066 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -44,6 +44,25 @@ function gitTrim(cwd, ...args) { return git(cwd, ...args).trim(); } +/** + * 嘗試同步執行 git 指令,失敗時回傳 false,成功時回傳 true。 + * + * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 + * @param {...string} args - 傳給 git 的參數。 + * @returns {boolean} git 指令是否成功結束。 + * @remarks + * 使用情境:修復淺層 checkout 的歷史不足時,部分 fetch 策略可能因 runner + * 或遠端版本不同而失敗;呼叫端可依序嘗試多種策略,不讓第一個失敗中斷流程。 + */ +function tryGit(cwd, ...args) { + try { + git(cwd, ...args); + return true; + } catch { + return false; + } +} + /** * 取得目前 HEAD 最新一筆 commit 的訊息標題(commit message 第一行)。 * @@ -64,14 +83,14 @@ function latestCommitSubject(cwd) { /** * 解析 PR base 分支與目前 HEAD 的 merge-base commit SHA。 * - * 先嘗試 `git fetch origin ` 更新 base 分支資料(失敗時靜默忽略, - * 因 fetch-depth: 0 的 checkout 通常已含 base 分支,可直接沿用本地資料), - * 再以 `git merge-base origin/ HEAD` 取得共同祖先。 + * 先以 refspec 明確更新 `origin/`,再以 `git merge-base origin/ HEAD` + * 取得共同祖先。若 checkout 是淺層歷史而導致 merge-base 失敗,會補抓完整或更深的 + * base/head 歷史後重試,避免 PR workflow 因 checkout 預設深度不足而中斷。 * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} baseRef - PR 目標(base)分支名稱,例如 'master' 或 'develop';不含 'origin/' 前綴。 * @returns {string} merge-base 的 commit SHA(40 碼十六進位字串)。 - * @throws {Error} 本地不存在 origin/、或兩者無共同祖先時,`git merge-base` 失敗並拋出(fetch 失敗不會拋出)。 + * @throws {Error} 補抓歷史後仍無法取得共同祖先時,丟出含 baseRef 的明確錯誤。 * @remarks * 使用情境:AI code review 以此結果作為 diff 比較基準—— * 先 `resolveMergeBase(cwd, pr.base.ref)` 取得基準 SHA, @@ -79,12 +98,25 @@ function latestCommitSubject(cwd) { * 避免把 base 分支後續演進誤算進 diff。 */ function resolveMergeBase(cwd, baseRef) { + const remoteBase = `origin/${baseRef}`; + tryGit(cwd, 'fetch', '--no-tags', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`); try { - git(cwd, 'fetch', 'origin', baseRef); - } catch { - // fetch-depth: 0 的 checkout 通常已含 base 分支,抓不到時直接沿用本地資料。 + return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); + } catch (firstError) { + const isShallow = gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true'; + if (isShallow) { + tryGit(cwd, 'fetch', '--no-tags', '--unshallow', 'origin'); + } + tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`); + tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD'); + try { + return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); + } catch { + const error = new Error(`無法解析 origin/${baseRef} 與 HEAD 的 merge-base;請確認 checkout 有足夠歷史,或設定 checkout fetch-depth: 0。`); + error.cause = firstError; + throw error; + } } - return gitTrim(cwd, 'merge-base', `origin/${baseRef}`, 'HEAD'); } /** -- 2.53.0 From ccbe615c2a424e0bcb4ca53a4dadf35058e8d7f6 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Fri, 17 Jul 2026 11:03:14 +0000 Subject: [PATCH 03/79] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/findings/2026-07-17-19:03:13.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-17-19:03:13.json diff --git a/.gitea/ai-review/findings/2026-07-17-19:03:13.json b/.gitea/ai-review/findings/2026-07-17-19:03:13.json new file mode 100644 index 0000000..c70d864 --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-17-19:03:13.json @@ -0,0 +1,12 @@ +{ + "generatedAt": "2026/07/17 19:03:13", + "commitSha": "c4c23d45314bb260d7ec350db4f968021076ee17", + "prNumber": 4, + "tool": { + "name": "codex", + "version": "codex-cli 0.144.5", + "model": "(工具預設)" + }, + "findings": [], + "excluded": [] +} -- 2.53.0 From d751cee69d735efcc7aa96c36a69c3b798398985 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 09:46:12 +0800 Subject: [PATCH 04/79] =?UTF-8?q?refactor(=E5=AF=A9=E6=9F=A5=E6=B5=81?= =?UTF-8?q?=E7=A8=8B):=20=E5=B0=87=E8=88=8A=E7=95=99=E8=A8=80=E6=A8=99?= =?UTF-8?q?=E8=A8=98=E7=A7=BB=E5=88=B0=E5=81=B5=E6=B8=AC=20AI=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E4=B9=8B=E5=89=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 52 +++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index 90ab897..b4c86d0 100644 --- a/src/index.js +++ b/src/index.js @@ -42,9 +42,9 @@ const BOT_COMMIT_PREFIX = 'chore: update ai-review findings [ai-review-bot]'; * @param {Array} params.excluded - 被裁決為誤報而排除的問題清單。 * @returns {string} findings JSON 檔相對於 repo 根目錄的路徑(例如 `.gitea/ai-review/findings/xxx.json`)。 * @remarks - * 使用情境:`main()` 步驟 7 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果, + * 使用情境:`main()` 步驟 8 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果, * 再將回傳的相對路徑交給 `commitFindings` commit 並 push 回 PR 來源分支; - * 另在步驟 3 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。 + * 另在步驟 4 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。 * 本函式無 try/catch,檔案系統錯誤會往上拋出,由 `main().catch` 以 exit code 1 收場。 */ function saveFindings({ cwd, ctx, tool, kept, excluded }) { @@ -61,7 +61,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { }; fs.writeFileSync(findingsPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); const relativePath = path.relative(cwd, findingsPath); - log('步驟7', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`); + log('步驟8', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`); return relativePath; } @@ -88,7 +88,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { * @remarks * 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、 * 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json; - * 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 3 判定無可審查變更且非建問題模式時, + * 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 4 判定無可審查變更且非建問題模式時, * 也會以 result: 'success' 提交空 findings。 * 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。 */ @@ -119,13 +119,13 @@ function commitFindings({ cwd, ctx, files, result }) { * * 流程概要: * 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審; - * 2. 偵測 AI 工具(antigravity/codex/claude)並留言; - * 3. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, + * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言); + * 3. 偵測 AI 工具(antigravity/codex/claude)並留言; + * 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, * 一般模式 commit success、建問題模式略過 commit,回傳 0); - * 4–5. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題; - * 6–7. 防守方登場留言、裁決誤報後排序並保存 findings JSON, + * 5–6. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題; + * 7–8. 防守方登場留言、裁決誤報後排序並保存 findings JSON, * 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json; - * 8. 將 PR 既有舊留言標記為解決(跳過本回合留言); * 9. 嚴重問題逐條掛在程式碼行上留言; * 10. 警告+建議彙整為單一表格留言; * 建問題模式(input: create-issue):保留問題另建 issue(createIssueWithFindings)逐條留言明細; @@ -167,7 +167,7 @@ async function main() { return 1; } - // 本回合發出的一般留言 id:步驟 8 標註過時時要跳過這些。 + // 本回合發出的一般留言 id:步驟 2 標註過時時要跳過這些。 const currentRunCommentIds = new Set(); /** * 建立本回合 PR 一般留言並記錄留言 id,供後續舊留言處理排除。 @@ -185,13 +185,18 @@ async function main() { return created; }; - // ── 步驟 2:偵測 AI agent 工具並留言 ────────────────────────────────── + // ── 步驟 2:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── + // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時; + // 此時本回合尚未發出任何留言(currentRunCommentIds 為空),之後發出的留言自然不受影響。 + await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + + // ── 步驟 3:偵測 AI agent 工具並留言 ────────────────────────────────── const tool = agents.detectTool(); if (!tool) { - log('步驟2', 'ERR', '找不到可用的 AI 工具(antigravity/codex/claude)。'); + log('步驟3', 'ERR', '找不到可用的 AI 工具(antigravity/codex/claude)。'); return 1; } - log('步驟2', 'INF', `選用工具:${tool.name}(${tool.version})。`); + log('步驟3', 'INF', `選用工具:${tool.name}(${tool.version})。`); const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`; await postComment( templates.toolComment({ @@ -204,13 +209,13 @@ async function main() { }), ); - // ── 步驟 3:讀取 .reviewignore、整理 git diff 並留言 ─────────────────── + // ── 步驟 4:讀取 .reviewignore、整理 git diff 並留言 ─────────────────── const ignores = review.loadReviewIgnore(cwd); const base = gitrepo.resolveMergeBase(cwd, ctx.baseRef); const allFiles = gitrepo.changedFiles(cwd, base); const files = allFiles.filter((file) => !review.isIgnored(file, ignores)); const ignoredCount = allFiles.length - files.length; - log('步驟3', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); + log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); if (files.length === 0) { // 沒有可審查的變更:留言說明、保存空 findings、以 success 收場。 @@ -229,20 +234,20 @@ async function main() { await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); await postComment(templates.diffComment(diffRows, ignoredCount)); - // ── 步驟 4:攻擊方角色登場留言 ───────────────────────────────────────── + // ── 步驟 5:攻擊方角色登場留言 ───────────────────────────────────────── const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles')); const attackers = attackersOf(roles); const defenders = defendersOf(roles); - log('步驟4', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); + log('步驟5', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); await postComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers })); - // ── 步驟 5:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── + // ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); - // ── 步驟 6:防守方角色登場留言 ───────────────────────────────────────── + // ── 步驟 7:防守方角色登場留言 ───────────────────────────────────────── await postComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); - // ── 步驟 7:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── + // ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); review.sortFindings(kept); const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded }); @@ -250,13 +255,10 @@ async function main() { // 誤判/重複的問題附加到 exclusions.json(之後與審查結果一起 commit)。 const exclusionsChanged = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber }); - // ── 步驟 7(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─ + // ── 步驟 8(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─ const severe = kept.filter((finding) => finding.severity === '嚴重'); const others = kept.filter((finding) => finding.severity !== '嚴重'); - log('步驟7', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); - - // ── 步驟 8:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── - await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); // ── 步驟 9:嚴重問題逐條掛在程式碼行上留言(開發者可回覆)────────────── if (severe.length > 0) { -- 2.53.0 From 8662e8ca801c3dbf7f74ed7e758a7a55176735b8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 09:46:12 +0800 Subject: [PATCH 05/79] =?UTF-8?q?docs(=E5=AF=A9=E6=9F=A5=E6=AD=A5=E9=A9=9F?= =?UTF-8?q?=E7=B7=A8=E8=99=9F):=20=E4=BE=9D=E6=96=B0=E9=A0=86=E5=BA=8F?= =?UTF-8?q?=E9=87=8D=E7=B7=A8=E6=AD=A5=E9=A9=9F=E7=B7=A8=E8=99=9F=E8=88=87?= =?UTF-8?q?=E8=AA=AA=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.md | 46 +++++++++++------------ src/lib/agents.js | 2 +- src/lib/gitea.js | 8 ++-- src/lib/review.js | 87 ++++++++++++++++++++++---------------------- src/lib/roles.js | 2 +- src/lib/templates.js | 26 ++++++------- 6 files changed, 86 insertions(+), 85 deletions(-) diff --git a/readme.md b/readme.md index 35df151..1d9725d 100644 --- a/readme.md +++ b/readme.md @@ -38,13 +38,13 @@ jobs: ```mermaid flowchart TD S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure] - S1 -->|未命中| S2[2 偵測 AI 工具並留言] - S2 --> S3[3 讀 .reviewignore 整理 diff 並留言] - S3 --> S4[4 攻擊方登場留言] - S4 --> S5[5 攻擊方 sub agent 並行找問題] - S5 --> S6[6 防守方登場留言] - S6 --> S7[7 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json] - S7 --> S8[8 舊留言標記解決] + S1 -->|未命中| S2[2 舊留言標記解決] + S2 --> S3[3 偵測 AI 工具並留言] + S3 --> S4[4 讀 .reviewignore 整理 diff 並留言] + S4 --> S5[5 攻擊方登場留言] + S5 --> S6[6 攻擊方 sub agent 並行找問題] + S6 --> S7[7 防守方登場留言] + S7 --> S8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json] S8 --> S9[9 嚴重問題逐條掛行留言] S9 --> S10[10 警告+建議彙整表格留言] S10 --> E1[收尾 commit/push + exit code] @@ -104,9 +104,9 @@ flowchart TD | [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) | | [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) | | [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) | -| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L88) | [產生步驟 2 審查工具留言](#templatestoolcomment) | -| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L132) | [產生步驟 3 變更摘要留言](#templatesdiffcomment) | -| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L173) | [產生步驟 4/6 角色登場留言](#templatesrolescomment) | +| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L88) | [產生步驟 3 審查工具留言](#templatestoolcomment) | +| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L132) | [產生步驟 4 變更摘要留言](#templatesdiffcomment) | +| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L173) | [產生步驟 5/7 角色登場留言](#templatesrolescomment) | | [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) | | [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) | | [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) | @@ -168,8 +168,8 @@ taipeiFromIso(''); // '—' ```js const { log } = require('./src/lib/log'); -log('步驟3', 'INF', '變更檔案 5 個,送審 3 個。'); -// [2026/07/17 16:46:13][步驟3][INF]: 變更檔案 5 個,送審 3 個。 +log('步驟4', 'INF', '變更檔案 5 個,送審 3 個。'); +// [2026/07/17 16:46:13][步驟4][INF]: 變更檔案 5 個,送審 3 個。 ``` @@ -246,7 +246,7 @@ const committed = gitrepo.commitAndPushFindings(cwd, { ### gitea.whoAmI -取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 8 以 `login` 比對留言作者辨識本 action 發過的留言。 +取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 2 以 `login` 比對留言作者辨識本 action 發過的留言。 ```js const gitea = require('./src/lib/gitea'); @@ -269,7 +269,7 @@ await gitea.createCommentOnIssue(ctx, issue.number, '🔴 嚴重|...'); ```js const created = await gitea.createIssueComment(ctx, '## 📋 變更摘要 ...'); -// created.id 記入本回合留言集合,步驟 8 標註過時時跳過 +// created.id 記入本回合留言集合,步驟 2 標註過時時跳過 ``` @@ -294,7 +294,7 @@ const issue = await gitea.createIssue(ctx, { title: 'PR 標題', body: '…', la ### gitea.listIssueComments -列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 8 據此找出 bot 舊留言標註〔已過時〕。 +列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 2 據此找出 bot 舊留言標註〔已過時〕。 ```js const comments = await gitea.listIssueComments(ctx); @@ -303,7 +303,7 @@ const comments = await gitea.listIssueComments(ctx); ### gitea.editIssueComment -以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 8 用來替舊留言加上〔已過時〕前綴。 +以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 2 用來替舊留言加上〔已過時〕前綴。 ```js await gitea.editIssueComment(ctx, comment.id, `> 〔已過時〕…\n\n${comment.body}`); @@ -323,7 +323,7 @@ await gitea.createReview(ctx, '## 🔴 嚴重問題(共 2 條)…', [ ### gitea.listReviews -列出 PR 全部 review(自動分頁);步驟 8 據此逐一取出行內留言嘗試解決。 +列出 PR 全部 review(自動分頁);步驟 2 據此逐一取出行內留言嘗試解決。 ```js const reviews = await gitea.listReviews(ctx); @@ -411,7 +411,7 @@ const defenders = defendersOf(roles); // [Paladin] ### templates.toolComment -產生步驟 2 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 8 辨識。 +產生步驟 3 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 2 辨識。 ```js const body = templates.toolComment({ @@ -424,7 +424,7 @@ const body = templates.toolComment({ ### templates.diffComment -產生步驟 3 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。 +產生步驟 4 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。 ```js const body = templates.diffComment(diffRows, ignoredCount); @@ -433,7 +433,7 @@ const body = templates.diffComment(diffRows, ignoredCount); ### templates.rolesComment -產生步驟 4/6 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。 +產生步驟 5/7 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。 ```js const body = templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers }); @@ -534,7 +534,7 @@ await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); ### review.runAttackers -步驟 5:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。 +步驟 6:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。 ```js const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); @@ -543,7 +543,7 @@ const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attack ### review.runDefenders -步驟 7:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。 +步驟 8:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。 ```js const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); @@ -600,7 +600,7 @@ if (ctx.createIssue && kept.length > 0) { ### review.resolveOldComments -步驟 8:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。 +步驟 2:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。 ```js await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); diff --git a/src/lib/agents.js b/src/lib/agents.js index acec87d..e7cfe73 100644 --- a/src/lib/agents.js +++ b/src/lib/agents.js @@ -47,7 +47,7 @@ const TOOLS = [ * @returns {{ name: string, buildArgs: Function, resultFrom: string, version: string } | null} * 中選工具的描述物件(TOOLS 項目加上 version 欄位);所有工具皆不可用時回傳 null。 * @remarks - * 使用情境:action 主流程(步驟 2)啟動審查前呼叫一次,取得工具描述後交給 + * 使用情境:action 主流程(步驟 3)啟動審查前呼叫一次,取得工具描述後交給 * runAgent 執行;若回傳 null,主流程會記 ERR 並以失敗收場(無工具即無法審查)。 */ function detectTool() { diff --git a/src/lib/gitea.js b/src/lib/gitea.js index 232c9db..ef71722 100644 --- a/src/lib/gitea.js +++ b/src/lib/gitea.js @@ -80,7 +80,7 @@ async function listAll(ctx, apiPath) { * @returns {Promise} Gitea 使用者物件(含 `id`、`login` 等欄位, * 依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx,例如 token 無效時 401)由底層 `api` 丟出。 - * @remarks 使用情境:action 步驟 8 先查出 bot 自己的帳號, + * @remarks 使用情境:action 步驟 2 先查出 bot 自己的帳號, * 之後比對 PR 留言的作者,辨識哪些留言是本 action 先前發出的 * (例如要將舊留言標註為已過時)。 */ @@ -178,7 +178,7 @@ function createIssue(ctx, { title, body, labels }) { * @returns {Promise>} 留言物件陣列(含 `id`、`body`、`user` 等欄位); * 無留言時為空陣列。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:步驟 8 重跑 review 前,先撈出 PR 全部留言並搭配 `whoAmI` + * @remarks 使用情境:步驟 2 重跑 review 前,先撈出 PR 全部留言並搭配 `whoAmI` * 比對作者,找出本 action(bot)先前發過的留言,以便編輯標註為已過時。 */ function listIssueComments(ctx) { @@ -237,7 +237,7 @@ function createReview(ctx, body, comments) { * @returns {Promise>} review 物件陣列(含 `id`、`user`、`body` 等欄位); * 無 review 時為空陣列。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:步驟 8 重跑 review 前,先找出 PR 上既有 review, + * @remarks 使用情境:步驟 2 重跑 review 前,先找出 PR 上既有 review, * 再以 `listReviewComments` 取出其行內留言做後續解決標記。 */ function listReviews(ctx) { @@ -280,7 +280,7 @@ function listReviewComments(ctx, reviewId) { * @param {number|string} commentId - 要標記為已解決的行內留言 id。 * @returns {Promise} 標記成功回傳 `true`;任何失敗 * (版本不支援、權限不足、留言不存在等)一律回傳 `false`,不丟出例外。 - * @remarks 使用情境:步驟 8 嘗試把舊回合的行內留言標記為已解決;若回傳 `false` + * @remarks 使用情境:步驟 2 嘗試把舊回合的行內留言標記為已解決;若回傳 `false` * (例如目標 Gitea 版本無此 API),呼叫端應停止嘗試並記 WRN * (由 `resolveOldComments` 實作此降級)。 */ diff --git a/src/lib/review.js b/src/lib/review.js index da0086e..cf504f8 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -22,7 +22,7 @@ const TOTAL_DIFF_LIMIT = 160_000; * @param {string} workspace - 工作目錄絕對路徑(`.reviewignore` 所在的 repo 根目錄)。 * @returns {string[]} 忽略用的路徑前綴陣列;檔案不存在時為空陣列。 * @remarks - * 使用情境:審查流程「步驟 3」開頭由 `src/index.js` 呼叫, + * 使用情境:審查流程「步驟 4」開頭由 `src/index.js` 呼叫, * 取得前綴清單後搭配 {@link isIgnored} 過濾 `gitrepo.changedFiles` 的結果, * 決定哪些變更檔案要納入送審。 */ @@ -46,7 +46,7 @@ function loadReviewIgnore(workspace) { * @param {string[]} prefixes - 忽略路徑前綴清單(通常來自 {@link loadReviewIgnore})。 * @returns {boolean} `true` 表示忽略、不納入審查;`false` 表示送審。 * @remarks - * 使用情境:審查流程「步驟 3」中,`src/index.js` 以 + * 使用情境:審查流程「步驟 4」中,`src/index.js` 以 * `allFiles.filter((file) => !review.isIgnored(file, ignores))` * 過濾變更檔案清單,被排除的檔案數量會反映在變更摘要留言的排除統計。 */ @@ -70,7 +70,7 @@ function isIgnored(file, prefixes) { * @returns {Array<{file: string, purpose: string, lines: number, chars: number, truncated: boolean, lastUpdated: string, diffForPrompt: string}>} * 每檔一列的 diff 資料列;`purpose` 初始為「—」,由 {@link fillPurposes} 補齊。 * @remarks - * 使用情境:審查流程「步驟 3」由 `src/index.js` 呼叫,產出的 rows 同時餵給 + * 使用情境:審查流程「步驟 4」由 `src/index.js` 呼叫,產出的 rows 同時餵給 * {@link fillPurposes}(補用途)、`templates.diffComment`(變更摘要留言)與 * {@link buildAttackPrompt}(攻擊方提示的變更內容區塊)。 */ @@ -86,12 +86,12 @@ function collectDiffRows({ cwd, files, base, gitrepo }) { if (diffForPrompt.length > PER_FILE_DIFF_LIMIT) { diffForPrompt = `${diffForPrompt.slice(0, PER_FILE_DIFF_LIMIT)}\n...(diff 過長,其餘截斷未送審)`; truncated = true; - log('步驟3', 'WRN', `${file} 的 diff 超過單檔上限(${chars} 字元),已截斷送審。`); + log('步驟4', 'WRN', `${file} 的 diff 超過單檔上限(${chars} 字元),已截斷送審。`); } if (totalChars + diffForPrompt.length > TOTAL_DIFF_LIMIT) { diffForPrompt = '(全部 diff 總量超過送審上限,本檔內容未送審,僅列出檔名)'; truncated = true; - log('步驟3', 'WRN', `${file} 因總量上限未送審 diff 內容。`); + log('步驟4', 'WRN', `${file} 因總量上限未送審 diff 內容。`); } else { totalChars += diffForPrompt.length; } @@ -121,7 +121,7 @@ function collectDiffRows({ cwd, files, base, gitrepo }) { * @param {Array} params.diffRows - {@link collectDiffRows} 產出的資料列;本函式會就地更新其 `purpose` 欄位。 * @returns {Promise} 無回傳值;結果反映在 `diffRows` 的 `purpose` 欄位。 * @remarks - * 使用情境:審查流程「步驟 3」在 `collectDiffRows` 之後、發布 + * 使用情境:審查流程「步驟 4」在 `collectDiffRows` 之後、發布 * `templates.diffComment` 變更摘要留言之前呼叫,讓摘要表格的「用途」欄有內容。 */ async function fillPurposes({ tool, model, cwd, diffRows }) { @@ -140,12 +140,12 @@ ${sections} - 不得輸出個資(PII)。`; const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 }); if (!res.ok) { - log('步驟3', 'WRN', '檔案用途摘要產生失敗,以「—」代替。'); + log('步驟4', 'WRN', '檔案用途摘要產生失敗,以「—」代替。'); return; } const parsed = extractJson(res.output); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - log('步驟3', 'WRN', '檔案用途摘要回覆無法解析,以「—」代替。'); + log('步驟4', 'WRN', '檔案用途摘要回覆無法解析,以「—」代替。'); return; } for (const row of diffRows) { @@ -163,7 +163,7 @@ ${sections} * @param {*} value - 攻擊方回覆的 severity 原始值(可能是任何型別;非字串會先轉字串)。 * @returns {'嚴重'|'警告'|'建議'} 收斂後的等級字串。 * @remarks - * 使用情境:審查流程「步驟 5」中 {@link normalizeFinding} 檢核每條 finding 時呼叫, + * 使用情境:審查流程「步驟 6」中 {@link normalizeFinding} 檢核每條 finding 時呼叫, * 確保後續 {@link sortFindings} 的 `templates.SEVERITY_ORDER` 排序、 * 「嚴重」分組(步驟 9 逐條留言 vs 步驟 10 彙整表格)都能以固定用詞比對。 * 本函式未匯出,僅供模組內部使用。 @@ -188,7 +188,7 @@ function normalizeSeverity(value) { * @param {Array} diffRows - {@link collectDiffRows} 產出的送審資料列(file/purpose/lastUpdated/diffForPrompt)。 * @returns {string} 可直接餵給 `runAgent` stdin 的完整提示字串。 * @remarks - * 使用情境:審查流程「步驟 5」{@link runAttackers} 為每個攻擊方角色各組一份提示, + * 使用情境:審查流程「步驟 6」{@link runAttackers} 為每個攻擊方角色各組一份提示, * 並行送入 sub agent 找問題。本函式未匯出,僅供模組內部使用。 */ function buildAttackPrompt(role, diffRows) { @@ -233,7 +233,7 @@ ${sections} * @returns {?{reviewer: string, focus: string, badge: string, severity: string, file: string, startLine: number, endLine: number, problem: string, suggestion: string, suggestedCode: string}} * 標準化後的 finding;輸入不合格時為 `null`。 * @remarks - * 使用情境:審查流程「步驟 5」{@link runAttackers} 解析每個攻擊方的 JSON 回覆後, + * 使用情境:審查流程「步驟 6」{@link runAttackers} 解析每個攻擊方的 JSON 回覆後, * 逐條經本函式檢核,通過者才進入合併列表並編派 id,供防守方裁決與留言使用。 * 本函式未匯出,僅供模組內部使用。 */ @@ -256,7 +256,7 @@ function normalizeFinding(fromAgent, role) { } /** - * 步驟 5:每個攻擊方角色一個 sub agent 並行分析送審 diff,合併為單一問題列表並編派 id。 + * 步驟 6:每個攻擊方角色一個 sub agent 並行分析送審 diff,合併為單一問題列表並編派 id。 * * 單一角色失敗(執行失敗或回覆無法解析為 JSON 陣列)只記 WRN 並以空結果代替, * 不阻斷其他角色(失敗降級行為);每條回覆先經 {@link normalizeFinding} 檢核, @@ -270,25 +270,25 @@ function normalizeFinding(fromAgent, role) { * @param {Array} params.diffRows - {@link collectDiffRows} 產出的送審資料列。 * @returns {Promise>} 合併後的標準化 finding 列表(每條含 `id`);全部失敗或無問題時為空陣列。 * @remarks - * 使用情境:審查流程「步驟 5」由 `src/index.js` 在攻擊方登場留言後呼叫, - * 結果直接交給步驟 7 的 {@link runDefenders} 裁決。 + * 使用情境:審查流程「步驟 6」由 `src/index.js` 在攻擊方登場留言後呼叫, + * 結果直接交給步驟 8 的 {@link runDefenders} 裁決。 */ async function runAttackers({ tool, model, cwd, attackers, diffRows }) { const results = await Promise.all( attackers.map(async (role) => { - log('步驟5', 'INF', `攻擊方 ${role.meta.name} 開始分析。`); + log('步驟6', 'INF', `攻擊方 ${role.meta.name} 開始分析。`); const res = await runAgent(tool, { model, prompt: buildAttackPrompt(role, diffRows), cwd }); if (!res.ok) { - log('步驟5', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${(res.error && res.error.message) || '未知錯誤'}。`); + log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${(res.error && res.error.message) || '未知錯誤'}。`); return []; } const parsed = extractJson(res.output); if (!Array.isArray(parsed)) { - log('步驟5', 'WRN', `攻擊方 ${role.meta.name} 回覆無法解析為 JSON 陣列,略過該角色結果。`); + log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 回覆無法解析為 JSON 陣列,略過該角色結果。`); return []; } const list = parsed.map((f) => normalizeFinding(f, role)).filter(Boolean); - log('步驟5', 'INF', `攻擊方 ${role.meta.name} 完成:${list.length} 條問題。`); + log('步驟6', 'INF', `攻擊方 ${role.meta.name} 完成:${list.length} 條問題。`); return list; }), ); @@ -296,7 +296,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) { merged.forEach((finding, index) => { finding.id = `F${String(index + 1).padStart(3, '0')}`; }); - log('步驟5', 'INF', `全部攻擊方完成,合併後共 ${merged.length} 條問題。`); + log('步驟6', 'INF', `全部攻擊方完成,合併後共 ${merged.length} 條問題。`); return merged; } @@ -309,7 +309,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) { * @param {number} limit - 保留的最大字元數(超過即截斷)。 * @returns {string} 截斷後的檔案內容;檔案不存在時為空字串。 * @remarks - * 使用情境:審查流程「步驟 7」{@link runDefenders} 以 + * 使用情境:審查流程「步驟 8」{@link runDefenders} 以 * `readCapped(/.gitea/ai-review/exclusions.json, 20_000)` * 讀取已知排除事項,嵌入 {@link buildDefendPrompt} 的防守方提示, * 避免排除清單過長撐爆提示。本函式未匯出,僅供模組內部使用。 @@ -331,7 +331,7 @@ function readCapped(filePath, limit) { * @param {string} cwd - 工作目錄(repo 根目錄,findings 目錄位於其下 `.gitea/ai-review/findings`)。 * @returns {string} 歷史 findings 摘要文字(Markdown 區段 + JSON);無歷史時為空字串。 * @remarks - * 使用情境:審查流程「步驟 7」{@link runDefenders} 呼叫本函式取得歷史摘要, + * 使用情境:審查流程「步驟 8」{@link runDefenders} 呼叫本函式取得歷史摘要, * 嵌入 {@link buildDefendPrompt},讓防守方能以「與歷史 findings 重複」為由裁決排除。 * 本函式未匯出,僅供模組內部使用。 */ @@ -379,7 +379,7 @@ function loadHistory(cwd) { * @param {string} historyText - {@link loadHistory} 產出的歷史 findings 摘要;空字串時提示顯示「(無)」。 * @returns {string} 可直接餵給 `runAgent` stdin 的完整裁決提示字串。 * @remarks - * 使用情境:審查流程「步驟 7」{@link runDefenders} 為每個防守方角色各組一份提示, + * 使用情境:審查流程「步驟 8」{@link runDefenders} 為每個防守方角色各組一份提示, * 並行送入 sub agent 逐條裁決是否可排除(重複或誤判)。本函式未匯出,僅供模組內部使用。 */ function buildDefendPrompt(role, findings, exclusionsText, historyText) { @@ -424,7 +424,7 @@ ${JSON.stringify(minimal, null, 2)} } /** - * 步驟 7:每個防守方角色一個 sub agent 並行裁決 findings; + * 步驟 8:每個防守方角色一個 sub agent 並行裁決 findings; * 「全部防守方都判可排除」才移除該條,其餘一律保留(保守原則)。 * * 失敗降級:某防守方執行失敗或回覆無法解析 → 該角色視為全部保留; @@ -441,7 +441,7 @@ ${JSON.stringify(minimal, null, 2)} * `kept`=保留(至少一位防守方不同意排除)、`excluded`=移除(全數防守方判可排除); * 兩邊元素都已附 `verdicts`。 * @remarks - * 使用情境:審查流程「步驟 7」由 `src/index.js` 呼叫;`kept` 隨後經 + * 使用情境:審查流程「步驟 8」由 `src/index.js` 呼叫;`kept` 隨後經 * {@link sortFindings} 排序、依「嚴重」分組發留言(步驟 9/10), * `kept` 與 `excluded` 一併保存進 `.gitea/ai-review/findings/*.json`。 */ @@ -451,7 +451,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { const historyText = loadHistory(cwd); const verdictsPerDefender = await Promise.all( defenders.map(async (role) => { - log('步驟7', 'INF', `防守方 ${role.meta.name} 開始裁決。`); + log('步驟8', 'INF', `防守方 ${role.meta.name} 開始裁決。`); const res = await runAgent(tool, { model, prompt: buildDefendPrompt(role, findings, exclusionsText, historyText), @@ -459,7 +459,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { }); const verdicts = new Map(); if (!res.ok) { - log('步驟7', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留。`); + log('步驟8', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留。`); return { role: role.meta.name, verdicts }; } const parsed = extractJson(res.output); @@ -473,9 +473,9 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { } } } else { - log('步驟7', 'WRN', `防守方 ${role.meta.name} 回覆無法解析,該角色視為全部保留。`); + log('步驟8', 'WRN', `防守方 ${role.meta.name} 回覆無法解析,該角色視為全部保留。`); } - log('步驟7', 'INF', `防守方 ${role.meta.name} 完成裁決。`); + log('步驟8', 'INF', `防守方 ${role.meta.name} 完成裁決。`); return { role: role.meta.name, verdicts }; }), ); @@ -493,7 +493,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { finding.verdicts = verdicts; (allExclude ? excluded : kept).push(finding); } - log('步驟7', 'INF', `裁決完成:保留 ${kept.length} 條、排除 ${excluded.length} 條。`); + log('步驟8', 'INF', `裁決完成:保留 ${kept.length} 條、排除 ${excluded.length} 條。`); return { kept, excluded }; } @@ -514,7 +514,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { * false=無排除問題、或既有檔案壞損/非陣列而略過寫入。 * @throws {Error} 檔案系統寫入失敗(如權限不足)時由 fs 拋出,未攔截。 * @remarks - * 使用情境:`main()`(src/index.js)於步驟 7 防守方裁決後呼叫本函式, + * 使用情境:`main()`(src/index.js)於步驟 8 防守方裁決後呼叫本函式, * 並以回傳值決定收尾時是否把 exclusions.json 一併 commit * (一般模式:findings+exclusions.json;建問題模式:只 commit exclusions.json)。 */ @@ -527,11 +527,11 @@ function appendExclusions({ cwd, excluded, prNumber }) { try { entries = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { - log('步驟7', 'WRN', 'exclusions.json 無法解析,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); + log('步驟8', 'WRN', 'exclusions.json 無法解析,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); return false; } if (!Array.isArray(entries)) { - log('步驟7', 'WRN', 'exclusions.json 非 JSON 陣列,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); + log('步驟8', 'WRN', 'exclusions.json 非 JSON 陣列,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); return false; } } @@ -552,7 +552,7 @@ function appendExclusions({ cwd, excluded, prNumber }) { } fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, `${JSON.stringify(entries, null, 2)}\n`, 'utf8'); - log('步驟7', 'INF', `已將 ${excluded.length} 條誤判/重複問題附加到 exclusions.json。`); + log('步驟8', 'INF', `已將 ${excluded.length} 條誤判/重複問題附加到 exclusions.json。`); return true; } @@ -565,7 +565,7 @@ function appendExclusions({ cwd, excluded, prNumber }) { * @param {Array<{severity: string, file: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為 {@link runDefenders} 回傳的 `kept`)。 * @returns {void} 無回傳值;排序結果反映在傳入陣列本身。 * @remarks - * 使用情境:審查流程「步驟 7」裁決完成後、保存 findings 與分組發留言之前, + * 使用情境:審查流程「步驟 8」裁決完成後、保存 findings 與分組發留言之前, * `src/index.js` 對 `kept` 呼叫本函式,確保步驟 9 逐條留言與步驟 10 彙整表格 * 都以「嚴重度優先、同檔集中、行號遞增」的穩定順序呈現。 */ @@ -758,7 +758,7 @@ function readSnippet(cwd, finding) { } /** - * 步驟 8:將 PR 既有的 bot 留言標記為已解決,本回合剛發的留言除外。 + * 步驟 2:將 PR 既有的 bot 留言標記為已解決,本回合剛發的留言除外。 * * 兩類處理: * - 一般留言(bot 發、含隱藏標記、非本回合、尚未標註)→ 編輯加上「〔已過時〕」前綴。 @@ -772,15 +772,16 @@ function readSnippet(cwd, finding) { * @param {Set} params.currentRunCommentIds - 本回合發出的一般留言 id 集合;這些留言不標註過時。 * @returns {Promise} 無回傳值;結果反映在 PR 留言狀態與日誌。 * @remarks - * 使用情境:審查流程「步驟 8」在防守方裁決、保存 findings 之後、 - * 發布本回合嚴重問題留言(步驟 9)之前呼叫,確保 PR 上只有最新回合的審查結果醒目可見。 + * 使用情境:審查流程「步驟 2」在步驟 1 快速回報與前置檢查之後、偵測工具(步驟 3) + * 與所有本回合留言之前呼叫;此時本回合尚未發出任何留言(currentRunCommentIds 為空), + * 之後發出的留言自然不受影響,確保 PR 上只有最新回合的審查結果醒目可見。 */ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) { let botLogin = ''; try { botLogin = (await gitea.whoAmI(ctx)).login || ''; } catch (err) { - log('步驟8', 'WRN', `無法取得 bot 身分(${err.message}),略過留言解決。`); + log('步驟2', 'WRN', `無法取得 bot 身分(${err.message}),略過留言解決。`); return; } @@ -797,9 +798,9 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) { await gitea.editIssueComment(ctx, comment.id, `${templates.OUTDATED_PREFIX}${comment.body}`); outdatedCount += 1; } - log('步驟8', 'INF', `一般留言已標註〔已過時〕:${outdatedCount} 則。`); + log('步驟2', 'INF', `一般留言已標註〔已過時〕:${outdatedCount} 則。`); } catch (err) { - log('步驟8', 'WRN', `標註一般留言失敗:${err.message}。`); + log('步驟2', 'WRN', `標註一般留言失敗:${err.message}。`); } // review 程式碼留言:盡力 resolve;API 不支援(第一次就失敗)即停止嘗試。 @@ -825,12 +826,12 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) { } } if (resolveSupported) { - log('步驟8', 'INF', `review 程式碼留言已解決:${resolvedCount} 則。`); + log('步驟2', 'INF', `review 程式碼留言已解決:${resolvedCount} 則。`); } else { - log('步驟8', 'WRN', 'Gitea 版本不支援 resolve API,review 程式碼留言維持原狀(已解決 ' + resolvedCount + ' 則)。'); + log('步驟2', 'WRN', 'Gitea 版本不支援 resolve API,review 程式碼留言維持原狀(已解決 ' + resolvedCount + ' 則)。'); } } catch (err) { - log('步驟8', 'WRN', `解決 review 留言失敗:${err.message}。`); + log('步驟2', 'WRN', `解決 review 留言失敗:${err.message}。`); } } diff --git a/src/lib/roles.js b/src/lib/roles.js index f120b67..16eac2b 100644 --- a/src/lib/roles.js +++ b/src/lib/roles.js @@ -23,7 +23,7 @@ const path = require('path'); * @throws {Error} 當 `rolesDir` 不存在、無法讀取,或個別檔案讀取失敗時, * 由 `fs.readdirSync` / `fs.readFileSync` 直接拋出(未在函式內捕捉)。 * @remarks - * 使用情境:`src/index.js` 於審查流程步驟 4 呼叫 + * 使用情境:`src/index.js` 於審查流程步驟 5 呼叫 * `loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'))` 載入全部角色, * 再以 {@link attackersOf} / {@link defendersOf} 依 frontmatter 的 `side` 欄位 * 分出攻擊方(Mage/Assassin/Rogue/Bard/Leo/Maya)與防守方(Paladin), diff --git a/src/lib/templates.js b/src/lib/templates.js index 89ab109..6219b0f 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -2,10 +2,10 @@ // 固定留言模板:本 action 發到 PR 的留言一律由此產生(繁體中文、UTF-8、表格優先)。 -// 隱藏標記:辨識哪些留言是本 action 發的(步驟 8 標註過時時使用)。 +// 隱藏標記:辨識哪些留言是本 action 發的(步驟 2 標註過時時使用)。 const MARK = ''; -// 舊留言標註前綴(步驟 8 的降級做法:無 resolve API 時編輯加註)。 +// 舊留言標註前綴(步驟 2 的降級做法:無 resolve API 時編輯加註)。 const OUTDATED_PREFIX = '> 〔已過時〕本留言屬於較舊的審查回合。\n\n'; // 嚴重等級對應的 emoji 與排序權重。 @@ -33,7 +33,7 @@ const FOCUS_LABEL = { * @param {*} text - 任意待處理內容;非字串會先以 `String()` 轉型,null/undefined 視為空字串。 * @returns {string} 已逸出、單行化的儲存格內容;若結果為空則回傳 `'—'`。 * @remarks - * 使用情境:審查流程中所有表格型留言的共用防呆——例如步驟 3 的 + * 使用情境:審查流程中所有表格型留言的共用防呆——例如步驟 4 的 * `diffComment()` 產生變更摘要表格時,檔名與用途欄位都經本函式處理, * 避免檔名或 AI 產生的描述含 `|` 或換行而撐破 Markdown 表格。 * 本函式未匯出,僅供模組內部使用。 @@ -55,7 +55,7 @@ function cell(text) { * @param {string} focus - 審查面向代碼(例如 `'logic'`、`'security'`);可為 undefined。 * @returns {string} 顯示字串:命中時如 `'邏輯(logic)'`;未命中時原樣回傳 `focus`;falsy 時回傳 `'—'`。 * @remarks - * 使用情境:審查流程步驟 4/6 的角色登場留言——`rolesComment()` + * 使用情境:審查流程步驟 5/6 的角色登場留言——`rolesComment()` * 產生「角色|面向|個性」表格時,以本函式把每位審查員 * (攻擊方/防守方)的 focus 代碼轉成中英並列的面向欄位內容。 * 本函式未匯出,僅供模組內部使用。 @@ -66,7 +66,7 @@ function focusLabel(focus) { } /** - * 產生審查流程步驟 2 的「審查工具」PR 留言內容。 + * 產生審查流程步驟 3 的「審查工具」PR 留言內容。 * * 留言以隱藏標記 `MARK` 開頭,包含工具資訊表格(工具/版本/模型/ * 審查 commit/Run Job 連結)與一張 mermaid 流程圖,說明整條審查管線 @@ -81,9 +81,9 @@ function focusLabel(focus) { * @param {string} params.runLink - CI run 的網址,直接內插為 Markdown 連結目標。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記,結尾帶換行)。 * @remarks - * 使用情境:審查流程步驟 2——每回合審查開始時,先把工具身分與 + * 使用情境:審查流程步驟 3——每回合審查開始時,先把工具身分與 * 管線流程圖留言到 PR,讓開發者知道這回合由哪個版本/模型執行; - * 留言開頭的 MARK 讓步驟 8 能辨識並將舊回合留言標註為過時。 + * 留言開頭的 MARK 讓步驟 2 能辨識並將舊回合留言標註為過時。 */ function toolComment({ toolName, version, model, sha, runNumber, runLink }) { return `${MARK} @@ -108,7 +108,7 @@ flowchart LR } /** - * 產生審查流程步驟 3 的「變更摘要(送審 git diff)」PR 留言內容。 + * 產生審查流程步驟 4 的「變更摘要(送審 git diff)」PR 留言內容。 * * 以四欄表格(檔案/用途/git diff 長度/最後更新時間)列出本回合 * 送審的每個檔案;diff 過長被截斷送審的檔案會加註「(過長截斷送審)」, @@ -125,7 +125,7 @@ flowchart LR * @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示排除註記。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:審查流程步驟 3——整理完 git diff 後,把「哪些檔案、多長、 + * 使用情境:審查流程步驟 4——整理完 git diff 後,把「哪些檔案、多長、 * 是否截斷、哪些被 .reviewignore 排除」留言到 PR,讓開發者確認送審範圍 * 與 AI 實際看到的內容一致。 */ @@ -150,10 +150,10 @@ function diffComment(rows, ignoredCount) { } /** - * 產生審查流程步驟 4/6 共用的「角色登場」PR 留言內容。 + * 產生審查流程步驟 5/6 共用的「角色登場」PR 留言內容。 * * 以三欄表格(角色/面向/個性)列出本回合登場的審查員; - * 攻擊方(步驟 4)與防守方(步驟 6)共用本模板,僅標題不同。 + * 攻擊方(步驟 5)與防守方(步驟 7)共用本模板,僅標題不同。 * 面向欄位經 focusLabel() 轉成「中文(原文)」並列格式。 * * @param {Object} params - 留言內容(解構參數)。 @@ -166,7 +166,7 @@ function diffComment(rows, ignoredCount) { * @param {string} params.roles[].meta.personality - 角色個性描述;經 cell() 防呆。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:審查流程步驟 4(攻擊方登場)與步驟 6(防守方登場)—— + * 使用情境:審查流程步驟 5(攻擊方登場)與步驟 7(防守方登場)—— * 在各階段開始審查前,把該回合參與的審查員角色、負責面向與個性 * 留言到 PR,讓開發者理解後續 findings 是由哪些視角產出的。 */ @@ -371,7 +371,7 @@ function issueFindingComment(finding) { * @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示「(N 個檔案被排除)」註記。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:審查流程步驟 3 的替代路徑——整理 git diff 時發現 + * 使用情境:審查流程步驟 4 的替代路徑——整理 git diff 時發現 * 過濾後送審清單為空(例如整包變更都被 .reviewignore 排除), * 直接以本留言告知開發者本回合視為審查通過,不再進入 * 攻擊方/防守方審查階段。 -- 2.53.0 From af6b9a197f586bc12b6b18e7966f344c19e37161 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 01:46:49 +0000 Subject: [PATCH 06/79] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/findings/2026-07-20-09:46:49.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-20-09:46:49.json diff --git a/.gitea/ai-review/findings/2026-07-20-09:46:49.json b/.gitea/ai-review/findings/2026-07-20-09:46:49.json new file mode 100644 index 0000000..7d9d6ec --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-20-09:46:49.json @@ -0,0 +1,12 @@ +{ + "generatedAt": "2026/07/20 09:46:49", + "commitSha": "8662e8ca801c3dbf7f74ed7e758a7a55176735b8", + "prNumber": 4, + "tool": { + "name": "claude", + "version": "2.1.215 (Claude Code)", + "model": "(工具預設)" + }, + "findings": [], + "excluded": [] +} -- 2.53.0 From 91fff79f22f902f54ba5fe32d91d2a3480ac7208 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 11:24:48 +0800 Subject: [PATCH 07/79] =?UTF-8?q?feat(=E5=BB=BA=E5=95=8F=E9=A1=8C=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F):=20=E5=AF=A9=E6=9F=A5=E7=95=99=E8=A8=80=E6=94=B9?= =?UTF-8?q?=E7=99=BC=E5=88=B0=20issue=E3=80=81=E8=B7=B3=E9=81=8E=E8=88=8A?= =?UTF-8?q?=E7=95=99=E8=A8=80=E8=99=95=E7=90=86=E4=B8=A6=E5=9B=9E=E8=B2=BC?= =?UTF-8?q?=20PR=20=E9=80=A3=E7=B5=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 123 ++++++++++++++++++++++++++++++++++--------- src/lib/gitea.js | 39 ++++++++++---- src/lib/review.js | 100 ++++++++--------------------------- src/lib/templates.js | 52 ++++++++++++++++-- 4 files changed, 199 insertions(+), 115 deletions(-) diff --git a/src/index.js b/src/index.js index b4c86d0..72d6237 100644 --- a/src/index.js +++ b/src/index.js @@ -117,9 +117,9 @@ function commitFindings({ cwd, ctx, files, result }) { /** * AI code review 主流程:依固定 10 步驟執行多角色審查,回傳 process exit code。 * - * 流程概要: + * 流程概要(步驟 2~10 描述一般模式;建問題模式差異見末段): * 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審; - * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言); + * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步); * 3. 偵測 AI 工具(antigravity/codex/claude)並留言; * 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, * 一般模式 commit success、建問題模式略過 commit,回傳 0); @@ -128,7 +128,10 @@ function commitFindings({ cwd, ctx, files, result }) { * 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json; * 9. 嚴重問題逐條掛在程式碼行上留言; * 10. 警告+建議彙整為單一表格留言; - * 建問題模式(input: create-issue):保留問題另建 issue(createIssueWithFindings)逐條留言明細; + * 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言 + * 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後才建立 issue 並一次寫入, + * 嚴重問題與警告+建議亦發到該 issue);無保留問題則不建 issue、僅在 PR 留審查通過提示; + * 收束時依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結形成雙向關聯; * 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、 * 建問題模式只 commit exclusions.json、無檔案可 commit 時略過; * commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。 @@ -167,28 +170,60 @@ async function main() { return 1; } - // 本回合發出的一般留言 id:步驟 2 標註過時時要跳過這些。 + // 本回合(一般模式)發出的 PR 留言 id:resolveOldComments 標註過時時要跳過這些。 const currentRunCommentIds = new Set(); + // 建問題模式:issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色) + // 先暫存於 issueBuffer,建立 issue 後一次寫入。 + const issueBuffer = []; + let issue = null; /** - * 建立本回合 PR 一般留言並記錄留言 id,供後續舊留言處理排除。 + * 發布一則審查留言。依模式決定去向: + * - 一般模式:發到 PR,並記錄留言 id 供 `resolveOldComments` 排除。 + * - 建問題模式:issue 已建立時發到 issue;尚未建立時先暫存到 `issueBuffer`。 * - * @param {string} body 要發布到 PR 的 Markdown 留言內容。 - * @returns {Promise} Gitea API 建立的留言物件;至少預期包含 `id`。 + * @param {string} body 要發布的 Markdown 留言內容。 + * @returns {Promise} 一般模式、或建問題模式且 issue 已建立時回傳 Gitea 留言物件; + * 建問題模式尚未建立 issue 而先暫存時回傳 null。 * @remarks * 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與 - * 警告/建議彙整等一般留言。若 Gitea API 失敗,例外會往上拋出並由 - * 主流程頂層 catch 收斂。 + * 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。 */ const postComment = async (body) => { + if (ctx.createIssue) { + if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body); + issueBuffer.push(body); + return null; + } const created = await gitea.createIssueComment(ctx, body); currentRunCommentIds.add(created.id); return created; }; + /** + * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,先不掛標籤), + * 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 + * 僅於「確定有保留問題」時呼叫一次。 + * + * @returns {Promise} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。 + */ + const ensureIssueCreated = async () => { + issue = await gitea.createIssue(ctx, { + title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, + body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), + labels: [], + }); + log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`); + for (const body of issueBuffer) { + await gitea.createCommentOnIssue(ctx, issue.number, body); + } + issueBuffer.length = 0; + }; // ── 步驟 2:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── - // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時; - // 此時本回合尚未發出任何留言(currentRunCommentIds 為空),之後發出的留言自然不受影響。 - await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + // 僅一般模式執行;建問題模式不觸碰 PR 既有留言(審查內容改發到 issue)。 + // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時(此時尚無本回合留言)。 + if (!ctx.createIssue) { + await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + } // ── 步驟 3:偵測 AI agent 工具並留言 ────────────────────────────────── const tool = agents.detectTool(); @@ -218,8 +253,13 @@ async function main() { log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); if (files.length === 0) { - // 沒有可審查的變更:留言說明、保存空 findings、以 success 收場。 - await postComment(templates.nothingToReviewComment(ignoredCount)); + // 沒有可審查的變更:告知、保存空 findings、以 success 收場。 + // 建問題模式無問題可追蹤 → 不建 issue,直接在 PR 告知(暫存的情境留言捨棄)。 + if (ctx.createIssue) { + await gitea.createIssueComment(ctx, templates.nothingToReviewComment(ignoredCount)); + } else { + await postComment(templates.nothingToReviewComment(ignoredCount)); + } const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] }); if (ctx.createIssue) { // 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。 @@ -260,24 +300,59 @@ async function main() { const others = kept.filter((finding) => finding.severity !== '嚴重'); log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); - // ── 步驟 9:嚴重問題逐條掛在程式碼行上留言(開發者可回覆)────────────── - if (severe.length > 0) { - await review.postSevereComments({ ctx, gitea, severe, cwd }); + // ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入; + // 無保留問題則不建 issue,改在 PR 留一則審查通過提示。 ────────────────── + if (ctx.createIssue) { + if (kept.length > 0) { + await ensureIssueCreated(); + } else { + log('建問題', 'INF', '沒有保留的問題,略過建立 issue,於 PR 留審查通過提示。'); + await gitea.createIssueComment(ctx, templates.noFindingsComment()); + } } - // ── 步驟 10:警告+建議彙整為單一表格留言 ────────────────────────────── + // ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式發到 issue)─ + if (severe.length > 0) { + if (ctx.createIssue) { + await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe }); + } else { + await review.postSevereComments({ ctx, gitea, severe, cwd }); + } + } + + // ── 步驟 10:警告+建議彙整為單一表格留言(去向由 postComment 依模式決定)── if (others.length > 0) { await postComment(templates.othersComment(others)); log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); } - // ── 建問題模式(input: create-issue):另建 issue 逐條留言問題明細 ────── - if (ctx.createIssue) { - if (kept.length > 0) { - await review.createIssueWithFindings({ ctx, gitea, tool, model: ctx.model, cwd, findings: kept }); - } else { - log('建問題', 'INF', '沒有保留的問題,略過建立 issue。'); + // ── 建問題模式收束:依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結(雙向關聯)─ + if (ctx.createIssue && issue) { + try { + const labels = await gitea.listLabels(ctx); + const labelIds = await review.selectLabels({ + tool, + model: ctx.model, + cwd, + labels, + prTitle: ctx.prTitle, + prBody: ctx.prBody, + findings: kept, + }); + await gitea.addLabelsToIssue(ctx, issue.number, labelIds); + } catch (err) { + log('建問題', 'WRN', `補掛標籤失敗(${err.message}),issue 不掛標籤。`); } + await gitea.createIssueComment( + ctx, + templates.issueLinkComment({ + issueNumber: issue.number, + issueUrl: issue.html_url, + severeCount: severe.length, + otherCount: others.length, + }), + ); + log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`); } // ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)─────── diff --git a/src/lib/gitea.js b/src/lib/gitea.js index ef71722..91d0c29 100644 --- a/src/lib/gitea.js +++ b/src/lib/gitea.js @@ -99,8 +99,8 @@ function whoAmI(ctx) { * @returns {Promise} 建立成功的留言物件(含 `id`、`body`、`user` 等欄位, * 依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下, - * `createIssueWithFindings` 建立 issue 後,逐條把 finding 明細留言到該 issue; + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立 issue 後, + * 把工具/diff/角色情境留言與 `review.postSevereToIssue` 的嚴重問題明細留言到該 issue; * 另外 `createIssueComment` 也委派本函式對 `ctx.prNumber` 留言。 */ function createCommentOnIssue(ctx, issueNumber, body) { @@ -134,9 +134,9 @@ function createIssueComment(ctx, body) { * @returns {Promise} 標籤物件陣列(每筆含 `id`、`name`、`color` 等欄位, * 依 Gitea API 回應而定);存取庫無標籤時為空陣列。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下, - * `createIssueWithFindings` 先以本函式取得可用標籤,再交給 `selectLabels` - * 讓 AI 從中挑選適合掛在新 issue 上的標籤子集合。 + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 收束時 + * 先以本函式取得可用標籤,再交給 `review.selectLabels` 讓 AI 挑出適合的標籤子集合, + * 最後以 `addLabelsToIssue` 補掛到追蹤 issue 上。 */ function listLabels(ctx) { return listAll(ctx, `/repos/${ctx.owner}/${ctx.repo}/labels`); @@ -156,10 +156,9 @@ function listLabels(ctx) { * @returns {Promise} 建立成功的 issue 物件(含 `number`、`title`、 * `html_url` 等欄位,依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下, - * `createIssueWithFindings` 以 PR 標題/描述為 issue 標題與本文、 - * 配上 `selectLabels` 挑出的標籤 id,呼叫本函式建立追蹤問題的 issue, - * 再逐條把 finding 明細留言到該 issue。 + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 的 `ensureIssueCreated` + * 以 PR 標題/描述為 issue 標題與本文(先不掛標籤)呼叫本函式建立追蹤問題的 issue, + * 之後再把審查內容留言到該 issue、並以 `addLabelsToIssue` 補掛標籤。 */ function createIssue(ctx, { title, body, labels }) { return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues`, { @@ -169,6 +168,27 @@ function createIssue(ctx, { title, body, labels }) { }); } +/** + * 為既有 issue 追加標籤(不影響既有標籤)。 + * 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/labels`。 + * `labels` 為空陣列時不呼叫 API、直接回傳 `null`(省一次無意義請求)。 + * + * @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、 + * `owner`(repo 擁有者)、`repo`(repo 名稱)。 + * @param {number|string} issueNumber - 目標 issue 編號。 + * @param {number[]} labels - 要追加的標籤 id 陣列。 + * @returns {Promise} 追加後該 issue 的標籤陣列(依 Gitea API 回應而定); + * `labels` 為空時回傳 `null`(未發出請求)。 + * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 + * @remarks 使用情境:建問題模式(input: create-issue)下先以空標籤建立 issue、 + * 待防守方裁決得到保留問題後,再以 `selectLabels` 挑出的標籤 id 呼叫本函式補掛, + * 讓標籤挑選能參考最終的問題清單。 + */ +function addLabelsToIssue(ctx, issueNumber, labels) { + if (!labels || labels.length === 0) return Promise.resolve(null); + return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/labels`, { labels }); +} + /** * 列出 PR 上的全部一般留言(自動分頁撈取,每頁 50 筆直到取完)。 * 對應 endpoint:`GET /repos/{owner}/{repo}/issues/{prNumber}/comments`。 @@ -303,6 +323,7 @@ module.exports = { createCommentOnIssue, listLabels, createIssue, + addLabelsToIssue, listIssueComments, editIssueComment, createReview, diff --git a/src/lib/review.js b/src/lib/review.js index cf504f8..73302b5 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -578,30 +578,6 @@ function sortFindings(findings) { ); } -/** - * 建問題模式的就地排序:依檔案路徑、再依嚴重等級(嚴重→警告→建議)、再依起始行遞增。 - * - * 與 {@link sortFindings}(嚴重度優先)不同,本排序以檔案路徑為第一鍵, - * 讓 issue 上逐條留言的問題「同檔集中」,便於開發者逐檔處理。 - * 嚴重等級權重取自 `templates.SEVERITY_ORDER`;未知等級排最後。 - * 注意:直接修改傳入陣列(in-place),無回傳值。 - * - * @param {Array<{file: string, severity: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為保留問題 `kept` 的複本)。 - * @returns {void} 無回傳值;排序結果反映在傳入陣列本身。 - * @remarks - * 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings} - * 先以 `[...findings]` 複製保留問題(不動原陣列的嚴重度排序), - * 再對複本呼叫本函式,依「檔案→嚴重度→行號」的順序逐條留言到新 issue。 - */ -function sortFindingsForIssue(findings) { - findings.sort( - (a, b) => - a.file.localeCompare(b.file) || - (templates.SEVERITY_ORDER[a.severity] ?? 9) - (templates.SEVERITY_ORDER[b.severity] ?? 9) || - a.startLine - b.startLine, - ); -} - /** * 建問題模式:以 AI 依 PR 標題/描述與問題列表摘要, * 從存取庫可用標籤中挑選適合掛在追蹤 issue 上的標籤子集合。 @@ -620,9 +596,9 @@ function sortFindingsForIssue(findings) { * @param {Array} params.findings - 保留的問題列表;每條取 severity/focus/file 與截斷 120 字的 problem 作為挑選依據。 * @returns {Promise} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。 * @remarks - * 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings} - * 先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式取得標籤 id 子集合, - * 傳給 `gitea.createIssue` 讓新 issue 自動掛上合適標籤。 + * 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js) + * 在建立追蹤 issue 後先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式依保留問題 + * 挑出標籤 id 子集合,交給 `gitea.addLabelsToIssue` 補掛到 issue 上。 */ async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) { if (labels.length === 0) return []; @@ -670,61 +646,30 @@ ${JSON.stringify(brief)} } /** - * 建問題模式:把保留的審查問題建成存取庫的追蹤 issue 並逐條留言明細。 + * 建問題模式:把嚴重 findings 逐條以一般留言發到追蹤 issue。 * - * 流程:AI 挑標籤(`listLabels` + {@link selectLabels},失敗不掛標籤) - * → 建立 issue(標題=PR 標題、本文=PR 描述加追溯資訊;失敗記 ERR 並回傳 null 不阻斷主流程) - * → 複製 findings 依「檔案路徑→嚴重等級→起始行」排序({@link sortFindingsForIssue}) - * → 逐條以 `templates.issueFindingComment` 留言到 issue。 + * issue 無法把留言掛在程式碼行上(沒有 diff 定位),故改以 + * {@link templates.issueFindingComment} 在內文標明位置逐條發布—— + * 等同一般模式 PR 步驟 9 的嚴重問題,改以 issue 留言呈現。 + * findings 由呼叫端事先以 {@link sortFindings} 排序(嚴重度→檔案→行號),本函式不再排序。 * * @param {Object} params - 解構參數。 - * @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);使用 `prNumber`、`prTitle`、`prBody` 及 Gitea API 認證欄位。 - * @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `listLabels`、`createIssue`、`createCommentOnIssue`。 - * @param {Object} params.tool - `detectTool()` 偵測到的 AI CLI 工具描述物件(挑標籤用)。 - * @param {string} params.model - 指定 AI 模型名稱;空字串=工具預設。 - * @param {string} params.cwd - agent 的工作目錄(repo 根目錄)。 - * @param {Array} params.findings - 要寫進 issue 的問題列表(通常為防守方裁決後保留的 `kept`);本函式以複本排序,不改動原陣列順序。 - * @returns {Promise} 建立成功的 Gitea issue 物件(含 `number` 等欄位);建立 issue 失敗時為 null。 - * @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出;列標籤與建 issue 的失敗則已於函式內降級處理。 + * @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);供 Gitea API 認證。 + * @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `createCommentOnIssue`。 + * @param {number} params.issueNumber - 目標追蹤 issue 的編號。 + * @param {Array} params.severe - severity 為「嚴重」的 finding 列表(已排序;呼叫端保證非空)。 + * @returns {Promise} 無回傳值;結果反映在 issue 留言與日誌。 + * @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出,由主流程頂層 catch 收斂。 * @remarks - * 使用情境:`main()`(src/index.js)在步驟 10 之後、收尾之前, - * 於 `ctx.createIssue` 為 true 且 `kept.length > 0` 時呼叫本函式; - * 此模式下問題明細已保存在 issue 留言,收尾只 commit exclusions.json、findings 檔不進版控。 + * 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)於防守方裁決後 + * 建立追蹤 issue、寫入情境留言,再以本函式把嚴重問題逐條留言到該 issue; + * 警告+建議則以 {@link templates.othersComment} 彙整成單一表格另發到同一 issue。 */ -async function createIssueWithFindings({ ctx, gitea, tool, model, cwd, findings }) { - let labelIds = []; - try { - const labels = await gitea.listLabels(ctx); - labelIds = await selectLabels({ - tool, - model, - cwd, - labels, - prTitle: ctx.prTitle, - prBody: ctx.prBody, - findings, - }); - } catch (err) { - log('建問題', 'WRN', `取得存取庫標籤失敗(${err.message}),issue 不掛標籤。`); +async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { + for (const finding of severe) { + await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding)); } - let issue; - try { - issue = await gitea.createIssue(ctx, { - title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, - body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), - labels: labelIds, - }); - } catch (err) { - log('建問題', 'ERR', `建立 issue 失敗:${err.message}。`); - return null; - } - const sorted = [...findings]; - sortFindingsForIssue(sorted); - for (const finding of sorted) { - await gitea.createCommentOnIssue(ctx, issue.number, templates.issueFindingComment(finding)); - } - log('建問題', 'INF', `issue #${issue.number} 已建立並逐條留言 ${sorted.length} 條問題。`); - return issue; + log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`); } /** @@ -882,9 +827,8 @@ module.exports = { runDefenders, sortFindings, appendExclusions, - sortFindingsForIssue, selectLabels, - createIssueWithFindings, + postSevereToIssue, resolveOldComments, postSevereComments, }; diff --git a/src/lib/templates.js b/src/lib/templates.js index 6219b0f..ef364e7 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -302,7 +302,7 @@ function othersComment(findings) { * @param {string} [params.prBody] - PR 描述原文;nullish 或 trim 後為空時輸出佔位文字。 * @returns {string} 完整 issue 本文 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:建問題模式下 `createIssueWithFindings`(src/lib/review.js)建立 issue 時, + * 使用情境:建問題模式下 `main()`(src/index.js)的 `ensureIssueCreated` 建立 issue 時, * 以「標題=PR 標題、本文=本函式輸出」呼叫 `gitea.createIssue`, * 讓 issue 讀者能從本文回溯到觸發審查的 PR,再從下方留言逐條查看問題明細。 */ @@ -334,9 +334,9 @@ ${body || '(PR 無描述)'} * @param {string} [finding.suggestedCode] - 建議寫法程式碼;有值才輸出「建議寫法」區塊。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:建問題模式下 `createIssueWithFindings`(src/lib/review.js)建立 issue 後, - * 把保留的 findings 依「檔案路徑→嚴重等級→起始行」排序,逐條以本函式產生留言內容、 - * 經 `gitea.createCommentOnIssue` 發布到新 issue 上,作為問題明細的追蹤紀錄。 + * 使用情境:建問題模式下 `review.postSevereToIssue`(src/lib/review.js)把每條嚴重 finding + * 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上, + * 作為問題明細的追蹤紀錄。 */ function issueFindingComment(finding) { const emoji = SEVERITY_EMOJI[finding.severity] || '🔵'; @@ -383,6 +383,48 @@ function nothingToReviewComment(ignoredCount) { 本次 PR 套用 \`.reviewignore\` 後**沒有可審查的變更**${ignoredCount > 0 ? `(${ignoredCount} 個檔案被排除)` : ''},視為審查通過。`; } +/** + * 產生建問題模式(input: create-issue)下,回貼到「PR」的追蹤問題連結留言。 + * + * 建問題模式把審查內容全部發到 issue、不留在 PR;本留言是 PR 上唯一的一則審查留言, + * 提供 issue 連結與問題數量統計,讓 PR 讀者一眼看到「本次審查結果在哪個 issue」。 + * + * @param {Object} params - 解構參數。 + * @param {number} params.issueNumber - 追蹤問題的 issue 編號;內插為 Markdown 連結文字。 + * @param {string} params.issueUrl - 追蹤問題的 issue 網址;作為 Markdown 連結目標。 + * @param {number} params.severeCount - 嚴重問題條數,顯示在統計。 + * @param {number} params.otherCount - 警告+建議問題條數,顯示在統計。 + * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 + * @remarks + * 使用情境:建問題模式下 `main()`(src/index.js)在 issue 建立並寫入全部審查內容後, + * 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以 + * {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。 + */ +function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) { + return `${MARK} +## 🔍 AI Code Review|已建立追蹤問題 + +本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`; +} + +/** + * 產生「已審查但無需保留問題」時,回貼到「PR」的審查通過留言。 + * + * 建問題模式下,若防守方裁決後沒有任何保留問題(不值得為此開 issue), + * 以本留言在 PR 上告知審查通過,取代原本會發到 issue 的問題內容。 + * + * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 + * @remarks + * 使用情境:建問題模式下 `main()`(src/index.js)於防守方裁決後 + * `kept.length === 0` 時呼叫;此情況不建立 issue,僅在 PR 留下本則通過提示。 + */ +function noFindingsComment() { + return `${MARK} +## ✅ AI Code Review|審查通過 + +本次 AI Code Review 未發現需保留的問題,視為審查通過。`; +} + module.exports = { MARK, OUTDATED_PREFIX, @@ -397,4 +439,6 @@ module.exports = { issueBody, issueFindingComment, nothingToReviewComment, + issueLinkComment, + noFindingsComment, }; -- 2.53.0 From 3631bf359233109159bb5bb5ec7a160da00d2117 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 11:24:48 +0800 Subject: [PATCH 08/79] =?UTF-8?q?chore(ci):=20=E5=95=9F=E7=94=A8=E5=BB=BA?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index e973722..59705bd 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -42,6 +42,8 @@ jobs: with: # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 + create-issue: 'true' # Codex 工具環境測試 job。 test-codex: # Job 在 workflow UI 顯示的名稱。 @@ -66,6 +68,8 @@ jobs: with: # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 + create-issue: 'true' # Claude 工具環境測試 job。 test-claude: # Job 在 workflow UI 顯示的名稱。 @@ -90,3 +94,5 @@ jobs: with: # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 + create-issue: 'true' -- 2.53.0 From 94b32737c592e5c21416e3088d7fd9fe9a5d67aa Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 12:06:21 +0800 Subject: [PATCH 09/79] =?UTF-8?q?feat(=E5=BB=BA=E5=95=8F=E9=A1=8C=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F):=20=E7=84=A1=E5=95=8F=E9=A1=8C=E6=99=82=E9=9D=9C?= =?UTF-8?q?=E9=BB=98=E9=80=9A=E9=81=8E=EF=BC=8C=E4=B8=8D=E5=9C=A8=20PR=20?= =?UTF-8?q?=E7=95=99=E4=BB=BB=E4=BD=95=E5=AF=A9=E6=9F=A5=E7=95=99=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 12 ++++++------ src/lib/templates.js | 19 ------------------- 2 files changed, 6 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index 72d6237..22453e9 100644 --- a/src/index.js +++ b/src/index.js @@ -130,7 +130,7 @@ function commitFindings({ cwd, ctx, files, result }) { * 10. 警告+建議彙整為單一表格留言; * 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言 * 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後才建立 issue 並一次寫入, - * 嚴重問題與警告+建議亦發到該 issue);無保留問題則不建 issue、僅在 PR 留審查通過提示; + * 嚴重問題與警告+建議亦發到該 issue);無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過); * 收束時依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結形成雙向關聯; * 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、 * 建問題模式只 commit exclusions.json、無檔案可 commit 時略過; @@ -253,10 +253,10 @@ async function main() { log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); if (files.length === 0) { - // 沒有可審查的變更:告知、保存空 findings、以 success 收場。 - // 建問題模式無問題可追蹤 → 不建 issue,直接在 PR 告知(暫存的情境留言捨棄)。 + // 沒有可審查的變更:保存空 findings、以 success 收場。 + // 一般模式在 PR 留言告知;建問題模式靜默通過(不建 issue、PR 也不留言,暫存的情境留言捨棄)。 if (ctx.createIssue) { - await gitea.createIssueComment(ctx, templates.nothingToReviewComment(ignoredCount)); + log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。'); } else { await postComment(templates.nothingToReviewComment(ignoredCount)); } @@ -306,8 +306,8 @@ async function main() { if (kept.length > 0) { await ensureIssueCreated(); } else { - log('建問題', 'INF', '沒有保留的問題,略過建立 issue,於 PR 留審查通過提示。'); - await gitea.createIssueComment(ctx, templates.noFindingsComment()); + // 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。 + log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。'); } } diff --git a/src/lib/templates.js b/src/lib/templates.js index ef364e7..4efb42f 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -407,24 +407,6 @@ function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) { 本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`; } -/** - * 產生「已審查但無需保留問題」時,回貼到「PR」的審查通過留言。 - * - * 建問題模式下,若防守方裁決後沒有任何保留問題(不值得為此開 issue), - * 以本留言在 PR 上告知審查通過,取代原本會發到 issue 的問題內容。 - * - * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 - * @remarks - * 使用情境:建問題模式下 `main()`(src/index.js)於防守方裁決後 - * `kept.length === 0` 時呼叫;此情況不建立 issue,僅在 PR 留下本則通過提示。 - */ -function noFindingsComment() { - return `${MARK} -## ✅ AI Code Review|審查通過 - -本次 AI Code Review 未發現需保留的問題,視為審查通過。`; -} - module.exports = { MARK, OUTDATED_PREFIX, @@ -440,5 +422,4 @@ module.exports = { issueFindingComment, nothingToReviewComment, issueLinkComment, - noFindingsComment, }; -- 2.53.0 From 4a68743b66233520e22313235efd32897b67811a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 13:41:41 +0800 Subject: [PATCH 10/79] =?UTF-8?q?chore(=E5=AF=A9=E6=9F=A5=E8=A8=98?= =?UTF-8?q?=E9=8C=84):=20AI=20=E5=A4=B1=E6=95=97=E6=99=82=E8=BC=B8?= =?UTF-8?q?=E5=87=BA=20exit=20code=20=E8=88=87=20stderr=EF=BC=8Fstdout=20?= =?UTF-8?q?=E4=BE=9B=E8=A8=BA=E6=96=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/review.js | 40 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/lib/review.js b/src/lib/review.js index 73302b5..e197a77 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -13,6 +13,38 @@ const templates = require('./templates'); const PER_FILE_DIFF_LIMIT = 16_000; const TOTAL_DIFF_LIMIT = 160_000; +/** + * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號/stderr/stdout 片段。 + * + * AI CLI 失敗時常見 stderr 為空、真正原因印在 stdout(例如 CLI 用法錯誤、未認證或額度提示), + * 若只記 `error.message` 會看不出原因。本函式把 exit code、stderr、stdout 各截前 500 字併成一行, + * 供各失敗點的 WRN log 使用。純函式、不拋例外。 + * + * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res + * `runAgent` 的回傳物件。 + * @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳 error.message 或「未知錯誤」。 + * @remarks + * 使用情境:{@link runAttackers}/{@link runDefenders}/{@link fillPurposes}/{@link selectLabels} + * 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗。 + * 本函式未匯出,僅供模組內部使用。 + */ +function agentFailureDetail(res) { + const parts = []; + const err = res && res.error; + if (err) { + if (err.killed) parts.push('已逾時終止'); + if (typeof err.code === 'number') parts.push(`exit ${err.code}`); + else if (err.code) parts.push(`code ${err.code}`); + else if (err.signal) parts.push(`signal ${err.signal}`); + } + const stderr = String((res && res.stderr) || '').trim(); + if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); + const stdout = String((res && res.output) || '').trim(); + if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); + if (parts.length === 0) parts.push((err && err.message) || '未知錯誤'); + return parts.join('|'); +} + /** * 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。 * @@ -140,7 +172,7 @@ ${sections} - 不得輸出個資(PII)。`; const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 }); if (!res.ok) { - log('步驟4', 'WRN', '檔案用途摘要產生失敗,以「—」代替。'); + log('步驟4', 'WRN', `檔案用途摘要產生失敗,以「—」代替:${agentFailureDetail(res)}。`); return; } const parsed = extractJson(res.output); @@ -279,7 +311,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) { log('步驟6', 'INF', `攻擊方 ${role.meta.name} 開始分析。`); const res = await runAgent(tool, { model, prompt: buildAttackPrompt(role, diffRows), cwd }); if (!res.ok) { - log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${(res.error && res.error.message) || '未知錯誤'}。`); + log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${agentFailureDetail(res)}。`); return []; } const parsed = extractJson(res.output); @@ -459,7 +491,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { }); const verdicts = new Map(); if (!res.ok) { - log('步驟8', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留。`); + log('步驟8', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留:${agentFailureDetail(res)}。`); return { role: role.meta.name, verdicts }; } const parsed = extractJson(res.output); @@ -633,7 +665,7 @@ ${JSON.stringify(brief)} - 沒有適合的標籤時輸出 []。`; const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 }); if (!res.ok) { - log('建問題', 'WRN', '標籤挑選失敗,issue 不掛標籤。'); + log('建問題', 'WRN', `標籤挑選失敗,issue 不掛標籤:${agentFailureDetail(res)}。`); return []; } const parsed = extractJson(res.output); -- 2.53.0 From e37a96433df84cd542d6c93c3b407873e873fa14 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 13:50:07 +0800 Subject: [PATCH 11/79] =?UTF-8?q?fix(ci):=20=E5=82=B3=E5=85=A5=E5=90=84=20?= =?UTF-8?q?AI=20=E5=B7=A5=E5=85=B7=E7=9A=84=20OAuth=20=E6=86=91=E8=AD=89?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=9C=AA=E7=99=BB=E5=85=A5=E5=95=8F=E9=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 59705bd..357a7ed 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -34,6 +34,8 @@ jobs: - name: 安裝工具 # 使用 Gitea composite action 安裝 Antigravity,版本由 vars 指定。 uses: https://gitea.jsc.idv.tw/composite-actions/setup-antigravity@${{ vars.ACTION_SETUP_ANTIGRAVITY_VERSION }} + with: + oauth: ${{ secrets.ANTIGRAVITY_OAUTH }} # 執行目前存取庫的 AI Code Review action。 - name: 程式碼審查 # 以目前存取庫根目錄的 action.yml 作為 action 來源。 @@ -60,6 +62,8 @@ jobs: - name: 安裝工具 # 使用 Gitea composite action 安裝 Codex,版本由 vars 指定。 uses: https://gitea.jsc.idv.tw/composite-actions/setup-codex@${{ vars.ACTION_SETUP_CODEX_VERSION }} + with: + oauth: ${{ secrets.CODEX_OAUTH }} # 執行目前存取庫的 AI Code Review action。 - name: 程式碼審查 # 以目前存取庫根目錄的 action.yml 作為 action 來源。 @@ -86,6 +90,8 @@ jobs: - name: 安裝工具 # 使用 Gitea composite action 安裝 Claude,版本由 vars 指定。 uses: https://gitea.jsc.idv.tw/composite-actions/setup-claude@${{ vars.ACTION_SETUP_CLAUDE_VERSION }} + with: + oauth: ${{ secrets.CLAUDE_OAUTH }} # 執行目前存取庫的 AI Code Review action。 - name: 程式碼審查 # 以目前存取庫根目錄的 action.yml 作為 action 來源。 -- 2.53.0 From d464998e4f9370cde226e381a4b02b27b8329330 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 05:53:12 +0000 Subject: [PATCH 12/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .gitea/ai-review/exclusions.json diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json new file mode 100644 index 0000000..54225e4 --- /dev/null +++ b/.gitea/ai-review/exclusions.json @@ -0,0 +1,13 @@ +[ + { + "addedAt": "2026/07/20 13:53:11", + "prNumber": 6, + "reviewer": "Mage", + "severity": "警告", + "file": "src/index.js", + "startLine": 205, + "endLine": 215, + "problem": "建立 issue 與寫入暫存留言不是可重入操作。最小重現:`createIssue` 成功後,第一則 `createCommentOnIssue` 因暫時性 500 或網路中斷而失敗;主流程退出,但已建立的 issue 不會被記錄或回貼 PR。workflow 重跑時 `issue` 又從 null 開始,因此會再建立一個內容相同的 issue,留下孤兒與重複追蹤項目。後續嚴重問題留言或 PR 回貼失敗也有相同結果。", + "reason": "Paladin:可排除(重複)。與 F004 指涉同一段 issue 建立及暫存留言沖刷流程,也描述相同的中途失敗後重跑會建立重複 issue 問題。" + } +] -- 2.53.0 From 5c8332928b921657f73e2674b833b3b2aa80d6e4 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 14:59:38 +0800 Subject: [PATCH 13/79] =?UTF-8?q?fix(ai-review=20=E8=A8=BA=E6=96=B7?= =?UTF-8?q?=E8=BC=B8=E5=87=BA):=20AI=20=E5=A4=B1=E6=95=97=E9=A0=90?= =?UTF-8?q?=E8=A8=AD=E4=B8=8D=E8=BC=B8=E5=87=BA=E5=8E=9F=E5=A7=8B=20stdout?= =?UTF-8?q?/stderr=E3=80=81=E9=99=A4=E9=8C=AF=E9=96=8B=E9=97=9C=E4=B8=8B?= =?UTF-8?q?=E9=81=AE=E7=BD=A9=E6=A9=9F=E5=AF=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 解決議題 #7 🔴 嚴重(Assassin/security):agentFailureDetail 原本無遮罩擷取 stderr/stdout 各 500 字寫入 CI log,可能長期外洩 token、PII 或原始碼祕密。 - 預設只輸出 exit code/signal/killed 等不含機密的分類資訊。 - 僅在明確開啟 ACTIONS_STEP_DEBUG=true 時,才附上經新增 redactSecrets 遮罩(Authorization/token/URL 帳密/長金鑰)並去除控制字元的片段。 - 另新增 postOthersToIssue:建問題模式下警告+建議改逐條發到 issue,讓每條問題可個別回覆(feat,配合 index.js)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/review.js | 96 ++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 79 insertions(+), 17 deletions(-) diff --git a/src/lib/review.js b/src/lib/review.js index e197a77..1d1b47a 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -14,18 +14,44 @@ const PER_FILE_DIFF_LIMIT = 16_000; const TOTAL_DIFF_LIMIT = 160_000; /** - * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號/stderr/stdout 片段。 + * 遮罩單行診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。 * - * AI CLI 失敗時常見 stderr 為空、真正原因印在 stdout(例如 CLI 用法錯誤、未認證或額度提示), - * 若只記 `error.message` 會看不出原因。本函式把 exit code、stderr、stdout 各截前 500 字併成一行, - * 供各失敗點的 WRN log 使用。純函式、不拋例外。 + * 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽 + * `Authorization` 標頭、`token=`/`token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/ + * 長 hex/`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,故僅在 + * 明確開啟除錯輸出時作為第二道防線使用(見 {@link agentFailureDetail})。 + * + * @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。 + * @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。 + * @remarks 本函式未匯出,僅供模組內部使用。 + */ +function redactSecrets(text) { + return String(text ?? '') + .replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ') + .replace(/(authorization\s*[:=]\s*)\S+/gi, '$1***') + .replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***') + .replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@') + .replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***') + .replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***') + .trim(); +} + +/** + * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。 + * + * 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或 + * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。故本函式**預設只輸出** + * exit code/signal/killed 等不含機密的分類資訊;**僅在明確開啟 Actions step debug** + * (環境變數 `ACTIONS_STEP_DEBUG=true`)時,才附上經 {@link redactSecrets} 遮罩且去除 + * 控制字元的 stderr/stdout 片段(各截前 500 字)作為診斷第二選擇。純函式、不拋例外。 * * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res * `runAgent` 的回傳物件。 - * @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳 error.message 或「未知錯誤」。 + * @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。 * @remarks * 使用情境:{@link runAttackers}/{@link runDefenders}/{@link fillPurposes}/{@link selectLabels} - * 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗。 + * 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗; + * 需要原始輸出診斷時,於 workflow 設定 secret `ACTIONS_STEP_DEBUG=true` 再重跑。 * 本函式未匯出,僅供模組內部使用。 */ function agentFailureDetail(res) { @@ -37,11 +63,18 @@ function agentFailureDetail(res) { else if (err.code) parts.push(`code ${err.code}`); else if (err.signal) parts.push(`signal ${err.signal}`); } - const stderr = String((res && res.stderr) || '').trim(); - if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); - const stdout = String((res && res.output) || '').trim(); - if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); - if (parts.length === 0) parts.push((err && err.message) || '未知錯誤'); + // 預設不輸出 AI CLI 原始 stderr/stdout(可能含 token、PII 或原始碼祕密); + // 僅在明確開啟 Actions step debug 時,附上「已遮罩+去控制字元」的片段作為診斷。 + const verbose = String(process.env.ACTIONS_STEP_DEBUG || '').trim().toLowerCase() === 'true'; + if (verbose) { + const stderr = redactSecrets((res && res.stderr) || ''); + if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); + const stdout = redactSecrets((res && res.output) || ''); + if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); + } + if (parts.length === 0) { + parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(詳細輸出已隱藏)'); + } return parts.join('|'); } @@ -629,8 +662,8 @@ function sortFindings(findings) { * @returns {Promise} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。 * @remarks * 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js) - * 在建立追蹤 issue 後先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式依保留問題 - * 挑出標籤 id 子集合,交給 `gitea.addLabelsToIssue` 補掛到 issue 上。 + * 於確定有保留問題後、建立追蹤 issue 前先呼叫 `gitea.listLabels` 取得可用標籤, + * 再以本函式依保留問題挑出標籤 id 子集合,於 `gitea.createIssue` 建立 issue 時一次帶入。 */ async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) { if (labels.length === 0) return []; @@ -704,6 +737,32 @@ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`); } +/** + * 建問題模式:把警告+建議(非嚴重)findings 逐條以獨立留言發到追蹤 issue。 + * + * 與一般模式(PR)把警告+建議彙整成單一表格({@link templates.othersComment})不同: + * issue 內每條問題各發一則留言({@link templates.issueFindingComment},內文標明位置), + * 讓開發者能針對「單一問題」直接回覆討論,而非只能回覆一整張表格。 + * findings 由呼叫端事先以 {@link sortFindings} 排序(嚴重度→檔案→行號),本函式不再排序。 + * + * @param {Object} params - 解構參數。 + * @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);供 Gitea API 認證。 + * @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `createCommentOnIssue`。 + * @param {number} params.issueNumber - 目標追蹤 issue 的編號。 + * @param {Array} params.others - severity 非「嚴重」(警告+建議)的 finding 列表(已排序;呼叫端保證非空)。 + * @returns {Promise} 無回傳值;結果反映在 issue 留言與日誌。 + * @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出,由主流程頂層 catch 收斂。 + * @remarks + * 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)步驟 10 + * 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。 + */ +async function postOthersToIssue({ ctx, gitea, issueNumber, others }) { + for (const finding of others) { + await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding)); + } + log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}。`); +} + /** * 讀取 finding 對應的程式碼片段:新版檔案的 startLine..endLine,最多 40 行。 * @@ -746,12 +805,14 @@ function readSnippet(cwd, finding) { * @param {Object} params - 解構參數。 * @param {Object} params.ctx - 執行環境 context(`loadContext()` 產出,含 repo/PR 編號/token 等 API 呼叫所需資訊)。 * @param {Object} params.gitea - Gitea API 模組(`src/lib/gitea.js`),需提供 `whoAmI`/`listIssueComments`/`editIssueComment`/`listReviews`/`listReviewComments`/`tryResolveReviewComment`;以參數注入便於測試替換。 - * @param {Set} params.currentRunCommentIds - 本回合發出的一般留言 id 集合;這些留言不標註過時。 + * @param {Set} params.currentRunCommentIds - 本回合已發出的一般留言 id 集合;這些留言不標註過時。 * @returns {Promise} 無回傳值;結果反映在 PR 留言狀態與日誌。 * @remarks - * 使用情境:審查流程「步驟 2」在步驟 1 快速回報與前置檢查之後、偵測工具(步驟 3) - * 與所有本回合留言之前呼叫;此時本回合尚未發出任何留言(currentRunCommentIds 為空), - * 之後發出的留言自然不受影響,確保 PR 上只有最新回合的審查結果醒目可見。 + * 使用情境:一般模式下,`src/index.js` 於「確定本回合審查已成功產生結果後、發布嚴重/ + * 其他問題留言之前」呼叫(見 `main()`),刻意延後到工具偵測、diff 整理與攻防裁決都成功之後, + * 避免任一前置步驟失敗時舊結果已被清掉、PR 卻沒有新結果。此時本回合的工具/diff/角色留言 + * 已發出並登錄於 `currentRunCommentIds`,本函式據此排除、不會把這些「新產生的留言」誤標為過時; + * 之後才發布的嚴重/其他問題留言更不受影響,確保 PR 上只有最新回合的審查結果醒目可見。 */ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) { let botLogin = ''; @@ -861,6 +922,7 @@ module.exports = { appendExclusions, selectLabels, postSevereToIssue, + postOthersToIssue, resolveOldComments, postSevereComments, }; -- 2.53.0 From 35887f1a68b98b29f1c3fff3eef863821ecb383f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 14:59:49 +0800 Subject: [PATCH 14/79] =?UTF-8?q?refactor(resolveMergeBase):=20=E8=A3=9C?= =?UTF-8?q?=E6=8A=93=E6=AD=B7=E5=8F=B2=E6=AF=8F=E6=AD=A5=E6=97=A9=E5=81=9C?= =?UTF-8?q?=E4=B8=A6=E5=BD=99=E6=95=B4=E7=AD=96=E7=95=A5=E8=A8=BA=E6=96=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 解決議題 #7 🟠 警告(Leo/maintainability、Rogue/efficiency):resolveMergeBase 原先無條件連做 --unshallow 與兩次 --deepen,且忽略各次 fetch 成敗、只把首次 merge-base 錯誤設為 cause。 - 改為資料驅動策略清單,每個補抓策略成功後立即重試 merge-base,一成功即回傳,避免多餘遠端往返(--unshallow 成功就不再 deepen)。 - 全部用盡才拋錯,訊息彙整各策略成功/失敗診斷(僅策略名與成敗,不含 git 原始輸出以免洩漏遠端資訊),並以 error.cause 保留首次錯誤。 - 本檔另含既有的 push-token(PAT)推送參數 pushToken(feat),維持原推送降級行為。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/gitrepo.js | 98 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 73 insertions(+), 25 deletions(-) diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index b7c2066..659638e 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -84,13 +84,17 @@ function latestCommitSubject(cwd) { * 解析 PR base 分支與目前 HEAD 的 merge-base commit SHA。 * * 先以 refspec 明確更新 `origin/`,再以 `git merge-base origin/ HEAD` - * 取得共同祖先。若 checkout 是淺層歷史而導致 merge-base 失敗,會補抓完整或更深的 - * base/head 歷史後重試,避免 PR workflow 因 checkout 預設深度不足而中斷。 + * 取得共同祖先。若 checkout 是淺層歷史而導致 merge-base 失敗,會依序補抓更完整的 + * base/head 歷史;**每個補抓策略成功後立即重試 merge-base,一成功即回傳**, + * 避免在已補到足夠歷史後仍多做無謂的 fetch 往返(例如 `--unshallow` 成功就不再 deepen)。 + * 各策略採資料驅動依序執行;全部用盡仍失敗時,丟出彙整了「哪個策略成功/失敗」診斷的錯誤, + * 方便維護者判斷是哪一步補抓不足(診斷僅含策略名與成敗,不含 git 原始輸出以免洩漏遠端資訊)。 * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} baseRef - PR 目標(base)分支名稱,例如 'master' 或 'develop';不含 'origin/' 前綴。 * @returns {string} merge-base 的 commit SHA(40 碼十六進位字串)。 - * @throws {Error} 補抓歷史後仍無法取得共同祖先時,丟出含 baseRef 的明確錯誤。 + * @throws {Error} 補抓歷史後仍無法取得共同祖先時,丟出含 baseRef 與各策略診斷的明確錯誤 + * (`error.cause` 保留首次 merge-base 失敗的原始錯誤)。 * @remarks * 使用情境:AI code review 以此結果作為 diff 比較基準—— * 先 `resolveMergeBase(cwd, pr.base.ref)` 取得基準 SHA, @@ -99,24 +103,56 @@ function latestCommitSubject(cwd) { */ function resolveMergeBase(cwd, baseRef) { const remoteBase = `origin/${baseRef}`; - tryGit(cwd, 'fetch', '--no-tags', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`); - try { - return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); - } catch (firstError) { - const isShallow = gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true'; - if (isShallow) { - tryGit(cwd, 'fetch', '--no-tags', '--unshallow', 'origin'); - } - tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`); - tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD'); + const diagnostics = []; + // 執行一個 fetch 策略並記錄成敗(只記策略名與成敗,不含 git 原始輸出,避免洩漏遠端資訊)。 + const runFetch = (label, ...args) => { + const ok = tryGit(cwd, ...args); + diagnostics.push(`${label}:${ok ? '成功' : '失敗'}`); + return ok; + }; + // 每個補抓策略後重試 merge-base:成功回傳 SHA,失敗記診斷並回傳 null。 + const tryMergeBase = (label) => { try { return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); } catch { - const error = new Error(`無法解析 origin/${baseRef} 與 HEAD 的 merge-base;請確認 checkout 有足夠歷史,或設定 checkout fetch-depth: 0。`); - error.cause = firstError; - throw error; + diagnostics.push(`merge-base(${label}):失敗`); + return null; } + }; + + // 先明確更新 origin/,再嘗試 merge-base。 + runFetch(`fetch base(${baseRef})`, 'fetch', '--no-tags', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`); + let firstError; + try { + return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); + } catch (err) { + firstError = err; + diagnostics.push('merge-base(首次):失敗'); } + + // 資料驅動的補抓策略:淺層才 unshallow;其後依序 deepen base 與 HEAD。 + // 每個策略成功後立即重試 merge-base,成功即回傳,避免多餘往返。 + const strategies = []; + if (gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true') { + strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']); + } + strategies.push([ + 'deepen base', + 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`, + ]); + strategies.push(['deepen HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD']); + + for (const [label, ...args] of strategies) { + if (!runFetch(label, ...args)) continue; // fetch 失敗就換下一個策略。 + const sha = tryMergeBase(`${label} 後`); + if (sha) return sha; + } + + const error = new Error( + `無法解析 origin/${baseRef} 與 HEAD 的 merge-base;請確認 checkout 有足夠歷史,或設定 checkout fetch-depth: 0。診斷:${diagnostics.join(';')}`, + ); + error.cause = firstError; + throw error; } /** @@ -187,7 +223,9 @@ function fileLastUpdatedIso(cwd, file) { * 若目前 HEAD 不在 PR head commit(例如 checkout 停在 merge commit), * 會先 `git checkout --detach ` 站上 head,避免把 merge 內容推回來源分支。 * commit 以 `-c` 臨時覆寫 user.name / user.email,不改動 repo 的 git 設定。 - * push 先走 origin;失敗(遠端未帶認證)時改用帶 token 的 URL 重試。 + * push 策略:提供 `pushToken`(PAT)時直接以該 token 的 URL 推送(略過 origin)——因為 origin + * 帶的是不會再觸發 CI 的自動 token,改以 PAT 身分推送才會讓 PR 的 synchronize 事件再觸發 CI; + * 未提供 `pushToken` 時先走 origin,失敗(遠端未帶認證)再改用帶 `token` 的 URL 重試。 * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {object} options - 提交與推送設定。 @@ -195,7 +233,8 @@ function fileLastUpdatedIso(cwd, file) { * @param {string} [options.headSha] - PR head 的 commit SHA;有提供且與目前 HEAD 不同時會先 detach 到此 commit。可省略(falsy 時不 detach,直接於目前 HEAD 上 commit)。 * @param {string} options.message - commit 訊息。 * @param {string[]} options.files - 要加入 commit 的檔案路徑清單(相對 repo 根目錄);全數無實際變更時不 commit、回傳 false。 - * @param {string} options.token - 具該 repo push 權限的 Gitea access token;僅在 origin push 失敗時用於組出帶認證的重試 URL。 + * @param {string} options.token - 具該 repo push 權限的 Gitea access token;未提供 pushToken 時,於 origin push 失敗才用於組出帶認證的重試 URL。 + * @param {string} [options.pushToken] - 專用推送 token(PAT);提供時直接以此 token 的 URL 推送(略過 origin),使 push 以 PAT 身分進行以再觸發 CI;未提供時走 origin、失敗再退回 `token`。 * @param {string} options.serverUrl - Gitea 伺服器根網址(例如 https://gitea.example.com),須為合法 URL。 * @param {string} options.repository - repo 完整名稱(owner/repo 格式),與 serverUrl 組成 clone URL。 * @returns {boolean} true=有變更且已 commit 並 push 到來源分支;false=暫存區與 HEAD 無差異,略過 commit/push。 @@ -210,7 +249,7 @@ function fileLastUpdatedIso(cwd, file) { * 絕對不得將此 URL 輸出到日誌、錯誤訊息或任何 action 輸出,以免洩漏 token; * 若需記錄重試行為,只能記載「改用帶認證 URL 重試」而不得包含 URL 本身。 */ -function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, serverUrl, repository }) { +function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, pushToken, serverUrl, repository }) { const current = gitTrim(cwd, 'rev-parse', 'HEAD'); if (headSha && current !== headSha) { git(cwd, 'checkout', '--detach', headSha); @@ -228,15 +267,24 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s '-c', 'user.email=ai-review-bot@noreply.gitea', 'commit', '-m', message, ); - try { - git(cwd, 'push', 'origin', `HEAD:refs/heads/${headRef}`); - } catch { - // 遠端未帶認證(checkout 未保留 credentials)時,改用帶 token 的 URL 重試。 - // 注意:不得把這個 URL 輸出到日誌,避免洩漏 token。 + if (pushToken) { + // 有專用 PAT → 直接以帶 token 的 URL 推送(略過 origin,因 origin 帶的是不會再觸發 CI 的自動 token); + // 以 PAT 身分推送才會讓 PR 的 synchronize 事件再觸發 CI。不得把含 token 的 URL 輸出到日誌。 const url = new URL(`${serverUrl}/${repository}.git`); url.username = 'ai-review-bot'; - url.password = token; + url.password = pushToken; git(cwd, 'push', url.toString(), `HEAD:refs/heads/${headRef}`); + } else { + try { + git(cwd, 'push', 'origin', `HEAD:refs/heads/${headRef}`); + } catch { + // 遠端未帶認證(checkout 未保留 credentials)時,改用帶 token 的 URL 重試。 + // 注意:不得把這個 URL 輸出到日誌,避免洩漏 token。 + const url = new URL(`${serverUrl}/${repository}.git`); + url.username = 'ai-review-bot'; + url.password = token; + git(cwd, 'push', url.toString(), `HEAD:refs/heads/${headRef}`); + } } return true; } -- 2.53.0 From e44a8ed0cbff7572bf16f88912d10bebeaaab114 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 15:00:15 +0800 Subject: [PATCH 15/79] =?UTF-8?q?feat(=E5=BB=BA=E5=95=8F=E9=A1=8C=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F):=20PR=20=E7=9B=B8=E4=BE=9D=E8=BF=BD=E8=B9=A4=20issue?= =?UTF-8?q?=E3=80=81=E6=A8=99=E7=B1=A4=E5=BB=BA=E7=AB=8B=E6=99=82=E5=B8=B6?= =?UTF-8?q?=E5=85=A5=E3=80=81=E8=AD=B0=E9=A1=8C=E5=95=8F=E9=A1=8C=E9=80=90?= =?UTF-8?q?=E6=A2=9D=E5=8F=AF=E5=9B=9E=E8=A6=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 整併議題 #7 的建問題模式流程調整與多項 findings 修復(index.js、gitea.js 交錯同檔,整檔歸此)。 - addIssueDependency:建立追蹤 issue 後讓 PR 相依於該 issue,issue 關閉前無法合併(需 repo 啟用問題相依)。 - 🔵 #10(Rogue/efficiency):標籤改於建立 issue 前先 selectLabels、createIssue 時一次帶入,移除事後 addLabelsToIssue 補掛的多餘 API 往返(addLabelsToIssue 保留為通用工具)。 - 使用者補充:建問題模式警告+建議改逐條發到 issue(postOthersToIssue),每條問題可個別回覆。 - 🔴 #1(Mage/logic)+使用者補充:resolveOldComments 延後到審查成功產生結果、發布問題留言前才執行,並以 currentRunCommentIds 排除本回合新留言,避免前置步驟失敗時舊結果被清卻無新結果。 - 🔵 #9(Bard/style):修正「無保留問題」段落註解與實作(靜默通過、不留言)一致。 - 同步更新相關 JSDoc(含 gitea.js listLabels/createIssue/addLabelsToIssue 使用情境)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/index.js | 105 +++++++++++++++++++++++++++++++---------------- src/lib/gitea.js | 42 +++++++++++++++---- 2 files changed, 104 insertions(+), 43 deletions(-) diff --git a/src/index.js b/src/index.js index 22453e9..a1ae574 100644 --- a/src/index.js +++ b/src/index.js @@ -90,6 +90,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { * 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json; * 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 4 判定無可審查變更且非建問題模式時, * 也會以 result: 'success' 提交空 findings。 + * 推送以 `ctx.pushToken`(PAT)優先,使結果 commit 再觸發 CI、由步驟 1 快速回報; * 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。 */ function commitFindings({ cwd, ctx, files, result }) { @@ -100,6 +101,7 @@ function commitFindings({ cwd, ctx, files, result }) { message: `${BOT_COMMIT_PREFIX}[${result}]`, files, token: ctx.token, + pushToken: ctx.pushToken, serverUrl: ctx.serverUrl, repository: ctx.repository, }); @@ -119,7 +121,9 @@ function commitFindings({ cwd, ctx, files, result }) { * * 流程概要(步驟 2~10 描述一般模式;建問題模式差異見末段): * 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審; - * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步); + * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步)—— + * 此步延後到「本回合審查已成功產生結果、即將發布問題留言前」才執行,避免工具偵測/diff/ + * 攻防裁決任一失敗時舊結果先被清掉卻沒有新結果(一般模式); * 3. 偵測 AI 工具(antigravity/codex/claude)並留言; * 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, * 一般模式 commit success、建問題模式略過 commit,回傳 0); @@ -129,9 +133,11 @@ function commitFindings({ cwd, ctx, files, result }) { * 9. 嚴重問題逐條掛在程式碼行上留言; * 10. 警告+建議彙整為單一表格留言; * 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言 - * 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後才建立 issue 並一次寫入, - * 嚴重問題與警告+建議亦發到該 issue);無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過); - * 收束時依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結形成雙向關聯; + * 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後先挑好標籤、連同標籤一次建立 issue + * 並寫入暫存留言,嚴重問題與警告+建議再逐條發到該 issue,讓每條問題都能被個別回覆); + * 無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過); + * 收束時在 PR 回貼 issue 連結形成雙向關聯, + * 並讓 PR 相依於該 issue(addIssueDependency,issue 完成/關閉前 PR 無法合併;需 repo 啟用問題相依功能); * 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、 * 建問題模式只 commit exclusions.json、無檔案可 commit 時略過; * commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。 @@ -199,17 +205,19 @@ async function main() { return created; }; /** - * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,先不掛標籤), + * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立), * 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 - * 僅於「確定有保留問題」時呼叫一次。 + * 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。 * + * @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選); + * 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。 * @returns {Promise} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。 */ - const ensureIssueCreated = async () => { + const ensureIssueCreated = async (labelIds = []) => { issue = await gitea.createIssue(ctx, { title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), - labels: [], + labels: labelIds, }); log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`); for (const body of issueBuffer) { @@ -218,12 +226,12 @@ async function main() { issueBuffer.length = 0; }; - // ── 步驟 2:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── - // 僅一般模式執行;建問題模式不觸碰 PR 既有留言(審查內容改發到 issue)。 - // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時(此時尚無本回合留言)。 - if (!ctx.createIssue) { - await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); - } + // ── 步驟 2:延後執行 ─────────────────────────────────────────────────── + // 「將 PR 既有留言標記為解決」原本在此執行,但若工具偵測/diff/攻防裁決任一失敗, + // 舊結果會先被清掉卻沒有新結果。故延後到「本回合審查已成功產生結果、發布問題留言前」 + // 才呼叫 review.resolveOldComments(見下方步驟 4 空變更路徑與步驟 9 前); + // 屆時本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時。 + // 建問題模式全程不觸碰 PR 既有留言(審查內容改發到 issue)。 // ── 步驟 3:偵測 AI agent 工具並留言 ────────────────────────────────── const tool = agents.detectTool(); @@ -259,6 +267,8 @@ async function main() { log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。'); } else { await postComment(templates.nothingToReviewComment(ignoredCount)); + // 已成功產生本回合結果留言(無可審查變更),此時才把舊留言標為過時(本回合留言已排除)。 + await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); } const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] }); if (ctx.createIssue) { @@ -301,17 +311,42 @@ async function main() { log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); // ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入; - // 無保留問題則不建 issue,改在 PR 留一則審查通過提示。 ────────────────── + // 無保留問題則不建 issue、PR 也完全不留言(靜默通過,暫存的情境留言捨棄)。 ────── if (ctx.createIssue) { if (kept.length > 0) { - await ensureIssueCreated(); + // 先依保留問題挑好標籤,於建立 issue 時一次帶入(省去「先建空標籤 issue 再補掛」的多餘 API 往返); + // 標籤挑選失敗一律降級為不掛標籤,不阻斷建 issue 流程。 + let labelIds = []; + try { + const labels = await gitea.listLabels(ctx); + labelIds = await review.selectLabels({ + tool, + model: ctx.model, + cwd, + labels, + prTitle: ctx.prTitle, + prBody: ctx.prBody, + findings: kept, + }); + } catch (err) { + log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`); + } + await ensureIssueCreated(labelIds); } else { // 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。 log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。'); } } - // ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式發到 issue)─ + // ── 步驟 2(延後執行,一般模式):審查已成功產生結果,發布問題留言前才把舊留言標為過時 ─ + // 延後到此可避免工具偵測/diff/攻防裁決任一失敗時舊結果先被清掉卻無新結果; + // 本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時; + // 嚴重/其他問題留言於本步驟之後才發布,同樣不受影響。 + if (!ctx.createIssue) { + await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + } + + // ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─ if (severe.length > 0) { if (ctx.createIssue) { await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe }); @@ -320,29 +355,20 @@ async function main() { } } - // ── 步驟 10:警告+建議彙整為單一表格留言(去向由 postComment 依模式決定)── + // ── 步驟 10:警告+建議——一般模式彙整為單一表格留言到 PR; + // 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ── if (others.length > 0) { - await postComment(templates.othersComment(others)); - log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); + if (ctx.createIssue) { + await review.postOthersToIssue({ ctx, gitea, issueNumber: issue.number, others }); + } else { + await postComment(templates.othersComment(others)); + log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); + } } - // ── 建問題模式收束:依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結(雙向關聯)─ + // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯),並讓 PR 相依於該 issue ─ + // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → ensureIssueCreated),此處不再補掛。 if (ctx.createIssue && issue) { - try { - const labels = await gitea.listLabels(ctx); - const labelIds = await review.selectLabels({ - tool, - model: ctx.model, - cwd, - labels, - prTitle: ctx.prTitle, - prBody: ctx.prBody, - findings: kept, - }); - await gitea.addLabelsToIssue(ctx, issue.number, labelIds); - } catch (err) { - log('建問題', 'WRN', `補掛標籤失敗(${err.message}),issue 不掛標籤。`); - } await gitea.createIssueComment( ctx, templates.issueLinkComment({ @@ -352,6 +378,13 @@ async function main() { otherCount: others.length, }), ); + // 讓 PR 相依於此追蹤 issue:issue 完成/關閉前 PR 無法合併(需 repo 啟用「問題相依」功能)。 + try { + await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number); + log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}:issue 關閉前無法合併。`); + } catch (err) { + log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`); + } log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`); } diff --git a/src/lib/gitea.js b/src/lib/gitea.js index 91d0c29..9943d66 100644 --- a/src/lib/gitea.js +++ b/src/lib/gitea.js @@ -134,9 +134,9 @@ function createIssueComment(ctx, body) { * @returns {Promise} 標籤物件陣列(每筆含 `id`、`name`、`color` 等欄位, * 依 Gitea API 回應而定);存取庫無標籤時為空陣列。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 收束時 + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 於確定有保留問題後 * 先以本函式取得可用標籤,再交給 `review.selectLabels` 讓 AI 挑出適合的標籤子集合, - * 最後以 `addLabelsToIssue` 補掛到追蹤 issue 上。 + * 最後於建立追蹤 issue 時(`createIssue`)一次帶入這些標籤。 */ function listLabels(ctx) { return listAll(ctx, `/repos/${ctx.owner}/${ctx.repo}/labels`); @@ -157,8 +157,8 @@ function listLabels(ctx) { * `html_url` 等欄位,依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 的 `ensureIssueCreated` - * 以 PR 標題/描述為 issue 標題與本文(先不掛標籤)呼叫本函式建立追蹤問題的 issue, - * 之後再把審查內容留言到該 issue、並以 `addLabelsToIssue` 補掛標籤。 + * 以 PR 標題/描述為 issue 標題與本文,並帶入 `review.selectLabels` 事先挑好的標籤 id + * 呼叫本函式一次建立追蹤問題的 issue(連同標籤),之後再把審查內容逐條留言到該 issue。 */ function createIssue(ctx, { title, body, labels }) { return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues`, { @@ -180,15 +180,42 @@ function createIssue(ctx, { title, body, labels }) { * @returns {Promise} 追加後該 issue 的標籤陣列(依 Gitea API 回應而定); * `labels` 為空時回傳 `null`(未發出請求)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下先以空標籤建立 issue、 - * 待防守方裁決得到保留問題後,再以 `selectLabels` 挑出的標籤 id 呼叫本函式補掛, - * 讓標籤挑選能參考最終的問題清單。 + * @remarks 使用情境:為既有 issue 動態追加標籤的通用工具(不影響既有標籤)。 + * 註:建問題模式主流程已改為「建立 issue 時一次帶入標籤」(見 `createIssue`), + * 不再於事後補掛;本函式保留為 Gitea 客戶端的通用能力,供需要事後追加標籤的情境使用。 */ function addLabelsToIssue(ctx, issueNumber, labels) { if (!labels || labels.length === 0) return Promise.resolve(null); return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/labels`, { labels }); } +/** + * 建立「問題相依」關係:讓 URL 上的 issue/PR 相依於(被阻擋於)表單指定的 issue。 + * 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/dependencies` + * (body 為 IssueMeta:`{index, owner, repo}`)。 + * + * 語義:URL 的 issue(`issueNumber`)相依於 body 的 issue(`dependency`)—— + * 在 `dependency` 關閉前,`issueNumber` 無法合併/關閉。本 endpoint 需 repo 啟用 + * 「問題相依(issue dependencies)」功能,屬版本/設定相依;未啟用或不支援時 API 會回非 2xx。 + * + * @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、 + * `owner`(repo 擁有者)、`repo`(repo 名稱)。 + * @param {number|string} issueNumber - 要被阻擋的 issue/PR 編號(相依方)。 + * @param {number} dependency - 作為阻擋來源的 issue 編號(同一 repo)。 + * @returns {Promise} 建立成功的相依關係物件(依 Gitea API 回應而定)。 + * @throws {Error} 請求失敗(非 2xx,例如未啟用問題相依功能)由底層 `api` 丟出,錯誤附 `status`、`data`。 + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立追蹤 issue 後, + * 以本函式把「PR(`ctx.prNumber`)相依於追蹤 issue」,讓 issue 完成/關閉前 PR 無法合併; + * 呼叫端以 try/catch 降級(功能未啟用時記 WRN、不阻斷流程)。 + */ +function addIssueDependency(ctx, issueNumber, dependency) { + return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, { + index: dependency, + owner: ctx.owner, + repo: ctx.repo, + }); +} + /** * 列出 PR 上的全部一般留言(自動分頁撈取,每頁 50 筆直到取完)。 * 對應 endpoint:`GET /repos/{owner}/{repo}/issues/{prNumber}/comments`。 @@ -324,6 +351,7 @@ module.exports = { listLabels, createIssue, addLabelsToIssue, + addIssueDependency, listIssueComments, editIssueComment, createReview, -- 2.53.0 From d3a09d892e51096f6cccded31150624c7773a71e Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 15:00:23 +0800 Subject: [PATCH 16/79] =?UTF-8?q?feat(push-token):=20=E6=96=B0=E5=A2=9E=20?= =?UTF-8?q?push-token=20=E8=BC=B8=E5=85=A5=E4=BB=A5=20PAT=20=E6=8E=A8?= =?UTF-8?q?=E9=80=81=E7=B5=90=E6=9E=9C=20commit=20=E8=A7=B8=E7=99=BC=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以自動 token(gitea.token/GITHUB_TOKEN)推送的 commit 不會再觸發 CI,導致新 head 缺檢查而卡合併;新增 push-token(PAT)由呼叫端以 secrets 傳入,改以 PAT 身分推送使 PR synchronize 事件再觸發 CI,由步驟 1 快速回報把結果蓋到新 head。留空=退回 token 走 origin 推送。 Co-Authored-By: Claude Opus 4.8 (1M context) --- action.yml | 11 +++++++++++ src/lib/context.js | 5 +++++ 2 files changed, 16 insertions(+) diff --git a/action.yml b/action.yml index 08eb38f..0cd08f6 100644 --- a/action.yml +++ b/action.yml @@ -41,6 +41,17 @@ inputs: required: false # 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用)。 default: 'false' + # 推送用 PAT:推送 findings/exclusions commit 時改以此 token 進行。 + push-token: + # 參數用途說明:以自動 token(gitea.token / GITHUB_TOKEN)推送的 commit 不會再觸發 CI, + # 導致新 head 缺檢查而卡合併;改用 PAT 推送會讓 PR 的 synchronize 事件再觸發 CI, + # 由主程式步驟 1 快速回報([success]/[failure])廉價地把結果蓋到新 head。 + # 呼叫端以 secrets 傳入(例如 secrets.TOKEN)。留空=退回以 token 走 origin 推送(不會再觸發)。 + description: '推送 findings/exclusions commit 用的 PAT(讓 CI 再觸發;呼叫端以 secrets 傳入;留空=退回 token 推送、不再觸發)' + # 選填:未指定時退回以 token 推送。 + required: false + # 預設為空字串,代表不使用專用推送 PAT。 + default: '' # 執行方式區塊:宣告本 action 為 node action 及其進入點。 runs: # 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite)。 diff --git a/src/lib/context.js b/src/lib/context.js index 05eb40c..a3c06ed 100644 --- a/src/lib/context.js +++ b/src/lib/context.js @@ -20,6 +20,7 @@ const path = require('path'); * token: string, * model: string, * createIssue: boolean, + * pushToken: string, * event: Object, * pr: (Object|null), * prNumber: (number|null), @@ -41,6 +42,8 @@ const path = require('path'); * - model:action input `model`(INPUT_MODEL,已 trim),指定 AI 模型,缺值時為空字串。 * - createIssue:action input `create-issue`(INPUT_CREATE-ISSUE),是否將問題建到 * 存取庫的問題追蹤(建問題模式);trim + 小寫後與字串 'true' 嚴格比對,預設 false。 + * - pushToken:action input `push-token`(INPUT_PUSH-TOKEN),推送 findings/exclusions commit 用的 PAT; + * 提供時以 PAT 身分推送使 CI 再觸發(配合步驟 1 快速回報);留空=退回以 token 走 origin 推送(不會再觸發)。 * - event:事件 payload 解析後的完整物件;讀取失敗時為空物件。 * - pr:payload 內的 pull_request 物件;非 PR 事件時為 null。 * - prNumber:PR 編號,優先取 payload,退而從 GITHUB_REF(refs/pull/N/...)解析;皆無時為 null。 @@ -92,6 +95,8 @@ function loadContext() { model: (process.env.INPUT_MODEL || '').trim(), // 是否將問題建到存取庫的問題追蹤(input: create-issue,字串 'true' 才啟用,預設否)。 createIssue: (process.env['INPUT_CREATE-ISSUE'] || '').trim().toLowerCase() === 'true', + // 推送 findings/exclusions commit 用的 PAT(input: push-token);提供時以 PAT 推送讓 CI 再觸發,留空=退回 token。 + pushToken: (process.env['INPUT_PUSH-TOKEN'] || '').trim(), event, pr, prNumber, -- 2.53.0 From dbeb56589c1cff2f55e3011e026b1be96a21427d Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 15:00:32 +0800 Subject: [PATCH 17/79] =?UTF-8?q?docs(templates):=20=E8=A7=92=E8=89=B2?= =?UTF-8?q?=E7=99=BB=E5=A0=B4=E6=AD=A5=E9=A9=9F=E7=B7=A8=E8=99=9F=E7=B5=B1?= =?UTF-8?q?=E4=B8=80=E7=82=BA=205=EF=BC=8F7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 解決議題 #7 🔵 建議(Bard/style):focusLabel 與 rolesComment 文件仍標「步驟 5/6」,但角色登場已調整為攻擊方步驟 5、防守方步驟 7;統一改為 5/7 與鄰近模板文件一致。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/templates.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/templates.js b/src/lib/templates.js index 4efb42f..3476aa8 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -55,7 +55,7 @@ function cell(text) { * @param {string} focus - 審查面向代碼(例如 `'logic'`、`'security'`);可為 undefined。 * @returns {string} 顯示字串:命中時如 `'邏輯(logic)'`;未命中時原樣回傳 `focus`;falsy 時回傳 `'—'`。 * @remarks - * 使用情境:審查流程步驟 5/6 的角色登場留言——`rolesComment()` + * 使用情境:審查流程步驟 5/7 的角色登場留言——`rolesComment()` * 產生「角色|面向|個性」表格時,以本函式把每位審查員 * (攻擊方/防守方)的 focus 代碼轉成中英並列的面向欄位內容。 * 本函式未匯出,僅供模組內部使用。 @@ -150,7 +150,7 @@ function diffComment(rows, ignoredCount) { } /** - * 產生審查流程步驟 5/6 共用的「角色登場」PR 留言內容。 + * 產生審查流程步驟 5/7 共用的「角色登場」PR 留言內容。 * * 以三欄表格(角色/面向/個性)列出本回合登場的審查員; * 攻擊方(步驟 5)與防守方(步驟 7)共用本模板,僅標題不同。 -- 2.53.0 From 10d826d1b17ab26521359a11e08dbe3e7ead8d31 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 15:00:32 +0800 Subject: [PATCH 18/79] =?UTF-8?q?chore(ci):=20=E6=94=B9=E4=BB=A5=20PAT?= =?UTF-8?q?=EF=BC=88push-token=EF=BC=89=E6=8E=A8=E9=80=81=20findings=20com?= =?UTF-8?q?mit=20=E8=A7=B8=E7=99=BC=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 各 job 的 token 改用 gitea.token、並新增 push-token: secrets.TOKEN,讓結果 commit 以 PAT 推送再觸發 CI,避免自動 token 推送不觸發而卡合併。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yaml | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 357a7ed..c623ab1 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -42,10 +42,13 @@ jobs: uses: ./ # 傳入 action inputs。 with: - # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 - token: ${{ secrets.GITHUB_TOKEN }} + # Gitea Actions 自動注入的 repo 範圍 token,用於 PR 留言與 findings 寫回。 + token: ${{ gitea.token }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' + # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, + # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 + push-token: ${{ secrets.TOKEN }} # Codex 工具環境測試 job。 test-codex: # Job 在 workflow UI 顯示的名稱。 @@ -70,10 +73,13 @@ jobs: uses: ./ # 傳入 action inputs。 with: - # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 - token: ${{ secrets.GITHUB_TOKEN }} + # Gitea Actions 自動注入的 repo 範圍 token,用於 PR 留言與 findings 寫回。 + token: ${{ gitea.token }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' + # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, + # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 + push-token: ${{ secrets.TOKEN }} # Claude 工具環境測試 job。 test-claude: # Job 在 workflow UI 顯示的名稱。 @@ -98,7 +104,10 @@ jobs: uses: ./ # 傳入 action inputs。 with: - # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 - token: ${{ secrets.GITHUB_TOKEN }} + # Gitea Actions 自動注入的 repo 範圍 token,用於 PR 留言與 findings 寫回。 + token: ${{ gitea.token }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' + # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, + # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 + push-token: ${{ secrets.TOKEN }} -- 2.53.0 From c980add8077dd1316a6e4d62be481bc4f1c94e25 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 07:03:32 +0000 Subject: [PATCH 19/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 54225e4..c22194a 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -9,5 +9,16 @@ "endLine": 215, "problem": "建立 issue 與寫入暫存留言不是可重入操作。最小重現:`createIssue` 成功後,第一則 `createCommentOnIssue` 因暫時性 500 或網路中斷而失敗;主流程退出,但已建立的 issue 不會被記錄或回貼 PR。workflow 重跑時 `issue` 又從 null 開始,因此會再建立一個內容相同的 issue,留下孤兒與重複追蹤項目。後續嚴重問題留言或 PR 回貼失敗也有相同結果。", "reason": "Paladin:可排除(重複)。與 F004 指涉同一段 issue 建立及暫存留言沖刷流程,也描述相同的中途失敗後重跑會建立重複 issue 問題。" + }, + { + "addedAt": "2026/07/20 15:03:30", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 205, + "endLine": 218, + "problem": "`ensureIssueCreated` 把「建立 issue」與「逐筆沖刷暫存留言」綁成一個不可恢復的流程。若 issue 已成功建立,但其中一則留言失敗,主流程會中止;下次重跑又會建立另一張 issue,留下重複且內容不完整的追蹤單。半年後維護者也很難從現有狀態判斷應重試留言、沿用既有 issue,還是重新建立。", + "reason": "Paladin:可排除(命中已知排除事項)。其指涉的正是 issue 建立成功、留言沖刷失敗後重跑會產生重複追蹤單的同一問題。" } ] -- 2.53.0 From b2129e0bc6c533cc5cfabd198698950377495966 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 15:22:35 +0800 Subject: [PATCH 20/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E5=AF=AB=E5=9B=9E=E8=AD=B0=E9=A1=8C=20#7=20=E5=BE=85=E4=BA=BA?= =?UTF-8?q?=E5=B7=A5=E8=99=95=E7=90=86=E5=95=8F=E9=A1=8C=E8=87=B3=20findin?= =?UTF-8?q?gs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../findings/2026-07-20-15:20:13.json | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-20-15:20:13.json diff --git a/.gitea/ai-review/findings/2026-07-20-15:20:13.json b/.gitea/ai-review/findings/2026-07-20-15:20:13.json new file mode 100644 index 0000000..964b1d0 --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-20-15:20:13.json @@ -0,0 +1,78 @@ +{ + "generatedAt": "2026/07/20 15:20:13", + "commitSha": "c980add8077dd1316a6e4d62be481bc4f1c94e25", + "prNumber": 6, + "tool": { + "name": "code-review-resolve", + "version": "0.0.8", + "model": "(工具預設)" + }, + "findings": [ + { + "id": "F001", + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 210, + "endLine": 224, + "problem": "`ensureIssueCreated` 同時建立 issue、修改外層 `issue` 狀態、逐筆清空 `issueBuffer`,但整段流程沒有可重入或冪等機制。若 issue 建立成功後,寫入其中一則暫存留言時失敗,主流程會中止;重跑後又會建立另一個 issue,留下內容不完整的孤兒 issue。這種依賴閉包可變狀態的半完成狀態,半年後要加入重試、續傳或測試失敗情境都會很痛苦。", + "suggestion": "把「建立追蹤 issue 並沖刷留言」抽成獨立、可注入 Gitea client 的服務函式,明確回傳 issue 與已寫入進度;建立前以 PR 編號或隱藏識別標記查找既有追蹤 issue,讓重跑能接續而非重複建立。至少也應保留已建立的 issue 編號並在錯誤訊息中回報,避免留下無法追蹤的半成品。", + "suggestedCode": "" + }, + { + "id": "F002", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/index.js", + "startLine": 306, + "endLine": 359, + "problem": "建問題模式的核心流程已大幅改變,但 diff 中沒有對應測試驗證各分支:有保留問題時才建立 issue、暫存留言依序送出、無問題時靜默通過、嚴重與非嚴重問題送往正確位置,以及標籤失敗後仍須回貼 PR 連結。這些分支牽涉多次外部 API 呼叫與狀態切換,未測試時很容易出現漏留言、留言送錯 PR/issue,或在 `issue` 尚未建立時解參考的回歸。", + "suggestion": "新增主流程測試並 mock Gitea、agent 與 git 操作,至少涵蓋:`kept=[]` 時不建立 issue 且不留言;僅嚴重問題;僅警告/建議;混合問題;建立 issue 或寫入暫存留言失敗;標籤查詢、AI 選標籤及補掛標籤失敗時仍回貼 issue 連結。除了呼叫次數,也應斷言 API 呼叫順序、目標 issue 編號及留言內容。", + "suggestedCode": "" + }, + { + "id": "F003", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitea.js", + "startLine": 188, + "endLine": 191, + "problem": "新增的 `addLabelsToIssue` 沒有對應測試,尚未驗證空值捷徑與實際 API 請求格式。這個函式位於新建問題流程的收尾路徑;若 endpoint、HTTP method 或 `{ labels }` payload 不符預期,追蹤 issue 將無法取得標籤,而空陣列是否真的不發出請求也未被保護。", + "suggestion": "新增單元測試,分別傳入 `undefined`、`null`、空陣列及多個 label id;斷言前三者回傳 `null` 且完全不呼叫 API,多個 id 時以 POST 呼叫正確的 owner/repo/issue endpoint 並傳送 `{ labels: [...] }`,另驗證 API 拋錯會原樣往上傳遞。", + "suggestedCode": "" + }, + { + "id": "F004", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 100, + "endLine": 122, + "problem": "`resolveMergeBase` 新增多階段 fetch 與淺層 checkout 修復邏輯,但沒有看到測試驗證成功、降級與最終失敗路徑。尤其初次 merge-base 失敗後,淺層與非淺層 repository 會走不同路徑,且多個 `tryGit` 失敗會被刻意吞掉;若參數、refspec 或重試順序有誤,只會在實際 CI checkout 深度不足時才暴露。", + "suggestion": "以 stub 的 git 執行器或暫存 repository 補齊案例:首次 merge-base 成功;淺層 repository 經 `--unshallow` 後成功;`--unshallow` 失敗但 `--deepen=1000` 後成功;非淺層首次失敗後重試成功;所有策略失敗時拋出含 `baseRef` 且保留原始 `cause` 的錯誤。並斷言 base/head fetch 的 refspec 與執行順序。", + "suggestedCode": "" + }, + { + "id": "F005", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 17, + "endLine": 45, + "problem": "新增的 agent 失敗診斷涵蓋逾時、數字或字串 exit code、signal、空輸出及 500 字截斷等多個邊界,但沒有測試鎖定輸出。這些資訊只在失敗路徑出現,正常審查不會自然覆蓋;日後修改時可能悄悄遺失真正的 CLI 錯誤內容,或破壞單行與截斷約束。", + "suggestion": "將摘要邏輯匯出供測試,或透過失敗的 `runAttackers`/`runDefenders`/`fillPurposes` 測試間接斷言 log。至少覆蓋 killed、數字 exit code、字串 code、signal、stderr 與 stdout 同時存在、超過 500 字、完全無資訊,以及 `res` 為 null/undefined 的案例。", + "suggestedCode": "" + } + ], + "excluded": [] +} -- 2.53.0 From dae379273a98cf75c14792ea69cf91fd29d3cd5a Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 07:26:22 +0000 Subject: [PATCH 21/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 99 ++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index c22194a..421b89d 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -20,5 +20,104 @@ "endLine": 218, "problem": "`ensureIssueCreated` 把「建立 issue」與「逐筆沖刷暫存留言」綁成一個不可恢復的流程。若 issue 已成功建立,但其中一則留言失敗,主流程會中止;下次重跑又會建立另一張 issue,留下重複且內容不完整的追蹤單。半年後維護者也很難從現有狀態判斷應重試留言、沿用既有 issue,還是重新建立。", "reason": "Paladin:可排除(命中已知排除事項)。其指涉的正是 issue 建立成功、留言沖刷失敗後重跑會產生重複追蹤單的同一問題。" + }, + { + "addedAt": "2026/07/20 15:26:21", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 179, + "endLine": 234, + "problem": "`main()` 新增了留言路由、issue 狀態、緩衝佇列、issue 建立與緩衝留言沖刷等職責,後續流程又多次以 `ctx.createIssue` 分支決定發布方式。這使主流程同時負責審查編排與發布狀態機;六個月後若再增加發布管道或重試策略,模式判斷會繼續散落,難以獨立測試各種狀態轉換。", + "reason": "Paladin:可排除(重複)。與 F006 指涉同一段 main() 內的留言路由、issue 狀態及緩衝佇列職責,亦提出相同的發布器抽離方向。" + }, + { + "addedAt": "2026/07/20 15:26:21", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 213, + "endLine": 233, + "problem": "`ensureIssueCreated` 在建立 issue 後逐則寫入 `issueBuffer`,但整段沒有可恢復或冪等機制。若寫到一半 API 失敗,主流程會中止;重新執行時無法辨識已建立但內容不完整的 issue,因而可能再建立一張重複 issue。這類部分完成狀態日後會很難人工清理與追查。", + "reason": "Paladin:可排除(命中已知排除事項且與歷史 finding 重複)。其描述的 issue 建立後沖刷留言失敗、重跑產生重複 issue,正是既有排除事項與歷史 finding 已記錄的問題。" + }, + { + "addedAt": "2026/07/20 15:26:21", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 171, + "endLine": 194, + "problem": "新增並匯出的 `addLabelsToIssue` 沒有被本次流程使用,註解也明確表示主流程已不再需要它。保留這個預想中的通用 API 會擴大公開介面與測試範圍;未來 Gitea API 行為改變時,維護者仍得判斷這個無使用者的函式是否需要同步修改。", + "reason": "Paladin:可排除(重複)。與 F007 指涉相同的未使用 addLabelsToIssue 函式及匯出,問題與移除建議均相同。" + }, + { + "addedAt": "2026/07/20 15:26:21", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 331, + "endLine": 331, + "problem": "建問題模式在此直接建立新的追蹤 issue,卻沒有任何可重入或去重機制。最小重現:issue 建立成功後,寫入 `issueBuffer`、發布 finding、回貼 PR 連結或最後 push 任一步驟失敗,整個 Action 會以失敗結束;重新執行同一個 commit 時仍會再次呼叫 `ensureIssueCreated`,因此產生內容相同的重複 issue。失敗若持續發生,每次重跑都會再新增一筆。", + "reason": "Paladin:可排除(命中已知排除事項且與 F009、歷史 finding 重複)。同樣是建立 issue 後任一步驟失敗,重跑無法沿用既有 issue 而重複建立的問題。" + }, + { + "addedAt": "2026/07/20 15:26:21", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 190, + "endLine": 238, + "problem": "新增的一般模式/建問題模式留言分流與 `issueBuffer` 清空流程,這次 diff 沒有對應測試。尚未驗證 issue 建立前留言只暫存、建立後依序送出、建立後的新留言直接送往 issue,以及任一 API 寫入失敗時的行為;這是本次建問題模式的核心路徑,沒有測試即無法確認留言不會遺失、重複或誤發到 PR。", + "reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已明確涵蓋建問題模式核心流程、暫存留言依序送出、發布分流及外部 API 狀態切換缺乏測試。" + }, + { + "addedAt": "2026/07/20 15:26:21", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 312, + "endLine": 389, + "problem": "建問題模式新增了「無 finding 靜默通過」「有 finding 才建 issue」「標籤失敗降級」「嚴重/其他問題分流」「PR 回貼連結」「相依 API 失敗不阻斷」等多個分支,但本次 diff 沒有測試驗證這些結果。尤其 `kept`、`severe`、`others` 的空集合組合與 API 失敗路徑未經試煉,重構後很容易出現 `issue` 為 null 卻被取用、建立空 issue,或意外在 PR 留言。", + "reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已列出有問題才建立 issue、無問題靜默通過、嚴重與非嚴重問題分流、標籤失敗及 PR 回貼等相同未測試分支。" + }, + { + "addedAt": "2026/07/20 15:26:21", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 103, + "endLine": 157, + "problem": "`resolveMergeBase` 新增多階段 fetch/重試狀態機,卻沒有新增測試驗證策略順序與停止條件。未驗證非 shallow、`--unshallow` 成功、各 fetch 失敗、補抓後仍無共同祖先等邊界,可能讓正常 checkout 多做 fetch,或在仍可恢復時提早拋錯。", + "reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已針對 resolveMergeBase 的多階段 fetch、淺層與非淺層分支、降級及最終失敗路徑缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 15:26:21", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 16, + "endLine": 77, + "problem": "新增的失敗診斷與機密遮罩會處理 CI 日誌中的敏感輸出,但本次 diff 沒有測試其邊界與失敗情境。未驗證 debug 預設關閉、大小寫/空白解析、控制字元單行化、各 token 格式遮罩、500 字截斷,以及 `error`/`stderr`/`output` 缺漏時仍能穩定回傳摘要。", + "reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已指出 agent 失敗診斷的退出狀態、空輸出、單行化與截斷等邊界缺少測試;本條只是進一步枚舉相同測試缺口。" + }, + { + "addedAt": "2026/07/20 15:26:21", + "prNumber": 6, + "reviewer": "Rogue", + "severity": "警告", + "file": "src/index.js", + "startLine": 226, + "endLine": 228, + "problem": "`issueBuffer` 內每則留言以 `await` 串行送出,建立 issue 的等待時間會線性累加為約 `留言數 × API RTT`;若每次往返 200~500 ms,6 則留言便額外卡住約 1.2~3 秒。", + "reason": "Paladin:可排除(誤判)。此處逐筆 await 用來維持暫存留言的 FIFO 發布順序;留言數量有限,所述短暫延遲不足以證明缺陷,而改成並行反而可能破壞順序並增加 API 限流風險。" } ] -- 2.53.0 From cfbbbe5f6c3beb591848162100bdbc6073759731 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 15:41:17 +0800 Subject: [PATCH 22/79] =?UTF-8?q?docs(review):=20=E4=BF=AE=E6=AD=A3=20post?= =?UTF-8?q?SevereToIssue=20=E8=A8=BB=E8=A7=A3=E7=82=BA=E5=BB=BA=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E6=A8=A1=E5=BC=8F=E8=AD=A6=E5=91=8A=EF=BC=8F=E5=BB=BA?= =?UTF-8?q?=E8=AD=B0=E9=80=90=E6=A2=9D=E7=95=99=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/review.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/review.js b/src/lib/review.js index 1d1b47a..b6a69ea 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -728,7 +728,8 @@ ${JSON.stringify(brief)} * @remarks * 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)於防守方裁決後 * 建立追蹤 issue、寫入情境留言,再以本函式把嚴重問題逐條留言到該 issue; - * 警告+建議則以 {@link templates.othersComment} 彙整成單一表格另發到同一 issue。 + * 警告+建議則由 {@link postOthersToIssue} 同樣逐條留言到同一 issue(皆可個別回覆), + * 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。 */ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { for (const finding of severe) { -- 2.53.0 From a6534ee0764fe2363ebbb5525586ad3868dc6720 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 07:44:23 +0000 Subject: [PATCH 23/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 66 ++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 421b89d..ef1ad2f 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -119,5 +119,71 @@ "endLine": 228, "problem": "`issueBuffer` 內每則留言以 `await` 串行送出,建立 issue 的等待時間會線性累加為約 `留言數 × API RTT`;若每次往返 200~500 ms,6 則留言便額外卡住約 1.2~3 秒。", "reason": "Paladin:可排除(誤判)。此處逐筆 await 用來維持暫存留言的 FIFO 發布順序;留言數量有限,所述短暫延遲不足以證明缺陷,而改成並行反而可能破壞順序並增加 API 限流風險。" + }, + { + "addedAt": "2026/07/20 15:44:22", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 179, + "endLine": 230, + "problem": "建問題模式的狀態管理、留言路由、issue 建立及緩衝區清空都直接塞進已負責整條審查流程的 `main()`,並透過 `issue`、`issueBuffer` 兩個可變閉包共享狀態。後續若再增加重試、不同發布目的地或補償處理,維護者必須同時追蹤閉包狀態與主流程分支;目前 `ensureIssueCreated` 若在逐則寫入緩衝留言時失敗,issue 已建立但狀態沒有可恢復的進度,重跑也可能再建立重複 issue。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。已知排除事項已涵蓋 main() 中留言路由、issue 狀態與緩衝佇列職責散落;其中重跑產生重複 issue 的部分也與歷史 finding 相同。" + }, + { + "addedAt": "2026/07/20 15:44:22", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 171, + "endLine": 195, + "problem": "新增的 `addLabelsToIssue` 沒有任何呼叫端,註解甚至明確說明主流程已不再使用它。現在就保留這個公開匯出會擴大 Gitea client 的 API 表面,未來維護者仍需替它維護文件、測試與版本相容性,卻沒有實際需求驗證其語意。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。已知排除事項已記錄相同的未使用 addLabelsToIssue 函式、公開匯出及移除建議。" + }, + { + "addedAt": "2026/07/20 15:44:22", + "prNumber": 6, + "reviewer": "Mage", + "severity": "警告", + "file": "src/index.js", + "startLine": 216, + "endLine": 225, + "problem": "追蹤 issue 的建立與暫存留言寫入不是可重入操作:`createIssue` 成功後,只要任一 `createCommentOnIssue` 失敗,主流程就直接中止,但已建立的 issue 不會回滾或留下可供重跑辨識的狀態。最小重現為:建立 issue 成功、第一則留言遇到暫時性 5xx;workflow 重跑後會再建立一張相同 issue,產生孤兒或重複追蹤問題。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。issue 建立成功、留言失敗後重跑會建立重複 issue,正是已知排除事項及歷史 finding 已記錄的同一問題。" + }, + { + "addedAt": "2026/07/20 15:44:22", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 184, + "endLine": 374, + "problem": "建問題模式新增了大量分支行為,但本次 diff 沒有對應測試驗證:留言先暫存再依序寫入 issue、無保留問題時靜默通過、標籤挑選失敗時降級、嚴重與非嚴重問題分流、PR 回貼連結,以及建立相依失敗時不中斷流程。這些都是可觀察的核心流程;沒有測試時,任何分支回歸都可能造成漏建 issue、留言送錯位置或流程意外失敗。", + "reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已涵蓋建問題模式的暫存留言、靜默通過、問題分流、標籤失敗及 PR 回貼等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 15:44:22", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 105, + "endLine": 157, + "problem": "`resolveMergeBase` 新增多階段 fetch/重試策略,卻沒有測試驗證最容易出錯的淺層歷史與失敗路徑,包括非 shallow repo、`--unshallow` 成功後立即停止、某次 fetch 失敗後繼續下一策略,以及全部失敗時的 diagnostics 與 `cause`。這類命令編排很容易因呼叫順序或 off-by-one 次重試而產生多餘網路請求,甚至在其實可取得 merge-base 時仍失敗。", + "reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已指出 resolveMergeBase 多階段 fetch 在淺層、非淺層、降級及最終失敗路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 15:44:22", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 17, + "endLine": 81, + "problem": "新增的 AI CLI 失敗診斷只在失敗與 debug 模式執行,但沒有測試覆蓋其邊界:`res`/`error` 缺值、逾時與 signal、debug 大小寫及空白、控制字元單行化、500 字截斷,以及多種 token/URL 帳密遮罩。這些失敗路徑若回歸,可能使診斷完全缺失或將未遮罩內容寫入 CI log。", + "reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已記錄 AI CLI 失敗診斷在退出狀態、空輸出、單行化、截斷及相關邊界缺少測試;本條是對同一測試缺口的細項枚舉。" } ] -- 2.53.0 From 8349fe8b71bd08e7771e17c7ede995cd554a5c77 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 16:01:05 +0800 Subject: [PATCH 24/79] =?UTF-8?q?fix(=E6=8E=A8=E9=80=81=E7=B5=90=E6=9E=9C?= =?UTF-8?q?=20commit):=20=E5=B8=B6=E8=AA=8D=E8=AD=89=E6=8E=A8=E9=80=81?= =?UTF-8?q?=E6=94=B9=E7=B6=93=20env=20=E5=82=B3=E5=85=A5=E4=B8=A6=E9=81=AE?= =?UTF-8?q?=E8=94=BD=20push=20=E5=A4=B1=E6=95=97=E9=8C=AF=E8=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/gitrepo.js | 66 ++++++++++++++++++++++++++++++++++------------ 1 file changed, 49 insertions(+), 17 deletions(-) diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index 659638e..3b80bb4 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -244,10 +244,10 @@ function fileLastUpdatedIso(cwd, file) { * findings 檔與 `.gitea/ai-review/exclusions.json` 等結果檔提交回 PR 來源分支, * 並依回傳值記錄「已 commit/push」或「無實際變更、略過」的不同日誌。 * - * 安全注意:push 重試時組出的 URL 內含 token(形如 - * `https://ai-review-bot:@host/owner/repo.git`), - * 絕對不得將此 URL 輸出到日誌、錯誤訊息或任何 action 輸出,以免洩漏 token; - * 若需記錄重試行為,只能記載「改用帶認證 URL 重試」而不得包含 URL 本身。 + * 安全注意:帶認證的推送一律透過 {@link pushWithCredential} 進行——認證只以 + * 環境變數(`http..extraheader` 的 base64 Basic)傳入,**不進 argv**, + * 推送目標 URL 本身不含帳密;且 push 失敗時改拋固定訊息,避免 `execFileSync` + * 例外把命令列(含 token)回顯到 CI log 或程序清單。 */ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, pushToken, serverUrl, repository }) { const current = gitTrim(cwd, 'rev-parse', 'HEAD'); @@ -267,28 +267,60 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, p '-c', 'user.email=ai-review-bot@noreply.gitea', 'commit', '-m', message, ); + const refspec = `HEAD:refs/heads/${headRef}`; + const remoteUrl = `${serverUrl}/${repository}.git`; if (pushToken) { - // 有專用 PAT → 直接以帶 token 的 URL 推送(略過 origin,因 origin 帶的是不會再觸發 CI 的自動 token); - // 以 PAT 身分推送才會讓 PR 的 synchronize 事件再觸發 CI。不得把含 token 的 URL 輸出到日誌。 - const url = new URL(`${serverUrl}/${repository}.git`); - url.username = 'ai-review-bot'; - url.password = pushToken; - git(cwd, 'push', url.toString(), `HEAD:refs/heads/${headRef}`); + // 有專用 PAT → 以 PAT 推送(略過 origin,因 origin 帶的是不會再觸發 CI 的自動 token); + // 以 PAT 身分推送才會讓 PR 的 synchronize 事件再觸發 CI。 + pushWithCredential(cwd, remoteUrl, pushToken, refspec); } else { try { - git(cwd, 'push', 'origin', `HEAD:refs/heads/${headRef}`); + git(cwd, 'push', 'origin', refspec); } catch { - // 遠端未帶認證(checkout 未保留 credentials)時,改用帶 token 的 URL 重試。 - // 注意:不得把這個 URL 輸出到日誌,避免洩漏 token。 - const url = new URL(`${serverUrl}/${repository}.git`); - url.username = 'ai-review-bot'; - url.password = token; - git(cwd, 'push', url.toString(), `HEAD:refs/heads/${headRef}`); + // 遠端未帶認證(checkout 未保留 credentials)時,改用帶認證的推送重試。 + pushWithCredential(cwd, remoteUrl, token, refspec); } } return true; } +/** + * 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。 + * + * 認證方式:等同 `https://ai-review-bot:@host/...` 的 HTTP Basic(git 會把 + * URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的 + * `GIT_CONFIG_*` 環境變數注入 `http..extraheader`,使 base64 憑證**不出現在 argv** + * (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。 + * 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。 + * + * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 + * @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。 + * @param {string} secret - 具 push 權限的 token/PAT(作為 Basic 認證的密碼)。 + * @param {string} refspec - push 的 refspec(形如 `HEAD:refs/heads/`)。 + * @returns {void} 成功即返回;失敗拋出不含 URL/argv/token 的固定錯誤。 + * @throws {Error} 推送失敗時拋出固定訊息(已隱藏遠端 URL 與認證資訊)。 + * @remarks 本函式未匯出,僅供 {@link commitAndPushFindings} 使用。 + */ +function pushWithCredential(cwd, remoteUrl, secret, refspec) { + const basic = Buffer.from(`ai-review-bot:${secret}`).toString('base64'); + try { + execFileSync('git', ['push', remoteUrl, refspec], { + cwd, + encoding: 'utf8', + maxBuffer: 64 * 1024 * 1024, + env: { + ...process.env, + GIT_TERMINAL_PROMPT: '0', + GIT_CONFIG_COUNT: '1', + GIT_CONFIG_KEY_0: `http.${remoteUrl}.extraheader`, + GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`, + }, + }); + } catch { + throw new Error('推送審查結果 commit 失敗(已隱藏遠端 URL 與認證資訊)。'); + } +} + module.exports = { latestCommitSubject, resolveMergeBase, -- 2.53.0 From f341dc6e07fa326406e3b6cc6e3654959dc1f0ca Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 16:01:05 +0800 Subject: [PATCH 25/79] =?UTF-8?q?perf(AI=20=E5=A4=B1=E6=95=97=E8=A8=BA?= =?UTF-8?q?=E6=96=B7):=20=E9=99=A4=E9=8C=AF=E8=BC=B8=E5=87=BA=E5=85=88?= =?UTF-8?q?=E6=88=AA=E9=95=B7=E5=BA=A6=E4=B8=8A=E9=99=90=E5=86=8D=E9=81=AE?= =?UTF-8?q?=E7=BD=A9=E9=99=8D=E4=BD=8E=E6=8E=83=E6=8F=8F=E6=88=90=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/review.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/lib/review.js b/src/lib/review.js index b6a69ea..172f22c 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -67,9 +67,12 @@ function agentFailureDetail(res) { // 僅在明確開啟 Actions step debug 時,附上「已遮罩+去控制字元」的片段作為診斷。 const verbose = String(process.env.ACTIONS_STEP_DEBUG || '').trim().toLowerCase() === 'true'; if (verbose) { - const stderr = redactSecrets((res && res.stderr) || ''); + // 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描; + // 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。 + const DEBUG_INPUT_LIMIT = 2_000; + const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, DEBUG_INPUT_LIMIT)); if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); - const stdout = redactSecrets((res && res.output) || ''); + const stdout = redactSecrets(String((res && res.output) || '').slice(0, DEBUG_INPUT_LIMIT)); if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); } if (parts.length === 0) { -- 2.53.0 From 0a51affa72c4221ac5822bea572573a9bded8abc Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 16:01:05 +0800 Subject: [PATCH 26/79] =?UTF-8?q?refactor(gitea=20client):=20=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E7=84=A1=E5=91=BC=E5=8F=AB=E7=AB=AF=E7=9A=84=20addLab?= =?UTF-8?q?elsToIssue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/gitea.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/lib/gitea.js b/src/lib/gitea.js index 9943d66..ab4ed6b 100644 --- a/src/lib/gitea.js +++ b/src/lib/gitea.js @@ -168,27 +168,6 @@ function createIssue(ctx, { title, body, labels }) { }); } -/** - * 為既有 issue 追加標籤(不影響既有標籤)。 - * 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/labels`。 - * `labels` 為空陣列時不呼叫 API、直接回傳 `null`(省一次無意義請求)。 - * - * @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、 - * `owner`(repo 擁有者)、`repo`(repo 名稱)。 - * @param {number|string} issueNumber - 目標 issue 編號。 - * @param {number[]} labels - 要追加的標籤 id 陣列。 - * @returns {Promise} 追加後該 issue 的標籤陣列(依 Gitea API 回應而定); - * `labels` 為空時回傳 `null`(未發出請求)。 - * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:為既有 issue 動態追加標籤的通用工具(不影響既有標籤)。 - * 註:建問題模式主流程已改為「建立 issue 時一次帶入標籤」(見 `createIssue`), - * 不再於事後補掛;本函式保留為 Gitea 客戶端的通用能力,供需要事後追加標籤的情境使用。 - */ -function addLabelsToIssue(ctx, issueNumber, labels) { - if (!labels || labels.length === 0) return Promise.resolve(null); - return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/labels`, { labels }); -} - /** * 建立「問題相依」關係:讓 URL 上的 issue/PR 相依於(被阻擋於)表單指定的 issue。 * 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/dependencies` @@ -350,7 +329,6 @@ module.exports = { createCommentOnIssue, listLabels, createIssue, - addLabelsToIssue, addIssueDependency, listIssueComments, editIssueComment, -- 2.53.0 From dcd3e931f4c00842e86dfabfd8e54b06f0c9bd7f Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 16:01:05 +0800 Subject: [PATCH 27/79] =?UTF-8?q?docs(readme):=20=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=9C=96=E6=AD=A5=E9=A9=9F=E9=A0=86=E5=BA=8F=E5=8F=8D=E6=98=A0?= =?UTF-8?q?=E8=88=8A=E7=95=99=E8=A8=80=E6=A8=99=E8=A8=98=E5=BB=B6=E5=BE=8C?= =?UTF-8?q?=E5=9F=B7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- readme.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/readme.md b/readme.md index 1d9725d..68799df 100644 --- a/readme.md +++ b/readme.md @@ -38,14 +38,14 @@ jobs: ```mermaid flowchart TD S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure] - S1 -->|未命中| S2[2 舊留言標記解決] - S2 --> S3[3 偵測 AI 工具並留言] + S1 -->|未命中| S3[3 偵測 AI 工具並留言] S3 --> S4[4 讀 .reviewignore 整理 diff 並留言] S4 --> S5[5 攻擊方登場留言] S5 --> S6[6 攻擊方 sub agent 並行找問題] S6 --> S7[7 防守方登場留言] S7 --> S8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json] - S8 --> S9[9 嚴重問題逐條掛行留言] + S8 --> S2[2 延後將舊留言標記解決(成功產生結果後才執行)] + S2 --> S9[9 嚴重問題逐條掛行留言] S9 --> S10[10 警告+建議彙整表格留言] S10 --> E1[收尾 commit/push + exit code] ``` -- 2.53.0 From 4c2b6d426569441d6c3d8ad892117b9fcdee9e11 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 16:01:05 +0800 Subject: [PATCH 28/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E8=A8=98=E5=85=A5=E8=AD=B0=E9=A1=8C=20#8=20=E4=B8=8D=E6=8E=A1?= =?UTF-8?q?=E7=B4=8D=E6=8E=92=E9=99=A4=E4=B8=A6=E5=AF=AB=E5=9B=9E=E5=BE=85?= =?UTF-8?q?=E4=BA=BA=E5=B7=A5=E8=99=95=E7=90=86=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/ai-review/exclusions.json | 11 ++ .../findings/2026-07-20-15:57:46.json | 143 ++++++++++++++++++ 2 files changed, 154 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-20-15:57:46.json diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index ef1ad2f..f60ee32 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -185,5 +185,16 @@ "endLine": 81, "problem": "新增的 AI CLI 失敗診斷只在失敗與 debug 模式執行,但沒有測試覆蓋其邊界:`res`/`error` 缺值、逾時與 signal、debug 大小寫及空白、控制字元單行化、500 字截斷,以及多種 token/URL 帳密遮罩。這些失敗路徑若回歸,可能使診斷完全缺失或將未遮罩內容寫入 CI log。", "reason": "Paladin:可排除(與歷史 finding 重複)。歷史 finding 已記錄 AI CLI 失敗診斷在退出狀態、空輸出、單行化、截斷及相關邊界缺少測試;本條是對同一測試缺口的細項枚舉。" + }, + { + "addedAt": "2026/07/20 15:57:46", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 268, + "endLine": 268, + "problem": "舊審查留言仍在本回合結果完整發布前就被標記為過時;建議把清理舊留言移到本輪所有必要結果均成功產生之後,避免失敗重跑留下「舊結果已清除、新結果不完整」的中間狀態。", + "reason": "維護者裁示(議題 #8 留言 #4838,系統管理員):此功能用途為把前一輪 PR 產生的訊息標記為已解決/過時,讓審查人員辨識舊訊息,因此刻意在本回合結果之前執行,不可延後,否則會誤導審查人員。依維護者決定不採納此修改,記為排除以免後續審查重複提出。" } ] diff --git a/.gitea/ai-review/findings/2026-07-20-15:57:46.json b/.gitea/ai-review/findings/2026-07-20-15:57:46.json new file mode 100644 index 0000000..66f479e --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-20-15:57:46.json @@ -0,0 +1,143 @@ +{ + "generatedAt": "2026/07/20 15:57:46", + "commitSha": "a6534ee0764fe2363ebbb5525586ad3868dc6720", + "prNumber": 6, + "tool": { + "name": "code-review-resolve", + "version": "0.0.8", + "model": "(工具預設)" + }, + "findings": [ + { + "id": "F001", + "reviewer": "Assassin", + "focus": "security", + "badge": "🗡️", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 72, + "endLine": 81, + "problem": "開啟 ACTIONS_STEP_DEBUG=true 後,會把攻擊者可間接操控的 AI CLI stderr/stdout(經黑名單式 redactSecrets 遮罩)寫入長期保存的 CI log。短 token、JWT、含標點或空白的密碼、非典型金鑰及 PII 仍可能繞過遮罩。建議即使除錯模式也只記錄退出碼/signal/逾時/診斷 ID,若需原文則寫入受限、短期保存、需授權取得的安全 artifact。", + "suggestion": "CI log 不輸出 AI CLI 原文,即使 debug 模式;需要原文時改寫入存取受限的安全 artifact,並套用允許清單式結構化診斷與 PII/機密掃描。此為安全性與可除錯性的政策取捨:現行已刻意保留 debug-gated 遮罩輸出,是否完全移除需維護者裁示。", + "suggestedCode": "function agentFailureDetail(res) {\n const err = res && res.error;\n if (err && err.killed) return '已逾時終止';\n if (err && typeof err.code === 'number') return `exit ${err.code}`;\n if (err && err.signal) return `signal ${err.signal}`;\n return 'AI CLI 執行失敗(原始輸出已隱藏)';\n}" + }, + { + "id": "F002", + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 176, + "endLine": 218, + "problem": "main() 內新增 issueBuffer、可變 issue、postComment 與 ensureIssueCreated 閉包,後續多處依 ctx.createIssue 分支。留言路由、issue 生命週期、標籤、相依關係與審查編排共享同一批可變狀態;未來增加發布目的地或重試策略時須同步理解並修改整個超長主流程,測試也只能透過 main() 間接覆蓋。", + "suggestion": "抽出具明確介面的發布器(如 PrReviewPublisher 與 IssueReviewPublisher),封裝留言暫存、issue 建立、沖刷、問題明細與收束關聯;main() 只呼叫一致的 publishContext/publishFindings/finalize,便於分別注入假的 Gitea client 測試兩種模式並移除散落的模式判斷。屬大範圍重構+設計取捨,需維護者確認方向。", + "suggestedCode": "" + }, + { + "id": "F003", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/index.js", + "startLine": 184, + "endLine": 245, + "problem": "建問題模式新增「issue 建立前暫存留言、建立後依序沖刷」的狀態流程,但沒有對應測試。尚未驗證 createIssue 或沖刷途中拋錯、空 buffer、重複呼叫、以及一般/建問題模式留言目的地是否正確;失敗路徑可能造成 issue 已建立但內容不完整或留言誤發到 PR。", + "suggestion": "補主流程整合測試並 mock Gitea API,驗證:一般模式直接發 PR 且記錄留言 id;issue 未建立時只暫存;建立後依序沖刷並清空 buffer;createIssue 與第 N 則沖刷留言失敗時以失敗結束且不再發布後續內容。屬測試架構決策(專案目前無測試框架)。", + "suggestedCode": "" + }, + { + "id": "F004", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/index.js", + "startLine": 309, + "endLine": 395, + "problem": "建問題模式核心分支被大幅改寫,但沒有測試驗證各種 findings 組合與 API 失敗行為。kept.length===0、只有嚴重、只有警告/建議、兩者皆有,以及標籤挑選/issue 建立/PR 回貼連結/相依 API 失敗等路徑尚未試煉,無法確認「無問題完全靜默」「有問題才建 issue」及相依失敗僅降級等契約成立。", + "suggestion": "以參數化測試覆蓋 findings 四種組合,斷言 API 呼叫順序/次數/issue number/統計;並分別讓 listLabels/selectLabels/createIssue/問題留言/PR 連結留言/addIssueDependency 拋錯,驗證哪些中止、哪些僅記警告續行;零 findings 時斷言所有寫入 API 皆不呼叫。屬測試架構決策。", + "suggestedCode": "" + }, + { + "id": "F005", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitea.js", + "startLine": 171, + "endLine": 215, + "problem": "新增的問題相依 API 包裝沒有測試驗證 endpoint、HTTP method 與 payload。特別是 addIssueDependency 容易顛倒的「PR 相依於 issue」方向未被斷言;若 index 或 body 欄位放反,合併阻擋語意會相反。(原併列的 addLabelsToIssue 已於本次移除。)", + "suggestion": "mock 底層 API,對相依關係使用不同的 PR/issue 編號,精確斷言 URL 指向 PR、body.index 指向阻擋來源 issue,並覆蓋 API 非 2xx 時錯誤原樣往上拋出的案例。屬測試架構決策。", + "suggestedCode": "" + }, + { + "id": "F006", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 103, + "endLine": 157, + "problem": "resolveMergeBase 新增多階段 fetch/重試策略,卻沒有測試鎖定淺層與失敗路徑:首次成功提早返回、非 shallow 不 unshallow、某次 fetch 失敗後仍嘗試下一策略、補抓成功立即停止、全部失敗時診斷與 cause 是否完整;易因呼叫順序或 off-by-one 在 runner 上才暴露。", + "suggestion": "將 git 執行器注入或 stub,建立表格化案例覆蓋首次成功/unshallow 後成功/deepen base 後成功/deepen HEAD 後成功/各 fetch 個別失敗/全部失敗;逐案斷言 git 參數與順序、成功後不再額外 fetch,並檢查最終錯誤含各策略成敗摘要且保留首次錯誤為 cause。屬可測性重構+測試架構決策。", + "suggestedCode": "" + }, + { + "id": "F007", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 249, + "endLine": 289, + "problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)", + "suggestion": "mock git 執行器與 URL/env 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。", + "suggestedCode": "" + }, + { + "id": "F008", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 16, + "endLine": 80, + "problem": "redactSecrets/agentFailureDetail 直接決定 CI 日誌是否洩漏內容及失敗診斷是否可用,但沒有測試覆蓋。空值、控制字元、Authorization/Bearer、URL 帳密、各種 token 樣式、截斷,以及 ACTIONS_STEP_DEBUG 大小寫與未啟用時隱藏 stdout/stderr 等邊界尚未驗證。", + "suggestion": "為這兩個純函式補單元測試(必要時受控匯出或抽獨立模組),以假憑證逐一測試遮罩規則與換行注入並斷言輸出不含原始秘密;保存還原 ACTIONS_STEP_DEBUG 驗證預設/true/混合大小寫/逾時/exit code/signal/無 error/超長輸出。屬測試架構決策。", + "suggestedCode": "" + }, + { + "id": "F009", + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "action.yml", + "startLine": 47, + "endLine": 61, + "problem": "push-token 的用途與退回行為在區塊註解、欄位描述、required 與 default 註解中反覆說明,且單行 description 過長,資訊雖完整但重複,日後修改語意易只改到一處。", + "suggestion": "保留一段「為何需要 PAT」的必要背景,其餘讓欄位名稱、required、default 自行表意,將 description 收斂成呼叫端真正需要知道的契約。註:本專案採 doc-funcs 高密度註解慣例,是否精簡屬慣例取捨,需維護者確認。", + "suggestedCode": " # 專用推送 PAT;以 PAT 推送可重新觸發 CI。留空時沿用 token。\n push-token:\n description: '推送審查結果 commit 的 PAT(留空時沿用 token)'\n required: false\n default: ''" + }, + { + "id": "F010", + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 203, + "endLine": 226, + "problem": "ensureIssueCreated 之名帶有「已存在便沿用」的冪等語意,實際卻無條件建立新 issue 並悄悄改寫外層 issue;名稱、行為與副作用不一致,閱讀呼叫處易形成錯誤預期。", + "suggestion": "若此函式只允許呼叫一次,改用直接表達「建立並沖刷暫存留言」的名稱並回傳建立結果,由呼叫端明確指派 issue,讓資料流一眼可見。註:本項與 F002(抽出 publisher 大重構)指向同一段核心流程、維護者正審視中,宜與該重構一併處理,避免重複改動。", + "suggestedCode": "const createIssueAndFlushBuffer = async (labelIds = []) => {\n const createdIssue = await gitea.createIssue(ctx, {\n title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`,\n body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),\n labels: labelIds,\n });\n for (const body of issueBuffer) {\n await gitea.createCommentOnIssue(ctx, createdIssue.number, body);\n }\n issueBuffer.length = 0;\n return createdIssue;\n};\nissue = await createIssueAndFlushBuffer(labelIds);" + } + ], + "excluded": [] +} -- 2.53.0 From 7823718ff436577910a9adaec9f3cb46b966dd51 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 08:03:45 +0000 Subject: [PATCH 29/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 132 +++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index f60ee32..e7c63d0 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -196,5 +196,137 @@ "endLine": 268, "problem": "舊審查留言仍在本回合結果完整發布前就被標記為過時;建議把清理舊留言移到本輪所有必要結果均成功產生之後,避免失敗重跑留下「舊結果已清除、新結果不完整」的中間狀態。", "reason": "維護者裁示(議題 #8 留言 #4838,系統管理員):此功能用途為把前一輪 PR 產生的訊息標記為已解決/過時,讓審查人員辨識舊訊息,因此刻意在本回合結果之前執行,不可延後,否則會誤導審查人員。依維護者決定不採納此修改,記為排除以免後續審查重複提出。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 54, + "endLine": 75, + "problem": "開啟 `ACTIONS_STEP_DEBUG=true` 後會把 AI CLI 的原始 stderr/stdout 寫入永久 CI log,但 `redactSecrets` 是可繞過的黑名單,也完全沒有移除 PII。攻擊者可透過惡意 diff/提示注入,要求 CLI 以未涵蓋的格式輸出憑證、電子郵件或其他敏感資料;例如短版金鑰、含標點的 token、JWT 或拆段輸出都可能避開現有正規表示式,造成機密與 PII 外洩。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 debug 模式輸出 AI CLI stderr/stdout、黑名單遮罩可繞過及敏感內容外洩提出相同問題。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 220, + "endLine": 231, + "problem": "`ensureIssueCreated` 之名暗示「若尚未建立才建立」的冪等保證,實作卻會無條件建立新 issue。即使目前只呼叫一次,命名仍會誤導後續維護者,長註解也無法替代準確的動詞。", + "reason": "Paladin:可排除(重複)。歷史 finding 已明確指出 `ensureIssueCreated` 名稱暗示冪等沿用,實際卻建立新 issue 並修改外層狀態。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "action.yml", + "startLine": 45, + "endLine": 57, + "problem": "新增的 `push-token` 以區塊註解、欄位描述、選填註解與預設值註解反覆敘述「PAT 觸發 CI、留空退回 token」;同一旋律重奏過多,真正重要的使用方式反而埋在十餘行說明裡,且明顯比鄰近 input 冗長。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 `push-token` 在註解、description、required 與 default 說明中重複敘述相同問題。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 234, + "problem": "`main()` 新增 `issueBuffer`、可變的 `issue`、`postComment` 與 `ensureIssueCreated`,把「決定留言目的地、建立 issue、暫存與沖刷留言」等多項職責綁在閉包狀態中。後續流程又必須知道 issue 是否已建立並直接讀取 `issue.number`;半年後若增加留言種類、重試或其他發布目的地,維護者得同時修改多個分支,也很難隔離單元測試發布狀態轉換。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。已知排除事項與歷史 finding 均已涵蓋 `main()` 內留言路由、issue 狀態、緩衝佇列及發布職責耦合的問題。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 221, + "endLine": 234, + "problem": "`ensureIssueCreated` 在所有暫存留言成功寫入前,就先把閉包中的 `issue` 設為已建立;沖刷途中任何一則 API 呼叫失敗,都會留下只寫入部分內容的 issue,且主流程沒有保存進度或可重入機制。重新執行時又會建立另一個 issue。這種部分完成狀態日後很難追查,也會讓重試與錯誤處理持續累積例外分支。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。issue 建立後沖刷留言失敗、留下部分完成狀態並在重跑時建立重複 issue,正是既有排除事項與歷史 finding 記錄的同一問題。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 342, + "endLine": 344, + "problem": "一般模式在發布新的嚴重/警告留言前,就先把舊審查留言標為過時。最小重現:`resolveOldComments` 成功後,步驟 9 的 `createReview` 或步驟 10 的留言 API 暫時失敗;本次 action 以失敗結束,但上一回合完整結果已被清除,新結果也未完整發布,PR 會失去可信的審查結果。這仍違反本次改動宣稱的「沒有新結果時保留舊結果」語義。", + "reason": "Paladin:可排除(命中已知排除事項)。維護者已明確裁示舊留言必須刻意在本回合結果之前標記為過時,以免舊資訊繼續誤導審查人員。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Mage", + "severity": "警告", + "file": "src/index.js", + "startLine": 224, + "endLine": 233, + "problem": "追蹤 issue 的建立與暫存留言寫入不是可重入操作。最小重現:`createIssue` 成功,但寫入 `issueBuffer` 第 2 則留言時 API 失敗;action 結束前沒有在 PR 留下連結或保存已建立的 issue 編號。工作流程重跑時會再建立一個新 issue,留下孤立且內容不完整的舊 issue,並可能持續產生重複 issue。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。建立 issue 後留言寫入失敗、未保存可恢復狀態而使重跑建立重複 issue,已由多筆既有排除事項及歷史 finding 完整涵蓋。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 311, + "endLine": 386, + "problem": "建問題模式的核心流程已大幅改寫,但本次 diff 沒有對應測試驗證:有保留問題時才建立 issue、標籤失敗時降級、暫存留言依序送出、嚴重與其他問題分流、PR 回貼連結,以及相依 API 失敗不阻斷流程。這些分支牽涉多次外部呼叫與狀態變化,未經測試容易出現 issue 未建立卻存取 `issue.number`、留言遺失或呼叫順序錯誤。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋建問題模式的 issue 建立條件、標籤降級、暫存留言、問題分流、PR 回貼及相依 API 失敗等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 103, + "endLine": 151, + "problem": "`resolveMergeBase` 新增多階段 fetch 復原與錯誤診斷,但沒有測試覆蓋淺層與失敗路徑。尚未驗證 unshallow 成功後會立即停止、fetch 失敗會繼續下一策略、非淺層 repo 不會執行 unshallow,以及所有策略失敗時的錯誤與 `cause` 是否正確;這類 git 邊界通常只會在 CI 的 shallow checkout 才暴露。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 `resolveMergeBase` 的淺層與非淺層分支、多階段 fetch、停止條件及最終錯誤資訊缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 270, + "endLine": 313, + "problem": "新增的 PAT 推送與憑證注入路徑沒有對應測試。尚未驗證有 `pushToken` 時必須略過 origin、無 `pushToken` 時只有 origin 失敗才使用一般 token、Authorization 僅透過環境變數傳入,以及推送失敗時不會把 token、遠端 URL或原始命令帶入錯誤。這些未驗證行為同時影響 CI 是否重觸發與失敗路徑的機密保護。", + "reason": "Paladin:可排除(重複)。歷史 finding 已明確記錄 pushToken、origin 與一般 token 的兩套推送策略、認證資訊遮蔽及失敗路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 16, + "endLine": 80, + "problem": "新增的失敗診斷與機密遮罩會處理不可信的 stderr/stdout,但沒有測試驗證邊界與失敗格式。特別是換行控制字元、Authorization、URL 帳密、各種 token 格式、超長輸出截斷,以及 `ACTIONS_STEP_DEBUG` 開關;任何漏測都可能造成除錯模式洩漏機密或產生可偽造的多行 CI 日誌。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 `redactSecrets`/`agentFailureDetail` 對控制字元、各類憑證、URL 帳密、截斷及 debug 開關等邊界缺少測試。" + }, + { + "addedAt": "2026/07/20 16:03:44", + "prNumber": 6, + "reviewer": "Rogue", + "severity": "警告", + "file": "src/index.js", + "startLine": 224, + "endLine": 226, + "problem": "`issueBuffer` 內每則留言都逐一 `await` 遠端 API,總耗時為 O(n × API RTT)。目前固定情境留言也會累加多次網路往返;若日後增加階段,等待時間會線性成長。", + "reason": "Paladin:可排除(命中已知排除事項)。逐筆 `await` 是為維持暫存留言的 FIFO 發布順序,留言數量有限;現有證據不足以證明延遲構成缺陷,而並行發送會增加順序不確定及 API 限流風險。" } ] -- 2.53.0 From 7b5c7c3e4ffdc4ba8a11900ed87241aadc629423 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 16:17:34 +0800 Subject: [PATCH 30/79] =?UTF-8?q?chore(ci):=20=E5=AF=A9=E6=9F=A5=20job=20?= =?UTF-8?q?=E7=9A=84=20token=20=E6=94=B9=E7=94=A8=20secrets.TOKEN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yaml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index c623ab1..69629f9 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -42,8 +42,8 @@ jobs: uses: ./ # 傳入 action inputs。 with: - # Gitea Actions 自動注入的 repo 範圍 token,用於 PR 留言與 findings 寫回。 - token: ${{ gitea.token }} + # PR 留言與 findings 寫回用的 token(呼叫端以 secrets 傳入)。 + token: ${{ secrets.TOKEN }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, @@ -73,8 +73,8 @@ jobs: uses: ./ # 傳入 action inputs。 with: - # Gitea Actions 自動注入的 repo 範圍 token,用於 PR 留言與 findings 寫回。 - token: ${{ gitea.token }} + # PR 留言與 findings 寫回用的 token(呼叫端以 secrets 傳入)。 + token: ${{ secrets.TOKEN }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, @@ -104,8 +104,8 @@ jobs: uses: ./ # 傳入 action inputs。 with: - # Gitea Actions 自動注入的 repo 範圍 token,用於 PR 留言與 findings 寫回。 - token: ${{ gitea.token }} + # PR 留言與 findings 寫回用的 token(呼叫端以 secrets 傳入)。 + token: ${{ secrets.TOKEN }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, -- 2.53.0 From 71b93a885a068701fc7a99cd624d9cab8bba3a3a Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 16:17:34 +0800 Subject: [PATCH 31/79] =?UTF-8?q?fix(AI=20=E5=A4=B1=E6=95=97=E8=A8=BA?= =?UTF-8?q?=E6=96=B7):=20CLI=20=E5=A4=B1=E6=95=97=E9=A0=90=E8=A8=AD?= =?UTF-8?q?=E8=BC=B8=E5=87=BA=E7=B6=93=E9=81=AE=E7=BD=A9=E7=9A=84=20stderr?= =?UTF-8?q?=20=E7=89=87=E6=AE=B5=E4=BB=A5=E5=88=A9=E9=99=A4=E9=8C=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/review.js | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/lib/review.js b/src/lib/review.js index 172f22c..6b45694 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -18,8 +18,8 @@ const TOTAL_DIFF_LIMIT = 160_000; * * 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽 * `Authorization` 標頭、`token=`/`token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/ - * 長 hex/`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,故僅在 - * 明確開啟除錯輸出時作為第二道防線使用(見 {@link agentFailureDetail})。 + * 長 hex/`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出 + * CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。 * * @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。 * @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。 @@ -40,10 +40,11 @@ function redactSecrets(text) { * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。 * * 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或 - * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。故本函式**預設只輸出** - * exit code/signal/killed 等不含機密的分類資訊;**僅在明確開啟 Actions step debug** - * (環境變數 `ACTIONS_STEP_DEBUG=true`)時,才附上經 {@link redactSecrets} 遮罩且去除 - * 控制字元的 stderr/stdout 片段(各截前 500 字)作為診斷第二選擇。純函式、不拋例外。 + * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。權衡「可除錯性」後:本函式 + * 於失敗時**預設**附上經 {@link redactSecrets} 遮罩且去除控制字元的 **stderr** 片段 + * (先截去過長輸入再取前 500 字)——只印 exit code 幾乎無從判斷 CLI + * 為何失敗;**另在明確開啟 Actions step debug**(環境變數 `ACTIONS_STEP_DEBUG=true`)時, + * 再附上同樣遮罩的 **stdout** 片段作為第二診斷來源。純函式、不拋例外。 * * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res * `runAgent` 的回傳物件。 @@ -63,20 +64,21 @@ function agentFailureDetail(res) { else if (err.code) parts.push(`code ${err.code}`); else if (err.signal) parts.push(`signal ${err.signal}`); } - // 預設不輸出 AI CLI 原始 stderr/stdout(可能含 token、PII 或原始碼祕密); - // 僅在明確開啟 Actions step debug 時,附上「已遮罩+去控制字元」的片段作為診斷。 + // 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描; + // 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。 + const DEBUG_INPUT_LIMIT = 2_000; + // 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 片段——CLI 失敗時若只印 + // exit code 幾乎無從除錯(見 test-claude 秒失敗案例);遮罩為盡力防線,仍以此權衡可除錯性。 + const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, DEBUG_INPUT_LIMIT)); + if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); + // 明確開啟 Actions step debug 時,再附上 stdout 片段作為第二診斷來源。 const verbose = String(process.env.ACTIONS_STEP_DEBUG || '').trim().toLowerCase() === 'true'; if (verbose) { - // 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描; - // 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。 - const DEBUG_INPUT_LIMIT = 2_000; - const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, DEBUG_INPUT_LIMIT)); - if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); const stdout = redactSecrets(String((res && res.output) || '').slice(0, DEBUG_INPUT_LIMIT)); if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); } if (parts.length === 0) { - parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(詳細輸出已隱藏)'); + parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); } return parts.join('|'); } -- 2.53.0 From afed9a3ccea38425a1b291b0f00abf31638758c3 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 08:20:40 +0000 Subject: [PATCH 32/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 121 +++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index e7c63d0..1b0b4f2 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -328,5 +328,126 @@ "endLine": 226, "problem": "`issueBuffer` 內每則留言都逐一 `await` 遠端 API,總耗時為 O(n × API RTT)。目前固定情境留言也會累加多次網路往返;若日後增加階段,等待時間會線性成長。", "reason": "Paladin:可排除(命中已知排除事項)。逐筆 `await` 是為維持暫存留言的 FIFO 發布順序,留言數量有限;現有證據不足以證明延遲構成缺陷,而並行發送會增加順序不確定及 API 限流風險。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 73, + "endLine": 74, + "problem": "攻擊者可在 PR diff 中植入不符合現有正規表示式的機密或 PII,再誘使 AI CLI 失敗並將提示內容回顯至 stderr;此處預設把前 500 字寫入 CI log。`redactSecrets` 僅是樣式黑名單,無法遮蔽電子郵件、短 token、含特殊符號的憑證或原始碼敏感資料,會形成可長期讀取的資料外洩通道。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對除錯模式輸出 AI CLI stderr/stdout、黑名單式遮罩可繞過,以及敏感內容或 PII 外洩提出相同問題。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 179, + "endLine": 232, + "problem": "`main()` 新增了 `issueBuffer`、可變的 `issue`,以及兩個捕捉外部狀態的區域函式,使原本已相當冗長的流程編排同時承擔留言路由、緩衝、issue 建立與緩衝區清空等細節。尤其 `ensureIssueCreated` 這個名稱暗示可重複安全呼叫,實作卻會無條件建立新 issue,名稱與行為並不押韻。", + "reason": "Paladin:可排除(重複)。歷史 findings 已分別涵蓋 main() 承擔留言路由與 issue 狀態管理,以及 ensureIssueCreated 名稱暗示冪等、實際卻建立新 issue 的問題。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "action.yml", + "startLine": 47, + "endLine": 58, + "problem": "`push-token` 周圍的註解重複敘述 PAT、CI 再觸發、步驟 1 快速回報與空值退回行為,且 `description` 又把同一段旋律再奏一次。Action manifest 因實作細節過密而難以快速掃讀,也與其他 input 較精簡的註解密度不一致。", + "reason": "Paladin:可排除(重複)。歷史 finding 與已知排除事項已記錄 push-token 在註解、description、required 與 default 說明中重複敘述相同資訊。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 181, + "endLine": 231, + "problem": "`main()` 新增以 `issueBuffer`、`issue` 及兩個閉包管理追蹤 issue,後續又在主流程內負責挑標籤、建立 issue、排空留言、發布各類問題、回貼 PR 與設定相依關係。建問題模式的狀態與發布規則因此散落於整個超長函式;未來新增留言種類或調整建立時機時,維護者必須同步追蹤多處 `ctx.createIssue` 分支與可變閉包狀態,容易漏改,也難以獨立單元測試部分失敗及重試行為。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 生命週期、緩衝佇列及發布職責耦合,並提出相同的發布器抽離方向。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 345, + "endLine": 347, + "problem": "舊留言在新問題留言真正發布成功前就被標記為過時。最小重現:`resolveOldComments` 成功後,步驟 9 的 `postSevereComments` 或步驟 10 的 `postComment` 因暫時性 API 錯誤失敗;本次流程以例外結束,但舊審查結果已被清除,PR 上只剩工具/角色等情境留言,沒有任何有效 finding。這仍違反此次延後清理所宣稱的「成功產生結果後才執行」語義。", + "reason": "Paladin:可排除(命中已知排除事項)。維護者已明確裁示舊留言應刻意在本回合結果發布前標記為過時,以免舊資訊持續誤導審查人員。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Mage", + "severity": "警告", + "file": "src/index.js", + "startLine": 219, + "endLine": 229, + "problem": "建立追蹤 issue 與寫入暫存留言不是可恢復的原子流程。最小重現:`createIssue` 成功,但寫入第 2 則 `issueBuffer` 留言時 API 失敗;流程中止後已留下內容不完整且沒有 PR 回鏈的 issue,下次重跑又會建立另一個 issue,造成重複追蹤與狀態分歧。後續 PR 回鏈、相依設定或 commit/push 失敗也有相同的重跑問題。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。建立 issue 後留言或後續步驟失敗、重跑無法沿用既有 issue 而產生孤兒或重複 issue,已由多筆既有紀錄完整涵蓋。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 105, + "endLine": 157, + "problem": "`resolveMergeBase` 新增多階段 fetch/merge-base 復原流程,但本次變更沒有對應測試。淺層與非淺層儲存庫、fetch 失敗後繼續、任一策略成功即停止,以及全部失敗時的診斷與 `cause` 都是新增行為;沒有驗證就可能在 runner 的 shallow checkout 上才暴露錯誤或多餘 fetch。", + "reason": "Paladin:可排除(重複)。歷史 finding 已記錄 resolveMergeBase 多階段 fetch 在淺層、非淺層、策略停止、降級與最終失敗診斷等路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 247, + "endLine": 311, + "problem": "新增 `pushToken` 與 `pushWithCredential` 後,推送分支及失敗路徑未見測試覆蓋。尚未驗證有 PAT 時確實略過 origin、無 PAT 時先推 origin 再以 `token` 降級、認證只進環境變數,以及失敗錯誤不帶 token/遠端 URL;這些都是本次新增且直接影響結果 commit 能否推送的行為。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 pushToken、origin 與一般 token 的推送策略、呼叫順序、認證遮蔽及失敗路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 309, + "endLine": 386, + "problem": "建問題模式改成「有保留問題才建 issue」、緩衝留言、逐類發布、回貼 PR 與設定相依關係,但本次沒有流程測試驗證這組新增分支。尤其 `kept` 為空、只有嚴重問題、只有警告/建議、標籤挑選失敗,以及相依 API 失敗降級等路徑,若接線錯誤可能造成漏留言、對 `null` issue 取值,或非預期中斷審查。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的 issue 建立條件、暫存留言、問題分流、標籤降級、PR 回貼及相依 API 失敗等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 16, + "endLine": 85, + "problem": "新增的失敗診斷與機密遮罩沒有對應測試,尚未驗證空值、控制字元、多種 token 格式、URL 內嵌帳密、500/2000 字截斷邊界,以及 `ACTIONS_STEP_DEBUG` 開關。這段程式會處理失敗路徑中的非可信 CLI 輸出;若正規表示式或截斷順序退化,測試無法及時發現機密殘留或假日誌換行。", + "reason": "Paladin:可排除(重複)。歷史 finding 已逐項記錄失敗診斷與遮罩對空值、控制字元、各類憑證、URL 帳密、截斷及 ACTIONS_STEP_DEBUG 開關缺少測試。" + }, + { + "addedAt": "2026/07/20 16:20:39", + "prNumber": 6, + "reviewer": "Maya", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 171, + "endLine": 181, + "problem": "新 API `addIssueDependency` 未見測試驗證 endpoint 與 IssueMeta payload。`issueNumber` 與 `dependency` 的方向一旦顛倒,請求仍可能成功,卻會建立相反的阻擋關係;只靠主流程成功測試不容易察覺。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向的測試,與本條指控相同。" } ] -- 2.53.0 From 491c170d366b162d758334732ff97fac5bc08b19 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 16:37:15 +0800 Subject: [PATCH 33/79] =?UTF-8?q?fix(AI=20=E5=A4=B1=E6=95=97=E8=A8=BA?= =?UTF-8?q?=E6=96=B7):=20=E5=A4=B1=E6=95=97=E6=99=82=E9=A0=90=E8=A8=AD?= =?UTF-8?q?=E4=BD=B5=E5=8D=B0=E9=81=AE=E7=BD=A9=E5=BE=8C=20stdout=EF=BC=88?= =?UTF-8?q?claude-code=20=E9=8C=AF=E8=AA=A4=E5=AF=AB=E5=9C=A8=20stdout?= =?UTF-8?q?=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/review.js | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/lib/review.js b/src/lib/review.js index 6b45694..0125a22 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -41,10 +41,9 @@ function redactSecrets(text) { * * 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或 * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。權衡「可除錯性」後:本函式 - * 於失敗時**預設**附上經 {@link redactSecrets} 遮罩且去除控制字元的 **stderr** 片段 - * (先截去過長輸入再取前 500 字)——只印 exit code 幾乎無從判斷 CLI - * 為何失敗;**另在明確開啟 Actions step debug**(環境變數 `ACTIONS_STEP_DEBUG=true`)時, - * 再附上同樣遮罩的 **stdout** 片段作為第二診斷來源。純函式、不拋例外。 + * 於失敗時**預設**附上經 {@link redactSecrets} 遮罩且去除控制字元的 **stderr 與 stdout** + * 片段(各先截去過長輸入再取前 500 字)——只印 exit code 幾乎無從判斷 CLI 為何失敗, + * 且部分 CLI(如 claude-code 的 `-p` 模式)將錯誤寫到 stdout 而非 stderr。純函式、不拋例外。 * * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res * `runAgent` 的回傳物件。 @@ -66,17 +65,14 @@ function agentFailureDetail(res) { } // 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描; // 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。 - const DEBUG_INPUT_LIMIT = 2_000; - // 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 片段——CLI 失敗時若只印 - // exit code 幾乎無從除錯(見 test-claude 秒失敗案例);遮罩為盡力防線,仍以此權衡可除錯性。 - const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, DEBUG_INPUT_LIMIT)); + const INPUT_LIMIT = 2_000; + // 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時 + // 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的 + // -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。 + const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT)); if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); - // 明確開啟 Actions step debug 時,再附上 stdout 片段作為第二診斷來源。 - const verbose = String(process.env.ACTIONS_STEP_DEBUG || '').trim().toLowerCase() === 'true'; - if (verbose) { - const stdout = redactSecrets(String((res && res.output) || '').slice(0, DEBUG_INPUT_LIMIT)); - if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); - } + const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT)); + if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); if (parts.length === 0) { parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); } -- 2.53.0 From 85235e43a4eb74dcd6beaf7396adaadd7af2ae32 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 08:39:40 +0000 Subject: [PATCH 34/79] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/exclusions.json | 121 +++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 1b0b4f2..96f241e 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -449,5 +449,126 @@ "endLine": 181, "problem": "新 API `addIssueDependency` 未見測試驗證 endpoint 與 IssueMeta payload。`issueNumber` 與 `dependency` 的方向一旦顛倒,請求仍可能成功,卻會建立相反的阻擋關係;只靠主流程成功測試不容易察覺。", "reason": "Paladin:可排除(重複)。歷史 finding 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向的測試,與本條指控相同。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 74, + "endLine": 77, + "problem": "攻擊者可在 PR diff 中植入機密或個資,誘使 AI CLI 在失敗時原樣回顯;此處卻預設把 stderr 與 stdout 寫入長期保存的 CI log。`redactSecrets()` 只是可繞過的黑名單,例如 `Authorization: Bearer <憑證>` 只會遮掉 `Bearer`,後方憑證仍會留下,且電子郵件、姓名、短 token 與未知格式完全不會被遮罩,違反回應不得含 PII 的規範。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出除錯模式輸出 AI CLI stderr/stdout、黑名單式遮罩可繞過,以及機密或個資外洩的相同問題。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 183, + "endLine": 229, + "problem": "新增的 `postComment`、`ensureIssueCreated`、`issueBuffer` 與可變的 `issue` 全部嵌在本就冗長的 `main()` 中,且第二個函式透過閉包同時讀寫多個外部狀態。讀者要在主流程、留言路由與 issue 生命週期三條旋律間來回切換,主流程輪廓因此被大量細節淹沒。", + "reason": "Paladin:可排除(重複)。與 F005 及歷史 finding 指涉相同的 main() 職責膨脹、閉包狀態與 issue 發布流程耦合問題。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 181, + "endLine": 229, + "problem": "`main()` 新增了 `issueBuffer`、`issue`、`postComment` 與 `ensureIssueCreated` 等可變狀態及閉包,並在後續流程散落多個 `ctx.createIssue` 分支。留言路由、issue 建立、緩衝區清空與主審查編排因此緊密耦合;半年後新增第三種發布方式或調整 issue 建立時機時,維護者必須同時追蹤整個 `main()` 的狀態轉移,也很難獨立單元測試發布行為。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布職責耦合的問題。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 344, + "endLine": 382, + "problem": "一般模式在嚴重問題與彙整留言發布前就呼叫 `resolveOldComments`。若後續 `postSevereComments` 或 `postComment` 因 API 暫時失敗而中斷,舊的完整審查結果已被標成過時,新回合卻只留下工具、diff、角色等情境留言。這與本次變更宣稱的「成功產生結果後才清舊留言」保證不一致,也讓未來維護者難以判斷何時才算發布完成。", + "reason": "Paladin:可排除(命中已知排除事項)。維護者已明確裁示舊留言須刻意在本回合結果發布前標記為過時,以免舊資訊繼續誤導審查人員。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 213, + "endLine": 225, + "problem": "建立追蹤 issue 與寫入暫存留言不是原子操作。最小重現:`gitea.createIssue` 成功後,第 2 則 `createCommentOnIssue` 因暫時性 API 錯誤失敗;主流程會中止,但遠端已留下內容不完整的 issue。工作重跑時沒有查找或續寫既有 issue 的機制,因此會再建立一張重複 issue,且先前已成功寫入的留言也可能重複。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。建立 issue 後沖刷暫存留言失敗、重跑產生孤兒或重複 issue,已由多筆既有紀錄完整涵蓋。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 342, + "endLine": 344, + "problem": "舊審查留言仍在新結果完整發布前被標記為過時。最小重現:防守方裁決與 findings 寫檔成功後,`resolveOldComments` 先清除舊結果;接著 `postSevereComments`、`postComment` 或最後的 commit/push 任一步失敗,流程便以失敗結束,PR 上只剩工具/角色等情境留言,既有問題已過時、新問題卻未完整發布。這仍違反本次改動宣稱的「失敗時保留舊結果」語義。", + "reason": "Paladin:可排除(命中已知排除事項且與 F006 重複)。維護者已裁示不得將舊留言清理延後至新結果完整發布之後。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 105, + "endLine": 159, + "problem": "新增的 merge-base 補抓流程沒有對應測試驗證。尚未確認非淺層/淺層 repository、首次 fetch 失敗、unshallow 成功、deepen base 或 HEAD 才成功,以及所有策略失敗時的診斷與 `cause`;這些分支會直接決定送審 diff 的基準,未經試煉仍可能漏審或錯審。", + "reason": "Paladin:可排除(重複)。歷史 finding 已記錄 resolveMergeBase 多階段 fetch 在淺層、非淺層、停止條件、降級與最終失敗診斷等路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 267, + "endLine": 308, + "problem": "新增的 `pushToken` 推送分流與 `pushWithCredential` 失敗降級沒有測試覆蓋。尚未驗證有 PAT 時確實略過 origin、無 PAT 時 origin 成功不重試、origin 失敗才使用一般 token,以及認證環境變數與固定錯誤訊息不會帶出 token;這是本次 CI 再觸發機制的核心行為。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 pushToken、origin 與一般 token 的推送策略、呼叫順序、認證遮蔽及失敗路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 309, + "endLine": 387, + "problem": "建問題模式被大幅改寫,但 diff 中沒有測試驗證各分支的外部行為。尤其 `kept` 為空時必須不建 issue、不碰 PR 留言;有問題時必須依序建立 issue、沖出暫存留言、發布各類 finding、回貼 PR 連結;標籤挑選與相依 API 失敗又必須降級而不中斷。這些成功與失敗路徑目前都未被驗證。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的 issue 建立條件、暫存留言、問題分流、標籤降級、PR 回貼及相依 API 失敗等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 16, + "endLine": 82, + "problem": "新增的失敗診斷與機密遮罩邏輯沒有測試驗證邊界。尚未確認 Authorization、token/password/secret、URL 內嵌帳密、GitHub 樣式 token、長字串、控制字元,以及機密剛好跨越 2,000 字截斷邊界時是否仍會被完整遮蔽;也未驗證 timeout、數字/字串 code、signal 與空輸出的摘要。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋失敗診斷與遮罩對空值、控制字元、各類憑證、URL 帳密、截斷及退出狀態等邊界缺少測試。" + }, + { + "addedAt": "2026/07/20 16:39:40", + "prNumber": 6, + "reviewer": "Maya", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 171, + "endLine": 195, + "problem": "新加入的 `addIssueDependency` API 封裝沒有契約測試,尚未驗證 URL 中使用相依方 PR 編號,而 request body 的 `index` 使用阻擋來源 issue 編號;兩者若對調,請求可能仍是合法格式,卻會建立反向的相依關係。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向的契約測試。" } ] -- 2.53.0 From 36bf2d8644d77876f3f1342fb4a5f74add273192 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 17:15:12 +0800 Subject: [PATCH 35/79] =?UTF-8?q?chore(ci):=20=E4=B8=89=E5=80=8B=E6=B8=AC?= =?UTF-8?q?=E8=A9=A6=20job=20=E6=8C=87=E5=AE=9A=E5=90=84=E5=B7=A5=E5=85=B7?= =?UTF-8?q?=E6=9C=80=E7=9C=81=20model=20=E4=BB=A5=E9=99=8D=E4=BD=8E?= =?UTF-8?q?=E6=B6=88=E8=80=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Antigravity→gemini-3.5-flash、Codex→gpt-5.5、Claude→claude-haiku-4-5 Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 69629f9..1d69ca8 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -46,6 +46,8 @@ jobs: token: ${{ secrets.TOKEN }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' + # 指定 AI 模型:CI 測試固定用 Antigravity 最省模型以降低消耗。 + model: gemini-3.5-flash # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 push-token: ${{ secrets.TOKEN }} @@ -77,6 +79,8 @@ jobs: token: ${{ secrets.TOKEN }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' + # 指定 AI 模型:CI 測試固定用 Codex 最省模型以降低消耗。 + model: gpt-5.5 # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 push-token: ${{ secrets.TOKEN }} @@ -108,6 +112,8 @@ jobs: token: ${{ secrets.TOKEN }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' + # 指定 AI 模型:CI 測試固定用 Claude 最省模型(Haiku 4.5)以降低消耗。 + model: claude-haiku-4-5 # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 push-token: ${{ secrets.TOKEN }} -- 2.53.0 From 321a717f91303bd4b865eacb0d11546ef9e79e11 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 17:17:42 +0800 Subject: [PATCH 36/79] =?UTF-8?q?chore(ci):=20Claude=20=E6=B8=AC=E8=A9=A6?= =?UTF-8?q?=20job=20=E6=94=B9=E7=94=A8=20claude-opus-4-8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 1d69ca8..d646e98 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -112,8 +112,8 @@ jobs: token: ${{ secrets.TOKEN }} # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 create-issue: 'true' - # 指定 AI 模型:CI 測試固定用 Claude 最省模型(Haiku 4.5)以降低消耗。 - model: claude-haiku-4-5 + # 指定 AI 模型:Claude job 使用 Opus 4.8。 + model: claude-opus-4-8 # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 push-token: ${{ secrets.TOKEN }} -- 2.53.0 From 9e6e8bb7938e656176dcfef0690e60f60bd585e9 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 09:21:17 +0000 Subject: [PATCH 37/79] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/exclusions.json | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 96f241e..85ca15b 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -570,5 +570,93 @@ "endLine": 195, "problem": "新加入的 `addIssueDependency` API 封裝沒有契約測試,尚未驗證 URL 中使用相依方 PR 編號,而 request body 的 `index` 使用阻擋來源 issue 編號;兩者若對調,請求可能仍是合法格式,卻會建立反向的相依關係。", "reason": "Paladin:可排除(重複)。歷史 finding 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向的契約測試。" + }, + { + "addedAt": "2026/07/20 17:21:16", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 70, + "endLine": 73, + "problem": "攻擊者可以把惡意內容塞進 PR diff 或 AI 工具輸出,誘使 AI CLI 在失敗時把環境變數、原始碼片段、token、PR 內容或 PII 印到 stdout/stderr;這裡預設把 stdout/stderr 前 500 字寫進 CI log。`redactSecrets` 只是黑名單式遮罩,擋不住 Gitea PAT、JWT、雲端金鑰、短 token、帶標點的密碼或一般 PII。尤其本 PR 又新增 `push-token` PAT,失敗診斷變成一條可被 prompt injection 利用的外洩通道。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 debug/失敗診斷輸出 AI CLI stdout/stderr、黑名單式遮罩可繞過,以及機密或個資外洩提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:21:16", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 365, + "problem": "`main()` 這次把「留言目的地切換、issue 暫存與 flush、標籤挑選、PR 回貼、issue dependency、舊留言清理時機」全部塞進同一段流程。半年後要改建問題模式時,維護者必須同時理解 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds` 這幾個閉包狀態的互動,任何一個留言新增在錯誤位置,都可能變成被暫存後丟棄、發錯地方,或被舊留言清理誤處理。這不是單純長度問題,而是 orchestration 和 delivery policy 已經耦合在一起。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布職責耦合的問題;本條只是擴充描述同一設計負擔。" + }, + { + "addedAt": "2026/07/20 17:21:16", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 30, + "endLine": 39, + "problem": "`agentFailureDetail()` 的說明文字前後不一致:開頭說「原始輸出預設隱藏」,下一段又說失敗時預設附上遮罩後的 stderr/stdout 片段。這種註解矛盾會讓未來維護者不確定目前政策到底是偏向保守隱藏,還是偏向可診斷輸出,尤其這段又是多個 AI CLI 失敗路徑共用的行為。", + "reason": "Paladin:可排除(列表內重複)。與 F002 指涉同一段 agentFailureDetail 註解前後政策描述不一致的問題。" + }, + { + "addedAt": "2026/07/20 17:21:16", + "prNumber": 6, + "reviewer": "Mage", + "severity": "警告", + "file": "src/index.js", + "startLine": 213, + "endLine": 224, + "problem": "`ensureIssueCreated` 先建立追蹤 issue,再逐則寫入 `issueBuffer`。只要 issue 建立成功後任一 buffered comment 發生暫時性 API 失敗,例外會往上拋出,action 以失敗結束,但已建立的 issue 不會回滾,也沒有被記錄成可重用狀態。\n\n最小重現情境:`create-issue=true`、有保留問題;`gitea.createIssue` 成功建立 #10;第一則 `createCommentOnIssue` timeout。這次 run 失敗;下次重跑又建立 #11,造成重複追蹤 issue,且 PR 可能沒有任何連結指向第一次建立的 issue。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。issue 建立成功後 buffered comment 失敗、重跑產生重複追蹤 issue,正是既有排除事項與歷史 finding 已裁定涵蓋的同一問題。" + }, + { + "addedAt": "2026/07/20 17:21:16", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 309, + "endLine": 381, + "problem": "建問題模式這次改了主要行為:有保留問題時才建 issue、先暫存工具/diff/角色留言、之後逐條留言到 issue、再回貼 PR 連結並建立 dependency;但 diff 沒看到對應測試。這條路徑牽涉多個外部 API 呼叫與順序,沒有測試就無法驗證「PR 只留連結、issue 收到完整內容、無保留問題時靜默通過」這些新契約。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式有 finding 才建 issue、暫存留言 flush、PR 只回貼連結、dependency 失敗降級等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 17:21:16", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 102, + "endLine": 146, + "problem": "`resolveMergeBase` 新增了多段 fetch fallback(明確更新 base、淺層時 unshallow、deepen base、deepen HEAD)以及失敗診斷,但沒有看到測試覆蓋這些邊界與失敗路徑。這段是 diff 基準來源;只要淺層 checkout 或 fetch 策略順序沒被驗證,審查範圍可能整個錯掉。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出 resolveMergeBase 多階段 fetch、淺層與非淺層路徑、停止條件及最終失敗診斷缺少測試。" + }, + { + "addedAt": "2026/07/20 17:21:16", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 267, + "endLine": 324, + "problem": "`push-token` 與 `pushWithCredential` 是新的提交推送行為,但 diff 沒有對應測試驗證分支選擇與失敗處理。尤其 `pushToken` 存在時會跳過 origin、未提供時才 fallback,這些都是會影響 CI 是否重新觸發的關鍵行為。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushToken、origin fallback、認證傳遞與推送失敗遮蔽等分支缺少測試。" + }, + { + "addedAt": "2026/07/20 17:21:16", + "prNumber": 6, + "reviewer": "Maya", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 13, + "endLine": 72, + "problem": "`redactSecrets` / `agentFailureDetail` 新增了輸出 stderr/stdout 診斷的行為,但沒有測試驗證遮罩規則與限長。這不是單純格式調整;一旦正規式或順序被改壞,失敗 log 可能變得不可診斷,或把控制字元與憑證樣式原樣輸出。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 redactSecrets/agentFailureDetail 的控制字元、憑證樣式、URL 帳密、截斷與 debug 開關缺少測試提出相同問題。" } ] -- 2.53.0 From 05178be5209d7b318cc92200344e2eada8d19890 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 17:24:18 +0800 Subject: [PATCH 38/79] =?UTF-8?q?fix(=E6=8E=A8=E9=80=81=E8=A7=B8=E7=99=BC?= =?UTF-8?q?=20CI):=20=E5=90=88=E4=BD=B5=20push-token=20=E9=80=B2=20token?= =?UTF-8?q?=EF=BC=8Cfindings=20=E4=B8=80=E5=BE=8B=E4=BB=A5=20token=20?= =?UTF-8?q?=E8=AA=8D=E8=AD=89=E6=8E=A8=E9=80=81=E8=A7=B8=E7=99=BC=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 移除 push-token input,token 兼作 Gitea API 認證與 findings 推送 - commitAndPushFindings 一律以 token 明確認證推送(不走 origin 自動 token); 只要 token 為能觸發 CI 的 PAT,結果 commit 即再觸發 PR 的 CI,避免新 head 缺檢查卡合併 - ci.yaml 三個 job 移除 push-token,保留 token: ${{ secrets.TOKEN }} Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/workflows/ci.yaml | 9 --------- action.yml | 21 ++++++--------------- src/index.js | 4 ++-- src/lib/context.js | 8 ++------ src/lib/gitrepo.js | 26 ++++++++------------------ 5 files changed, 18 insertions(+), 50 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index d646e98..0c1b7e7 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -48,9 +48,6 @@ jobs: create-issue: 'true' # 指定 AI 模型:CI 測試固定用 Antigravity 最省模型以降低消耗。 model: gemini-3.5-flash - # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, - # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 - push-token: ${{ secrets.TOKEN }} # Codex 工具環境測試 job。 test-codex: # Job 在 workflow UI 顯示的名稱。 @@ -81,9 +78,6 @@ jobs: create-issue: 'true' # 指定 AI 模型:CI 測試固定用 Codex 最省模型以降低消耗。 model: gpt-5.5 - # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, - # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 - push-token: ${{ secrets.TOKEN }} # Claude 工具環境測試 job。 test-claude: # Job 在 workflow UI 顯示的名稱。 @@ -114,6 +108,3 @@ jobs: create-issue: 'true' # 指定 AI 模型:Claude job 使用 Opus 4.8。 model: claude-opus-4-8 - # 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI, - # 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。 - push-token: ${{ secrets.TOKEN }} diff --git a/action.yml b/action.yml index 0cd08f6..68d0977 100644 --- a/action.yml +++ b/action.yml @@ -16,11 +16,13 @@ author: 'Jeffery' # 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入, # runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取。 inputs: - # Gitea API token:用於對 PR 留言審查結果、以及 push 審查結果檔回 repo。 + # Gitea API token:用於對 PR/issue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。 token: - # 參數用途說明:secrets/vars context 在 action 內不可用, - # 故由呼叫端 workflow 以 secrets.GITHUB_TOKEN 傳入。 - description: 'Gitea API token(PR 留言與 push findings 用;呼叫端以 secrets.GITHUB_TOKEN 傳入)' + # 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。 + # 建議傳入「能觸發 CI 的 PAT」:以自動 token(gitea.token / GITHUB_TOKEN)推送的結果 commit 不會 + # 再觸發 CI,導致新 head 缺檢查而卡合併;改用 PAT 推送會讓 PR 的 synchronize 事件再觸發 CI, + # 由主程式步驟 1 快速回報([success]/[failure])廉價地把結果蓋到新 head。 + description: 'Gitea API token(PR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)' # 必填:缺少 token 無法呼叫 Gitea API,action 無法運作。 required: true # 指定 AI 工具使用的模型名稱。 @@ -41,17 +43,6 @@ inputs: required: false # 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用)。 default: 'false' - # 推送用 PAT:推送 findings/exclusions commit 時改以此 token 進行。 - push-token: - # 參數用途說明:以自動 token(gitea.token / GITHUB_TOKEN)推送的 commit 不會再觸發 CI, - # 導致新 head 缺檢查而卡合併;改用 PAT 推送會讓 PR 的 synchronize 事件再觸發 CI, - # 由主程式步驟 1 快速回報([success]/[failure])廉價地把結果蓋到新 head。 - # 呼叫端以 secrets 傳入(例如 secrets.TOKEN)。留空=退回以 token 走 origin 推送(不會再觸發)。 - description: '推送 findings/exclusions commit 用的 PAT(讓 CI 再觸發;呼叫端以 secrets 傳入;留空=退回 token 推送、不再觸發)' - # 選填:未指定時退回以 token 推送。 - required: false - # 預設為空字串,代表不使用專用推送 PAT。 - default: '' # 執行方式區塊:宣告本 action 為 node action 及其進入點。 runs: # 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite)。 diff --git a/src/index.js b/src/index.js index a1ae574..5e3d037 100644 --- a/src/index.js +++ b/src/index.js @@ -90,7 +90,8 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { * 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json; * 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 4 判定無可審查變更且非建問題模式時, * 也會以 result: 'success' 提交空 findings。 - * 推送以 `ctx.pushToken`(PAT)優先,使結果 commit 再觸發 CI、由步驟 1 快速回報; + * 推送一律以 `ctx.token` 的身分進行(不走 runner 的 origin 自動 token);只要 token 是能觸發 CI 的 + * PAT,結果 commit 就會再觸發 CI、由步驟 1 快速回報; * 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。 */ function commitFindings({ cwd, ctx, files, result }) { @@ -101,7 +102,6 @@ function commitFindings({ cwd, ctx, files, result }) { message: `${BOT_COMMIT_PREFIX}[${result}]`, files, token: ctx.token, - pushToken: ctx.pushToken, serverUrl: ctx.serverUrl, repository: ctx.repository, }); diff --git a/src/lib/context.js b/src/lib/context.js index a3c06ed..f4a8d62 100644 --- a/src/lib/context.js +++ b/src/lib/context.js @@ -20,7 +20,6 @@ const path = require('path'); * token: string, * model: string, * createIssue: boolean, - * pushToken: string, * event: Object, * pr: (Object|null), * prNumber: (number|null), @@ -38,12 +37,11 @@ const path = require('path'); * - repository:`owner/repo` 全名(GITHUB_REPOSITORY)。 * - owner / repo:自 repository 拆出的擁有者與專案名,缺值時為空字串。 * - apiBase:Gitea REST API 基底網址(`/api/v1`)。 - * - token:action input `token`(INPUT_TOKEN),用於 API 認證,缺值時為空字串。 + * - token:action input `token`(INPUT_TOKEN),用於 Gitea API 認證,以及 push findings/exclusions + * commit 回 repo;建議為「能觸發 CI 的 PAT」(自動 token 推送不會再觸發 CI)。缺值時為空字串。 * - model:action input `model`(INPUT_MODEL,已 trim),指定 AI 模型,缺值時為空字串。 * - createIssue:action input `create-issue`(INPUT_CREATE-ISSUE),是否將問題建到 * 存取庫的問題追蹤(建問題模式);trim + 小寫後與字串 'true' 嚴格比對,預設 false。 - * - pushToken:action input `push-token`(INPUT_PUSH-TOKEN),推送 findings/exclusions commit 用的 PAT; - * 提供時以 PAT 身分推送使 CI 再觸發(配合步驟 1 快速回報);留空=退回以 token 走 origin 推送(不會再觸發)。 * - event:事件 payload 解析後的完整物件;讀取失敗時為空物件。 * - pr:payload 內的 pull_request 物件;非 PR 事件時為 null。 * - prNumber:PR 編號,優先取 payload,退而從 GITHUB_REF(refs/pull/N/...)解析;皆無時為 null。 @@ -95,8 +93,6 @@ function loadContext() { model: (process.env.INPUT_MODEL || '').trim(), // 是否將問題建到存取庫的問題追蹤(input: create-issue,字串 'true' 才啟用,預設否)。 createIssue: (process.env['INPUT_CREATE-ISSUE'] || '').trim().toLowerCase() === 'true', - // 推送 findings/exclusions commit 用的 PAT(input: push-token);提供時以 PAT 推送讓 CI 再觸發,留空=退回 token。 - pushToken: (process.env['INPUT_PUSH-TOKEN'] || '').trim(), event, pr, prNumber, diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index 3b80bb4..b839345 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -223,9 +223,9 @@ function fileLastUpdatedIso(cwd, file) { * 若目前 HEAD 不在 PR head commit(例如 checkout 停在 merge commit), * 會先 `git checkout --detach ` 站上 head,避免把 merge 內容推回來源分支。 * commit 以 `-c` 臨時覆寫 user.name / user.email,不改動 repo 的 git 設定。 - * push 策略:提供 `pushToken`(PAT)時直接以該 token 的 URL 推送(略過 origin)——因為 origin - * 帶的是不會再觸發 CI 的自動 token,改以 PAT 身分推送才會讓 PR 的 synchronize 事件再觸發 CI; - * 未提供 `pushToken` 時先走 origin,失敗(遠端未帶認證)再改用帶 `token` 的 URL 重試。 + * push 策略:一律以 `token` 的身分明確認證推送({@link pushWithCredential},不走 runner 的 + * origin 自動 token)——origin 帶的自動 token(gitea.token / GITHUB_TOKEN)推送不會再觸發 CI, + * 改以呼叫端提供的 `token`(建議為 PAT)身分推送,才會讓 PR 的 synchronize 事件再觸發 CI。 * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {object} options - 提交與推送設定。 @@ -233,8 +233,7 @@ function fileLastUpdatedIso(cwd, file) { * @param {string} [options.headSha] - PR head 的 commit SHA;有提供且與目前 HEAD 不同時會先 detach 到此 commit。可省略(falsy 時不 detach,直接於目前 HEAD 上 commit)。 * @param {string} options.message - commit 訊息。 * @param {string[]} options.files - 要加入 commit 的檔案路徑清單(相對 repo 根目錄);全數無實際變更時不 commit、回傳 false。 - * @param {string} options.token - 具該 repo push 權限的 Gitea access token;未提供 pushToken 時,於 origin push 失敗才用於組出帶認證的重試 URL。 - * @param {string} [options.pushToken] - 專用推送 token(PAT);提供時直接以此 token 的 URL 推送(略過 origin),使 push 以 PAT 身分進行以再觸發 CI;未提供時走 origin、失敗再退回 `token`。 + * @param {string} options.token - 具該 repo push 權限的 Gitea access token(建議為能觸發 CI 的 PAT);用於 findings commit 的認證推送。 * @param {string} options.serverUrl - Gitea 伺服器根網址(例如 https://gitea.example.com),須為合法 URL。 * @param {string} options.repository - repo 完整名稱(owner/repo 格式),與 serverUrl 組成 clone URL。 * @returns {boolean} true=有變更且已 commit 並 push 到來源分支;false=暫存區與 HEAD 無差異,略過 commit/push。 @@ -249,7 +248,7 @@ function fileLastUpdatedIso(cwd, file) { * 推送目標 URL 本身不含帳密;且 push 失敗時改拋固定訊息,避免 `execFileSync` * 例外把命令列(含 token)回顯到 CI log 或程序清單。 */ -function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, pushToken, serverUrl, repository }) { +function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, serverUrl, repository }) { const current = gitTrim(cwd, 'rev-parse', 'HEAD'); if (headSha && current !== headSha) { git(cwd, 'checkout', '--detach', headSha); @@ -269,18 +268,9 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, p ); const refspec = `HEAD:refs/heads/${headRef}`; const remoteUrl = `${serverUrl}/${repository}.git`; - if (pushToken) { - // 有專用 PAT → 以 PAT 推送(略過 origin,因 origin 帶的是不會再觸發 CI 的自動 token); - // 以 PAT 身分推送才會讓 PR 的 synchronize 事件再觸發 CI。 - pushWithCredential(cwd, remoteUrl, pushToken, refspec); - } else { - try { - git(cwd, 'push', 'origin', refspec); - } catch { - // 遠端未帶認證(checkout 未保留 credentials)時,改用帶認證的推送重試。 - pushWithCredential(cwd, remoteUrl, token, refspec); - } - } + // 一律以 token 的身分明確認證推送(不走 origin 的自動 token)——只要 token 是能觸發 CI 的 PAT, + // 結果 commit 就會讓 PR 的 synchronize 事件再觸發 CI,由步驟 1 快速回報把結果蓋到新 head。 + pushWithCredential(cwd, remoteUrl, token, refspec); return true; } -- 2.53.0 From 20a2b579985fd79d6d0df11e8cb3effacee5adb7 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 09:27:31 +0000 Subject: [PATCH 39/79] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/exclusions.json | 99 ++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 85ca15b..bddc99a 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -658,5 +658,104 @@ "endLine": 72, "problem": "`redactSecrets` / `agentFailureDetail` 新增了輸出 stderr/stdout 診斷的行為,但沒有測試驗證遮罩規則與限長。這不是單純格式調整;一旦正規式或順序被改壞,失敗 log 可能變得不可診斷,或把控制字元與憑證樣式原樣輸出。", "reason": "Paladin:可排除(重複)。歷史 findings 已針對 redactSecrets/agentFailureDetail 的控制字元、憑證樣式、URL 帳密、截斷與 debug 開關缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:27:30", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 74, + "endLine": 77, + "problem": "攻擊者只要讓 AI CLI 失敗,這裡就會把 stderr/stdout 片段寫進 CI log。那些輸出可能包含 prompt、diff 內容、環境診斷、第三方 CLI 錯誤訊息,甚至 token、API key、JWT、email 或其他 PII;`redactSecrets` 只是樣式比對,漏掉未列舉格式時,秘密會被永久留在多人可讀的建置紀錄裡。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI stderr/stdout 寫入 CI log、黑名單式遮罩可繞過,以及 token/PII 外洩風險提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:27:30", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 206, + "endLine": 217, + "problem": "`ensureIssueCreated` 這個名字唱的是「確保存在」,但實作每次呼叫都會直接建立新 issue;名稱與行為不同拍,日後重用時很容易誤讀。", + "reason": "Paladin:可排除(重複)。歷史 finding 已明確指出 `ensureIssueCreated` 名稱暗示冪等沿用,實作卻會建立新 issue,與本條指控相同。" + }, + { + "addedAt": "2026/07/20 17:27:30", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 176, + "endLine": 363, + "problem": "建問題模式把「留言目的地切換、issue 延後建立、標籤挑選、嚴重/非嚴重發布、PR 回貼連結、相依設定」全部塞進 `main()`,讓主流程同時承擔流程編排與發布策略。六個月後要改任何一種輸出模式時,很容易漏改其中一個 `ctx.createIssue` 分支,或讓一般模式與建問題模式的行為逐步分岔。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 `main()` 內留言路由、issue 生命週期、標籤、相依設定與發布職責耦合的問題。" + }, + { + "addedAt": "2026/07/20 17:27:30", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 31, + "endLine": 53, + "problem": "`agentFailureDetail` 的說明提到「需要原始輸出診斷時,於 workflow 設定 secret `ACTIONS_STEP_DEBUG=true` 再重跑」,但實作其實不看 `ACTIONS_STEP_DEBUG`,預設就會輸出遮罩後的 stderr/stdout 片段。註解與行為不一致會讓未來維護者誤判這段診斷的暴露條件。", + "reason": "Paladin:可排除(與 F001 重複)。本條同樣指向 `agentFailureDetail` 預設輸出 stderr/stdout 片段、與 debug 暴露條件不一致的同一行為;F001 已保留其核心風險。" + }, + { + "addedAt": "2026/07/20 17:27:30", + "prNumber": 6, + "reviewer": "Mage", + "severity": "警告", + "file": "src/index.js", + "startLine": 219, + "endLine": 228, + "problem": "`ensureIssueCreated` 先建立 issue,再逐則寫入暫存留言;任一留言 API 在中途失敗時,流程會直接丟出例外結束,但已建立的 issue 不會回滾,也沒有可重用的識別。最小情境:issue 建立成功,寫入第 2 則 `issueBuffer` 留言時 Gitea 暫時回 500;本次 action 失敗,下一次重跑會再建立一個新 issue,留下重複且內容不完整的追蹤 issue。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。建立 issue 後沖刷暫存留言失敗、重跑又建立重複 issue,正是既有排除事項與多筆歷史 finding 已涵蓋的問題。" + }, + { + "addedAt": "2026/07/20 17:27:30", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 312, + "endLine": 383, + "problem": "建問題模式的核心流程被大幅改寫,但這段新增行為還沒看到對應測試驗證:`kept.length > 0` 才建 issue、標籤挑選失敗要降級、嚴重與非嚴重問題要改發到 issue、最後還要回貼 PR 並嘗試建立 issue dependency。這些都是跨 API 的分支與失敗路徑,沒有測試時很容易只跑到快樂路徑,漏掉「沒有保留問題」「標籤 API 失敗」「dependency API 不支援」這類實際會發生的情境。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的 issue 建立條件、標籤降級、問題分流、PR 回貼與 dependency 失敗路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 17:27:30", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 102, + "endLine": 151, + "problem": "`resolveMergeBase` 新增了淺層 checkout 修復策略與多段 fallback,但邊界與失敗路徑還沒被驗證:初次 fetch 成功但 merge-base 失敗、`--unshallow` 成功後立即成功、`deepen base` 成功、`deepen HEAD` 成功,以及所有策略都失敗時錯誤訊息要包含診斷。這段是 review diff 的基準點,沒測到 fallback 行為就等於沒有確認淺層 checkout 的修復真的可用。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 `resolveMergeBase` 多階段 fetch、淺層/非淺層分支、停止條件與最終診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:27:30", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 266, + "endLine": 314, + "problem": "push 行為從「先 origin、失敗才帶 token URL 重試」改成一律透過 `pushWithCredential` 用環境變數注入 Basic header,但新增的認證與錯誤遮蔽行為沒有測試驗證。這裡的失敗路徑如果沒測,很容易在 push 失敗時回歸成洩漏 argv/token,或 refspec/remoteUrl 組錯卻直到 CI 才發現。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 `pushToken`、origin 與 credential push 策略、認證遮蔽及 push 失敗路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 17:27:30", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 14, + "endLine": 73, + "problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 log,並依賴 `redactSecrets` 做遮罩與單行化;但這段新增的失敗診斷路徑沒有看到測試覆蓋。這不只是快樂路徑問題,邊界包含空輸出、只有 stdout、逾時 killed、換行控制字元、Authorization/token/URL credentials/長 token 等格式,任何一個漏掉都會讓診斷品質或遮罩行為失真。", + "reason": "Paladin:可排除(重複)。歷史 finding 已記錄 `agentFailureDetail`/`redactSecrets` 對空輸出、控制字元、憑證格式、URL 帳密、截斷與 debug 開關缺少測試。" } ] -- 2.53.0 From b277ff2f7c4b565601d57ce93d346e4735b27ebb Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 17:28:43 +0800 Subject: [PATCH 40/79] =?UTF-8?q?fix(=E5=BB=BA=E5=95=8F=E9=A1=8C=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F):=20=E5=83=85=E5=9C=A8=E6=9C=89=E5=9A=B4=E9=87=8D?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E6=99=82=E8=AE=93=20PR=20=E7=9B=B8=E4=BE=9D?= =?UTF-8?q?=E6=96=BC=20issue=EF=BC=8C=E8=AD=A6=E5=91=8A=EF=BC=8F=E5=BB=BA?= =?UTF-8?q?=E8=AD=B0=E4=B8=8D=E9=98=BB=E6=93=8B=E5=90=88=E4=BD=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/index.js | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 5e3d037..9fe7562 100644 --- a/src/index.js +++ b/src/index.js @@ -137,7 +137,8 @@ function commitFindings({ cwd, ctx, files, result }) { * 並寫入暫存留言,嚴重問題與警告+建議再逐條發到該 issue,讓每條問題都能被個別回覆); * 無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過); * 收束時在 PR 回貼 issue 連結形成雙向關聯, - * 並讓 PR 相依於該 issue(addIssueDependency,issue 完成/關閉前 PR 無法合併;需 repo 啟用問題相依功能); + * 並「僅在有嚴重問題時」讓 PR 相依於該 issue(addIssueDependency,issue 關閉前 PR 無法合併; + * 需 repo 啟用問題相依功能)——僅有警告/建議時 issue 仍建立供追蹤,但不阻擋合併; * 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、 * 建問題模式只 commit exclusions.json、無檔案可 commit 時略過; * commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。 @@ -366,7 +367,7 @@ async function main() { } } - // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯),並讓 PR 相依於該 issue ─ + // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─ // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → ensureIssueCreated),此處不再補掛。 if (ctx.createIssue && issue) { await gitea.createIssueComment( @@ -378,12 +379,17 @@ async function main() { otherCount: others.length, }), ); - // 讓 PR 相依於此追蹤 issue:issue 完成/關閉前 PR 無法合併(需 repo 啟用「問題相依」功能)。 - try { - await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number); - log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}:issue 關閉前無法合併。`); - } catch (err) { - log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`); + // 只有「嚴重」問題才讓 PR 相依於追蹤 issue(issue 關閉前無法合併,需 repo 啟用「問題相依」功能); + // 僅有警告/建議時,issue 仍建立供追蹤,但不掛相依、不阻擋 PR 合併。 + if (severe.length > 0) { + try { + await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number); + log('建問題', 'INF', `有嚴重問題:已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number},issue 關閉前無法合併。`); + } catch (err) { + log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`); + } + } else { + log('建問題', 'INF', `無嚴重問題(僅警告/建議):issue #${issue.number} 僅供追蹤,不阻擋 PR 合併。`); } log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`); } -- 2.53.0 From be72aa094426fa8bbec06887e1f6b6966e5e4b40 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 09:32:01 +0000 Subject: [PATCH 41/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 77 ++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index bddc99a..44fa19c 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -757,5 +757,82 @@ "endLine": 73, "problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 log,並依賴 `redactSecrets` 做遮罩與單行化;但這段新增的失敗診斷路徑沒有看到測試覆蓋。這不只是快樂路徑問題,邊界包含空輸出、只有 stdout、逾時 killed、換行控制字元、Authorization/token/URL credentials/長 token 等格式,任何一個漏掉都會讓診斷品質或遮罩行為失真。", "reason": "Paladin:可排除(重複)。歷史 finding 已記錄 `agentFailureDetail`/`redactSecrets` 對空輸出、控制字元、憑證格式、URL 帳密、截斷與 debug 開關缺少測試。" + }, + { + "addedAt": "2026/07/20 17:32:00", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 52, + "endLine": 69, + "problem": "AI CLI 失敗時會把 stdout/stderr 片段預設寫進 CI log。攻擊者只要讓 CLI 失敗,並讓錯誤輸出夾帶 prompt、diff、環境診斷、原始碼片段或不符合目前 regex 的 token/PII,就能把本應留在審查沙箱內的內容長期落到多人可讀的 workflow log。`redactSecrets` 是黑名單式遮罩,擋不住未知格式憑證、中文個資、內部 URL、客戶資料或模型/CLI 回顯的任意文字。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI 失敗輸出 stdout/stderr、黑名單式遮罩可繞過,以及機密或個資落入 CI log 的風險提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:32:00", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 178, + "endLine": 235, + "problem": "`main()` 裡新加入的兩個閉包與大段 JSDoc 讓主流程開場變得過於厚重。這些說明本身有價值,但它們插在流程步驟之前,讓讀者要先穿過一整段留言派送與建 issue 細節,才聽見真正的 10 步驟主旋律。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 main() 承擔留言路由、issue 狀態、緩衝佇列與發布職責耦合,並提出抽離 helper/發布器的方向。" + }, + { + "addedAt": "2026/07/20 17:32:00", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 177, + "endLine": 378, + "problem": "`main()` 這次把「一般模式」與「建問題模式」的留言路由、issue 建立、暫存 buffer、標籤挑選、相依關係、舊留言清理全部塞進同一個流程函式。六個月後要改任一個發佈規則時,維護者必須同時理解 `ctx.createIssue`、`issue` 閉包狀態、`issueBuffer`、`currentRunCommentIds` 與步驟順序,控制流已經變成隱含狀態機,測試也很難只針對「留言目的地」或「issue 收束」單獨驗證。", + "reason": "Paladin:可排除(重複)。此條與歷史 Leo finding 同樣指向 main() 內一般模式/建問題模式發布規則、issueBuffer、issue 閉包狀態與 ctx.createIssue 分支耦合。" + }, + { + "addedAt": "2026/07/20 17:32:00", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 177, + "endLine": 390, + "problem": "建問題模式這次新增了大量可觀察行為,但 diff 沒看到對應測試:留言先暫存再寫入 issue、無保留問題時靜默通過、有嚴重問題才加 PR 相依、只有警告/建議時不阻擋合併,以及一般模式才延後 resolve 舊留言。這些都是很容易在分支條件中漏掉的流程行為,現在還沒有被試煉過。", + "reason": "Paladin:可排除(重複)。歷史 Maya findings 已涵蓋建問題模式的暫存留言、靜默通過、嚴重/非嚴重分流、PR 相依與一般模式舊留言清理等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 17:32:00", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 102, + "endLine": 158, + "problem": "`resolveMergeBase` 新增了多段 fetch 補救策略與錯誤診斷,但沒有看到測試驗證邊界與失敗路徑:首次 merge-base 成功、淺層 repo 走 `--unshallow`、unshallow 失敗後 deepen base/head、所有策略都失敗時要拋出含診斷且保留 `cause` 的錯誤。這段若沒測,很容易在淺層 checkout 才暴露審查整段中斷。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、停止條件、降級及最終錯誤診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:32:00", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 266, + "endLine": 306, + "problem": "`commitAndPushFindings` 改成一律透過 `pushWithCredential` 使用 token 推送,且 `pushWithCredential` 要保證 token 不進 argv、失敗錯誤被固定訊息取代;但 diff 沒有對這些失敗與遮蔽保證新增測試。這條路徑失敗時會直接影響 findings commit,也可能讓敏感資訊出現在測不到的例外訊息中。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 pushToken/pushWithCredential 推送策略、認證遮蔽與失敗路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 17:32:00", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 13, + "endLine": 72, + "problem": "`agentFailureDetail` 現在會把 stderr/stdout 片段寫進 CI log,雖然有 `redactSecrets`,但沒有看到測試驗證遮罩規則與截斷邊界。這不是快樂路徑;一旦 AI CLI 失敗,未驗證的遮罩就會變成長期保存的 log 風險。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 redactSecrets/agentFailureDetail 對 Authorization、token、URL 帳密、控制字元、截斷與空輸出等邊界缺少測試。" } ] -- 2.53.0 From 3ef8a302911c60b9f71024e1f4b60e0d4578b8fd Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 17:33:20 +0800 Subject: [PATCH 42/79] =?UTF-8?q?fix(=E7=B5=90=E6=9E=9C=E5=9B=9E=E5=A0=B1)?= =?UTF-8?q?:=20=E5=AF=A9=E6=9F=A5=E6=9C=AC=E8=BC=AA=E4=B8=8D=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=20exit=201=EF=BC=8C=E5=A4=B1=E6=95=97=E5=8F=AA?= =?UTF-8?q?=E7=94=B1=E6=AD=A5=E9=A9=9F=201=20=E8=AE=80=E7=B5=90=E6=9E=9C?= =?UTF-8?q?=20commit=20=E8=A8=8A=E6=81=AF=E5=9B=9E=E5=A0=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/index.js | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/index.js b/src/index.js index 9fe7562..15f2be6 100644 --- a/src/index.js +++ b/src/index.js @@ -143,13 +143,16 @@ function commitFindings({ cwd, ctx, files, result }) { * 建問題模式只 commit exclusions.json、無檔案可 commit 時略過; * commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。 * - * @returns {Promise} process exit code:0=成功(無嚴重問題或無可審查變更、或偵測到 success 標記); - * 1=失敗(有嚴重問題、缺 PR 編號/token、找不到 AI 工具、或偵測到 failure 標記)。 + * @returns {Promise} process exit code:本輪「審查」一律回傳 0(不因嚴重問題直接讓檢查失敗—— + * 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報); + * 回傳 1 僅發生於:步驟 1 偵測到 `[ai-review-bot][failure]` 結果 commit,或前置條件不足 + * (缺 PR 編號/token、找不到 AI 工具)等無法進行審查的情況。 * @remarks * 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))`; * 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記, - * 讓下一次 workflow 觸發時重新完整審查。警告+建議等級的問題不影響成敗,只有「嚴重」會使結果為 failure; - * 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變成敗判定。 + * 讓下一次 workflow 觸發時重新完整審查。警告+建議等級不影響結果標記,只有「嚴重」會使結果 commit + * 標記為 failure;而「失敗檢查(exit 1)」只由步驟 1 讀到該 failure 結果 commit 時產生,審查本輪不直接 exit 1。 + * 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變上述結果標記判定。 */ async function main() { const ctx = loadContext(); @@ -406,7 +409,13 @@ async function main() { } else { log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。'); } - return result === 'success' ? 0 : 1; + // 本輪「審查」一律以成功收場、不直接讓檢查失敗;有嚴重問題時已推出 [failure] 結果 commit, + // 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果 + // 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。) + if (result === 'failure') { + log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。'); + } + return 0; } main() -- 2.53.0 From ed883f6f20d20d299479e59ba3d74b18b31ed659 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 09:36:22 +0000 Subject: [PATCH 43/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 44fa19c..180294e 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -834,5 +834,115 @@ "endLine": 72, "problem": "`agentFailureDetail` 現在會把 stderr/stdout 片段寫進 CI log,雖然有 `redactSecrets`,但沒有看到測試驗證遮罩規則與截斷邊界。這不是快樂路徑;一旦 AI CLI 失敗,未驗證的遮罩就會變成長期保存的 log 風險。", "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 redactSecrets/agentFailureDetail 對 Authorization、token、URL 帳密、控制字元、截斷與空輸出等邊界缺少測試。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 72, + "endLine": 75, + "problem": "這裡把 AI CLI 的 stderr/stdout 片段預設寫進 CI log。攻擊者可以讓 CLI 失敗並把 prompt、diff、環境診斷、token、JWT、內部 URL 或 PR 內容中的敏感資料噴到 stdout/stderr;`redactSecrets` 只是正規表示式盡力遮罩,漏掉格式外的憑證或 PII 時,機密就被長期保存到多人可讀的 CI log。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 debug/失敗診斷輸出 AI CLI stderr/stdout、黑名單式遮罩可繞過,以及機密或個資外洩提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 180, + "endLine": 235, + "problem": "`main()` 裡新增了兩個閉包 helper,再各自搭配整段 JSDoc,像把副歌、橋段與註腳全塞進同一小節。這些註解本身不差,但放在主流程中間會稀釋流程主線,讓讀者在真正開始步驟 3 前先穿過一大段實作細節。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 `main()` 內留言路由、issue 建立、緩衝佇列與閉包 helper 造成主流程職責混雜,並提出抽出發布協作者的相同方向。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 234, + "problem": "`main()` 這次被塞進建問題模式的留言路由、issue 暫存佇列、issue 建立流程與標籤套用邏輯。未來只要要調整「留言要發到 PR 還是 issue」、「何時 flush 暫存留言」、「哪些模式要靜默通過」,維護者都必須在主流程裡追閉包狀態(`issueBuffer`、`issue`、`currentRunCommentIds`),主流程會越來越像狀態機但沒有清楚邊界。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 `main()` 中 issue 狀態、`issueBuffer`、留言路由與發布狀態機職責耦合的同一問題。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 309, + "endLine": 390, + "problem": "建問題模式的收束流程散在多段 `if (ctx.createIssue)` 分支中:建立 issue、選標籤、嚴重問題留言、其他問題留言、PR 回貼連結、設定 dependency 都在 `main()` 裡交錯。這讓「建問題模式」沒有單一可讀入口,未來維護者要確認模式行為時必須跨多個區塊拼湊流程,尤其容易漏掉 `issue` 只在 `kept.length > 0` 後才存在的隱含前提。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出建問題模式的 issue 建立、留言分流、PR 回貼與 dependency 等流程散落在 `main()`,同屬發布流程邊界不清。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 30, + "endLine": 44, + "problem": "`agentFailureDetail` 的文件先說「原始輸出預設隱藏」,後面又說預設附上遮罩後的 stderr/stdout 片段。這種註解與實作語意互相打架,未來維護者很容易誤判 CI log 會不會包含 CLI 輸出,進而在調整遮罩或除錯策略時做錯取捨。", + "reason": "Paladin:可排除(重複)。與 F003 指涉同一段 `agentFailureDetail` 文件,問題同為「預設隱藏原始輸出」與「預設輸出遮罩片段」語意互相衝突。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 397, + "endLine": 415, + "problem": "在 `create-issue=true` 且本輪有保留的嚴重問題、但 `excluded` 為空時,`exclusionsChanged` 會是 `false`,建問題模式又不 commit findings,因此收尾會「略過 commit/push」後直接 `return 0`。最小重現:PR 只有 1 條嚴重 finding、沒有任何誤判排除、問題相依 API 未啟用或設定失敗;流程會建立 issue、相依設定被 catch 成 WRN,沒有 `[failure]` 結果 commit,也沒有下一輪步驟 1 可回報失敗,最後 CI 成功通過。", + "reason": "Paladin:可排除(重複)。與 F001 指涉同一個嚴重 finding gate 依賴後續 commit/push 或 dependency side effect、而本輪可能直接回傳 0 的問題。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 409, + "problem": "建問題模式的流程被大幅改寫,但 diff 沒看到對應測試驗證這些新分支:留言先暫存到 `issueBuffer`、`kept.length > 0` 才建立 issue、無保留問題/無可審查變更時靜默通過、PR 回貼 issue 連結,以及只有嚴重問題才呼叫 `addIssueDependency`。這些都是使用者可觀察行為,沒有測試就很容易在之後調整流程時被改壞。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、有 finding 才建 issue、靜默通過、PR 回貼、嚴重問題與 dependency 等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 102, + "endLine": 159, + "problem": "`resolveMergeBase` 新增了多段 fetch/unshallow/deepen fallback 與錯誤診斷,但沒有看到針對淺層 checkout、fetch 失敗、merge-base 首次失敗後成功、所有策略失敗等邊界的測試。這段決定送審 diff 的基準,一旦 fallback 順序或錯誤處理壞掉,審查可能漏看或多看變更。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 `resolveMergeBase` 多階段 fetch、淺層 checkout、成功停止條件與全部失敗診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 266, + "endLine": 297, + "problem": "push 行為改成一律走 `pushWithCredential`,並宣稱 token 不進 argv、失敗時隱藏 URL/認證資訊,但沒有測試覆蓋成功與失敗路徑。這裡一旦 regression,可能導致結果 commit 推不上去,或失敗訊息洩漏認證材料。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushToken/pushWithCredential 的推送策略、認證環境變數、失敗遮蔽與成功失敗路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 17:36:21", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 13, + "endLine": 84, + "problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 CI log,並依賴 `redactSecrets` 遮罩機密;但新增的遮罩規則與截斷規則沒有測試。這是典型失敗路徑,平常快樂路徑不會跑到,沒測過就無法相信它真的能處理 Authorization、token、URL 帳密、控制字元與長金鑰。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 `redactSecrets`/`agentFailureDetail` 對 Authorization、token、URL 帳密、控制字元、長輸出與空輸出等邊界缺少測試。" } ] -- 2.53.0 From c63b1a6da2edc6326e766bd9f984d21b3cdd17b6 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 17:40:50 +0800 Subject: [PATCH 44/79] =?UTF-8?q?perf(resolveMergeBase):=20=E5=85=88=20dee?= =?UTF-8?q?pen=20base/HEAD=EF=BC=8C--unshallow=20=E9=99=8D=E7=82=BA?= =?UTF-8?q?=E6=9C=80=E5=BE=8C=E6=89=8B=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/gitrepo.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index b839345..499dec9 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -130,17 +130,16 @@ function resolveMergeBase(cwd, baseRef) { diagnostics.push('merge-base(首次):失敗'); } - // 資料驅動的補抓策略:淺層才 unshallow;其後依序 deepen base 與 HEAD。 - // 每個策略成功後立即重試 merge-base,成功即回傳,避免多餘往返。 - const strategies = []; + // 資料驅動的補抓策略:先以固定深度分批加深 base 與 HEAD(每步後重試 merge-base,成功即回傳); + // 只有仍失敗且為淺層 repo 時,才把成本最高的 --unshallow(下載完整歷史)當最後手段, + // 避免大型/長壽 repo 只為找共同祖先就無謂拉全史。 + const strategies = [ + ['deepen base', 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`], + ['deepen HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD'], + ]; if (gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true') { strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']); } - strategies.push([ - 'deepen base', - 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`, - ]); - strategies.push(['deepen HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD']); for (const [label, ...args] of strategies) { if (!runFetch(label, ...args)) continue; // fetch 失敗就換下一個策略。 -- 2.53.0 From deb20bb07852967328c70baace1409e259c32fdd Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 17:40:50 +0800 Subject: [PATCH 45/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E5=AF=AB=E5=9B=9E=E8=AD=B0=E9=A1=8C=20#9=20=E5=BE=85=E4=BA=BA?= =?UTF-8?q?=E5=B7=A5=E8=99=95=E7=90=86=E5=95=8F=E9=A1=8C=E8=87=B3=20findin?= =?UTF-8?q?gs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../findings/2026-07-20-17:37:54.json | 117 ++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-20-17:37:54.json diff --git a/.gitea/ai-review/findings/2026-07-20-17:37:54.json b/.gitea/ai-review/findings/2026-07-20-17:37:54.json new file mode 100644 index 0000000..c760ca0 --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-20-17:37:54.json @@ -0,0 +1,117 @@ +{ + "generatedAt": "2026/07/20 17:37:54", + "commitSha": "3ef8a302911c60b9f71024e1f4b60e0d4578b8fd", + "prNumber": 6, + "tool": { + "name": "code-review-resolve", + "version": "0.0.8", + "model": "(工具預設)" + }, + "findings": [ + { + "id": "F001", + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 121, + "endLine": 145, + "problem": "流程步驟編號硬編碼在主流程註解、日誌字串、README 與多個函式 JSDoc 中;插入一個步驟就要同步修改大量檔案,文件與實作高耦合,日後調整流程易漏改而互相矛盾。", + "suggestion": "共用函式 JSDoc 改以語意階段名稱描述、不引用易變動的數字;日誌集中定義階段名稱或由單一流程描述產生編號;README 流程圖也以語意名稱為主。屬跨檔重構+設計取捨。", + "suggestedCode": "const STAGE = Object.freeze({\n TOOL_DETECTION: '偵測工具',\n DIFF_COLLECTION: '整理差異',\n ATTACK_REVIEW: '攻擊方審查',\n DEFENSE_REVIEW: '防守方裁決',\n});\nlog(STAGE.DIFF_COLLECTION, 'INF', message);" + }, + { + "id": "F002", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitea.js", + "startLine": 172, + "endLine": 215, + "problem": "建立 issue 相依關係的 API 封裝(addIssueDependency)沒有對應測試:尚未驗證 URL 的 issue 編號與 {index, owner, repo} payload 正確、以及非 2xx 錯誤是否原樣往上傳遞。(原併提的 addLabelsToIssue 已於先前 commit 移除。)", + "suggestion": "mock 底層 API,驗證 addIssueDependency 的 URL issue 編號與 payload,加入 4xx/5xx 拋錯案例,並搭配主流程測試確認相依失敗會被降級而不阻斷審查。屬測試架構決策(專案無測試框架)。", + "suggestedCode": "" + }, + { + "id": "F003", + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 47, + "endLine": 65, + "problem": "tryGit 將所有 git 失敗壓成布林值,resolveMergeBase 的診斷只知策略成敗、無法區分認證/refspec/網路/版本問題;CI 出錯時維護者只能重跑或自行重現,診斷成本高。", + "suggestion": "讓嘗試結果保留結構化且已清理的錯誤分類(exit code、git 子命令、安全化後短訊息),仍避免記錄遠端 URL 或憑證;最終錯誤彙整足以行動的原因,並可對各失敗類型做單元測試。屬診斷重構+需測試。", + "suggestedCode": "function tryGit(cwd, ...args) {\n try {\n git(cwd, ...args);\n return { ok: true };\n } catch (error) {\n return { ok: false, code: error.status ?? error.code ?? null, reason: sanitizeGitError(error) };\n }\n}" + }, + { + "id": "F004", + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 270, + "endLine": 291, + "problem": "findings 推送的認證路徑沒有測試。註:原「PAT 直接推送 vs origin 失敗重試」雙路徑已於先前 commit 合併為「一律以 token 明確認證推送」,測試仍待補:空 token 邊界、推送目標正確、錯誤與輸出不含 token 原文。", + "suggestion": "補單元測試攔截 git 參數/env:斷言以 token 認證推送、推送目標 refspec 正確、呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。", + "suggestedCode": "" + }, + { + "id": "F005", + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "action.yml", + "startLine": 3, + "endLine": 3, + "problem": "檔頭「更新時間」為手動維護的固定字串,與實際檔案更新時間不一致;散落各檔的手動時間戳容易走調,讀者無法判斷可信度。", + "suggestion": "屬 jsc spec-time-log 慣例(各檔頭「更新時間」由 doc-funcs 流程統一產生/同步)。是否移除改用版控紀錄、或如何統一更新,宜由 doc-funcs 流程處理,不在 resolve 逐條硬改。", + "suggestedCode": "" + }, + { + "id": "F006", + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "readme.md", + "startLine": 3, + "endLine": 3, + "problem": "README 檔頭手動「更新時間」與實際更新時間不符,並與 action.yml、src/index.js 重複保存同類易過期資訊,形成多個不一致的真相來源。", + "suggestion": "同 F005:屬 jsc spec-time-log 慣例,交 doc-funcs 流程統一維護(移除或自動注入)。", + "suggestedCode": "" + }, + { + "id": "F007", + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 7, + "endLine": 7, + "problem": "啟動橫幅硬編碼的「更新時間」與程式實際更新時間不一致,每次改程式都要人工校準,製造噪音並讓執行日誌呈現失真版本資訊。", + "suggestion": "同 F005:屬 jsc spec-time-log 慣例,交 doc-funcs 流程統一維護;若日誌需辨識版本,可改顯示 CI 注入的 commit SHA/版本號(屬慣例調整)。", + "suggestedCode": "" + }, + { + "id": "F008", + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 179, + "endLine": 231, + "problem": "main() 內新增兩個帶完整 JSDoc 的閉包函式與一大段「步驟 2:延後執行」說明,使主流程在進入步驟 3 前被近六十行細節打斷,留言路由、issue 建立與流程說明混在同一層,閱讀節奏沉重。", + "suggestion": "將留言路由與 issue 建立封裝成具語義名稱的輔助物件/模組(例如 createCommentPublisher),讓 main() 只保留流程級呼叫;延後清理理由縮成貼近呼叫點的簡短註解。與 F001(語意階段名稱)同屬主流程重構,宜一併處理。", + "suggestedCode": "const comments = createCommentPublisher({ ctx, gitea });\nawait comments.post(templates.toolComment({ /* ... */ }));" + } + ], + "excluded": [] +} -- 2.53.0 From 4b779ee00b651a13cdcbc841b329c4db0354aa1c Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 09:43:57 +0000 Subject: [PATCH 46/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 180294e..027e061 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -944,5 +944,115 @@ "endLine": 84, "problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 CI log,並依賴 `redactSecrets` 遮罩機密;但新增的遮罩規則與截斷規則沒有測試。這是典型失敗路徑,平常快樂路徑不會跑到,沒測過就無法相信它真的能處理 Authorization、token、URL 帳密、控制字元與長金鑰。", "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 `redactSecrets`/`agentFailureDetail` 對 Authorization、token、URL 帳密、控制字元、長輸出與空輸出等邊界缺少測試。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 58, + "endLine": 72, + "problem": "`agentFailureDetail` 現在預設把 AI CLI 的 `stderr` 與 `stdout` 片段寫進 CI log。攻擊者可以在 PR diff 或提示注入內容中放入敏感資料形狀的字串,再誘導 CLI 失敗並回顯 prompt;`redactSecrets` 只是盡力遮罩,擋不住短密碼、內部 URL、email、客戶資料或非典型 token。這等於把不可信輸入與可能含機密的工具輸出灌進長期保存、多人可讀的 log。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI stderr/stdout 寫入 CI log、黑名單式遮罩可繞過,以及機密或個資外洩風險提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Bard", + "severity": "警告", + "file": "src/index.js", + "startLine": 119, + "endLine": 151, + "problem": "流程說明把「步驟 2」描述成延後到步驟 8 之後才執行,後面程式又用 `步驟 2(延後執行)` 回頭標示。這段樂譜的拍號倒著走,讀者必須在時間順序與編號順序之間來回換算,註解、log 與 README 流程圖都因此變得不直覺。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出流程步驟編號硬編碼於主流程註解、日誌與 README,調整流程時容易失準;本條是同一類步驟編號與執行順序不直覺問題。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 186, + "endLine": 224, + "problem": "`postComment` 與 `ensureIssueCreated` 是 `main()` 內部閉包,卻各自塞入完整 JSDoc,再加上前後多段長註解,使主流程像在旋律中突然插入大段腳註。這會稀釋真正重要的 10 步驟編排,可讀性變得厚重。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 main() 內新增兩個帶完整 JSDoc 的閉包與長段註解,打斷主流程閱讀節奏。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 390, + "problem": "`main()` 這次把「一般模式 PR 留言」、「建問題模式暫存留言」、「建立 issue」、「回貼 PR 連結」、「設定 issue dependency」都塞進同一段流程與閉包狀態(`issueBuffer`、`issue`、`postComment`、`ensureIssueCreated`)。半年後要改留言落點或新增第三種輸出模式時,維護者必須同時理解整條 10 步驟流程與這些隱含狀態轉移,出錯點會集中在同一個長函式裡。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 生命週期、緩衝佇列與發布職責耦合,並提出相同的發布器抽離方向。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 405, + "endLine": 426, + "problem": "在 `create-issue: true` 且有嚴重問題、但 `exclusionsChanged === false` 的情境下,流程不會產生任何結果 commit:`filesToCommit` 會是空陣列,接著進入略過 commit/push 的分支,最後仍固定 `return 0`。最小重現:PR 產生 1 條保留的「嚴重」 finding、防守方沒有排除項目,因此 `exclusions.json` 不變;若問題相依 API 未啟用或設定失敗,程式只記 WRN,沒有 failure commit 觸發下一輪步驟 1,也沒有非 0 exit code,CI 會通過。這和註解宣稱「嚴重問題由 `[failure]` 結果 commit 於下一輪回報」的契約衝突。", + "reason": "Paladin:可排除(重複)。此條與 F001 指涉同一建問題模式 fail-open 問題:嚴重 finding 可能因沒有 failure commit 且 issue dependency 失敗被降級而讓 CI 通過。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 102, + "endLine": 155, + "problem": "新增的 `resolveMergeBase` 淺層 checkout 補抓流程沒有看到對應測試驗證。這段現在有多個分支:初次 `merge-base` 成功、`deepen base` 後成功、`deepen HEAD` 後成功、淺層 repo 才跑 `--unshallow`、所有策略失敗時要帶診斷與 `cause`。這些都是會直接影響送審 diff 範圍的核心行為,沒被試煉過我會先當作未完成。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 resolveMergeBase 的多階段 fetch、淺層與非淺層分支、停止條件及最終失敗診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 265, + "endLine": 298, + "problem": "push 行為從「先 origin、失敗再帶 token URL」改成「一律透過 `pushWithCredential` 用 `GIT_CONFIG_*` extraheader 推送」,但 diff 沒有新增測試驗證成功路徑、失敗路徑與機密不進 argv。這段一旦組錯環境變數或 refspec,結果 commit 就推不上去;一旦錯誤訊息回顯 argv,也會破壞你想保護 token 的保證。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 findings 推送認證路徑、token push 目標、錯誤不含憑證與相關測試缺口。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 184, + "endLine": 397, + "problem": "建問題模式新增了大量流程分支,但沒有看到對應測試驗證留言去向與邊界。現在行為包含:issue 建立前先暫存工具/diff/角色留言、沒有保留問題時靜默通過、有保留問題才建 issue、嚴重問題才設定 PR 相依、只有警告/建議時不阻擋合併、最後回貼 issue 連結到 PR。這些不是單純註解調整,是使用者可觀察到的流程改變,若沒測試很容易在重構時漏發或發錯位置。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、無 finding 靜默通過、有 finding 才建 issue、嚴重與其他問題分流、PR 回貼及相依 API 分支缺少測試。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 16, + "endLine": 72, + "problem": "`redactSecrets` / `agentFailureDetail` 新增了把 AI CLI 的 stderr/stdout 寫進 CI log 的行為,但沒有看到測試驗證遮罩規則與截斷邊界。這裡的失敗路徑特別需要測:Authorization、token/password、URL 內嵌帳密、長 token、控制字元與超長輸出都應被處理,否則一個未覆蓋的格式就可能把敏感內容留下來。", + "reason": "Paladin:可排除(重複)。歷史 finding 已記錄 redactSecrets/agentFailureDetail 對 Authorization、token、URL 帳密、控制字元、截斷與 debug 開關等邊界缺少測試。" + }, + { + "addedAt": "2026/07/20 17:43:56", + "prNumber": 6, + "reviewer": "Maya", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 172, + "endLine": 194, + "problem": "新增的 `addIssueDependency` API wrapper 沒有看到測試確認 endpoint 與 body。這段雖然薄,但語意很容易寫反:URL 上的是被阻擋的 PR/issue,body 的 `index` 才是 dependency。少了測試,未來調整時很可能把相依方向顛倒而不自知。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload、錯誤傳遞與相依方向測試,與本條相同。" } ] -- 2.53.0 From b5264141c4f7eda12c0479604ac85cb4da6c3195 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 17:51:21 +0800 Subject: [PATCH 47/79] =?UTF-8?q?fix(=E6=8E=A8=E9=80=81=E8=A7=B8=E7=99=BC?= =?UTF-8?q?=20CI):=20push=20=E5=89=8D=E9=87=8D=E7=BD=AE=20checkout=20?= =?UTF-8?q?=E7=9A=84=E8=87=AA=E5=8B=95=20token=20extraheader=EF=BC=8C?= =?UTF-8?q?=E6=94=B9=E4=BB=A5=20PAT=20=E8=BA=AB=E5=88=86=E6=8E=A8=E9=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit checkout 於 http./.extraheader 持久化自動 Actions token;沿用它推送會被 Gitea 視為自動 token 觸發而不再觸發 workflow。改為推送前於同 scope 先空值重置、再注入 PAT 的 Authorization,使 findings 結果 commit 以 PAT 身分推送、觸發 PR synchronize。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/gitrepo.js | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index 499dec9..2e3e6f9 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -269,7 +269,7 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s const remoteUrl = `${serverUrl}/${repository}.git`; // 一律以 token 的身分明確認證推送(不走 origin 的自動 token)——只要 token 是能觸發 CI 的 PAT, // 結果 commit 就會讓 PR 的 synchronize 事件再觸發 CI,由步驟 1 快速回報把結果蓋到新 head。 - pushWithCredential(cwd, remoteUrl, token, refspec); + pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl); return true; } @@ -278,20 +278,30 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s * * 認證方式:等同 `https://ai-review-bot:@host/...` 的 HTTP Basic(git 會把 * URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的 - * `GIT_CONFIG_*` 環境變數注入 `http..extraheader`,使 base64 憑證**不出現在 argv** + * `GIT_CONFIG_*` 環境變數注入 `http./.extraheader`,使 base64 憑證**不出現在 argv** * (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。 + * + * 觸發 CI 關鍵:`actions/checkout` 會把「自動 Actions token」持久化在同一個 + * `http./.extraheader` scope;若沿用它推送,Gitea 會視為「自動 token 觸發」而 + * **不再觸發 workflow**(防遞迴)。故本函式對這次 push 於該 scope**先以空值重置**(清掉自動 + * token——git 對 extraHeader 給空值即清空既有清單),**再注入 PAT 的 Authorization**,讓推送以 + * PAT 身分進行、觸發 PR 的 synchronize;作用範圍僅限本次 push 的環境變數,不影響 action 其他 + * 仰賴 checkout 持久化憑證的 fetch(如 {@link resolveMergeBase})。 * 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。 * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。 * @param {string} secret - 具 push 權限的 token/PAT(作為 Basic 認證的密碼)。 * @param {string} refspec - push 的 refspec(形如 `HEAD:refs/heads/`)。 + * @param {string} serverUrl - Gitea 伺服器根網址(用於定位 checkout 持久化 extraheader 的 scope)。 * @returns {void} 成功即返回;失敗拋出不含 URL/argv/token 的固定錯誤。 * @throws {Error} 推送失敗時拋出固定訊息(已隱藏遠端 URL 與認證資訊)。 * @remarks 本函式未匯出,僅供 {@link commitAndPushFindings} 使用。 */ -function pushWithCredential(cwd, remoteUrl, secret, refspec) { +function pushWithCredential(cwd, remoteUrl, secret, refspec, serverUrl) { const basic = Buffer.from(`ai-review-bot:${secret}`).toString('base64'); + // checkout 持久化自動 token 的 scope 為 `http./.extraheader`(結尾帶斜線)。 + const headerScope = `http.${serverUrl.replace(/\/+$/, '')}/.extraheader`; try { execFileSync('git', ['push', remoteUrl, refspec], { cwd, @@ -300,9 +310,12 @@ function pushWithCredential(cwd, remoteUrl, secret, refspec) { env: { ...process.env, GIT_TERMINAL_PROMPT: '0', - GIT_CONFIG_COUNT: '1', - GIT_CONFIG_KEY_0: `http.${remoteUrl}.extraheader`, - GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`, + // 兩筆同 scope 設定:先空值清掉 checkout 的自動 token,再注入 PAT 的 Authorization。 + GIT_CONFIG_COUNT: '2', + GIT_CONFIG_KEY_0: headerScope, + GIT_CONFIG_VALUE_0: '', + GIT_CONFIG_KEY_1: headerScope, + GIT_CONFIG_VALUE_1: `Authorization: Basic ${basic}`, }, }); } catch { -- 2.53.0 From d354f30c270e99baa9dcfc6aa76510710c0193ca Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 09:54:00 +0000 Subject: [PATCH 48/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 143 +++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 027e061..fd34057 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1054,5 +1054,148 @@ "endLine": 194, "problem": "新增的 `addIssueDependency` API wrapper 沒有看到測試確認 endpoint 與 body。這段雖然薄,但語意很容易寫反:URL 上的是被阻擋的 PR/issue,body 的 `index` 才是 dependency。少了測試,未來調整時很可能把相依方向顛倒而不自知。", "reason": "Paladin:可排除(重複)。歷史 finding 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload、錯誤傳遞與相依方向測試,與本條相同。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 65, + "endLine": 78, + "problem": "AI CLI 失敗時預設把 stderr/stdout 片段寫進 CI log。攻擊者可以讓 CLI 失敗並誘使它回顯 prompt、diff、環境診斷或原始碼內容;`redactSecrets` 只是盡力遮罩,擋不住 PII、內部路徑、專案機密格式、JWT/雲端金鑰變體,或模型輸出的敏感片段。CI log 通常被長期保存且可被多人讀取,這條路徑會把不該外洩的內容變成可搜尋的紀錄。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI stderr/stdout 寫入 CI log、黑名單式遮罩可繞過,以及敏感內容外洩提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "action.yml", + "startLine": 2, + "endLine": 3, + "problem": "檔頭的「更新時間」仍停在 2026/07/17 18:49:58,但本次變更脈絡顯示檔案已在 2026/07/20 更新;這種手動時間戳與實際修改節奏不同拍,會讓讀者懷疑哪一份資訊才可信。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 action.yml 檔頭手動更新時間與實際更新時間不一致,且建議移除或改由 git history 表達。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "readme.md", + "startLine": 3, + "endLine": 3, + "problem": "README 的更新時間被改成 2026/07/17 18:49:58,卻與本次 2026/07/20 的文件變更不一致;文件首頁第一眼就走調,會削弱後續內容的可信度。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 README 手動更新時間與實際更新時間不符,且與其他檔案重複保存易過期資訊。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 7, + "endLine": 7, + "problem": "啟動橫幅的更新時間仍是 2026/07/17 18:49:58,但本檔本次已有大量流程調整;執行 log 會唱出過期的日期,維運者讀 log 時容易被誤導。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 src/index.js 啟動橫幅硬編碼更新時間與程式實際更新時間不一致。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Bard", + "severity": "警告", + "file": "src/index.js", + "startLine": 183, + "endLine": 237, + "problem": "`main()` 內新增 `postComment`、`ensureIssueCreated` 兩個閉包,還各自塞入完整 JSDoc;主流程本來應像總譜一樣清楚推進,現在在步驟前奏就被大量細節註解打斷,閱讀節奏明顯變重。", + "reason": "Paladin:可排除(重複)。歷史 finding 已涵蓋 main() 內新增閉包、JSDoc 與建問題模式細節打斷主流程可讀性的問題。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 407, + "problem": "`main()` 這次同時吃下「一般模式留言」、「建問題模式暫存/建 issue/貼回 PR/設定相依」、「舊留言延後解決」與「結果 commit」等多條流程。半年後要改其中一種模式時,維護者必須在同一個長函式裡追蹤 `issue`、`issueBuffer`、`currentRunCommentIds`、`kept/severe/others` 的狀態轉換,任何插入步驟都很容易破壞另一個模式。", + "reason": "Paladin:可排除(重複)。已知排除事項與歷史 findings 已涵蓋 main() 承擔一般模式、建問題模式、issue 狀態、緩衝佇列與發布職責耦合的問題。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 188, + "endLine": 216, + "problem": "`postComment` 以同一個函式名稱包了三種行為:PR 直接留言、issue 直接留言、issue 尚未建立時暫存且回傳 `null`。這個回傳型別與副作用都依 `ctx.createIssue`/`issue` 閉包狀態改變,未來新增呼叫點時很容易誤以為一定會真的發出留言或一定會拿到留言物件。", + "reason": "Paladin:可排除(重複)。此條仍屬 main() 中留言路由、issue 狀態與緩衝語意集中在閉包狀態的同一設計問題,已由歷史 findings 涵蓋。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 405, + "endLine": 419, + "problem": "在 `create-issue: true` 且本輪有嚴重問題、但 `exclusions.json` 沒有變更時,`filesToCommit` 會是空陣列,因此不會推出帶 `[failure]` 的結果 commit。接著第 419 行仍固定 `return 0`。最小情境:PR 產生 1 條嚴重 finding、沒有任何 excluded finding、Gitea 未啟用 issue dependency 或 `addIssueDependency` 失敗;流程只建立 issue 並記 WRN,CI check 卻成功結束,也沒有下一輪可讀取 `[failure]` commit,嚴重問題不會阻擋合併。", + "reason": "Paladin:可排除(重複)。與 F001 指涉同一個建問題模式嚴重 finding 在 dependency 失敗且無 failure marker 時 fail-open 的問題。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 321, + "endLine": 395, + "problem": "建問題模式的核心流程被大幅改寫,但 diff 沒看到對應測試驗證幾個分支:沒有保留問題時不建 issue、不在 PR 留言;只有警告/建議時建立 issue 但不加 dependency;有嚴重問題時建立 issue、回貼 PR 連結並嘗試加 dependency。這些都是會影響 PR 合併與通知位置的新行為,沒有測試就很容易在後續調整時悄悄退化。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式核心分支缺少測試,包括無保留問題、嚴重與非嚴重分流、PR 回貼與相依 API 失敗降級。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 104, + "endLine": 148, + "problem": "`resolveMergeBase` 新增多段 fetch/deepen/unshallow 補救流程與診斷錯誤,但沒有看到測試驗證淺層 checkout、fetch 失敗後改走下一策略、某策略成功後立即停止,以及全部失敗時錯誤訊息含診斷。這裡直接決定送審 diff 的基準,邊界沒驗證就可能把錯誤檔案送審或漏審。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層路徑、停止條件及最終診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 307, + "endLine": 333, + "problem": "`pushWithCredential` 新增以 `GIT_CONFIG_*` 注入 Basic header、先清空 checkout 自動 token、失敗時隱藏 URL/憑證的行為,但 diff 沒看到測試覆蓋成功與失敗路徑。這段是結果 commit 能否觸發下一輪 CI 的關鍵,也承擔避免 token 出現在 argv/錯誤訊息的保護,沒有測試很難防回歸。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushWithCredential/pushToken 推送認證路徑、環境變數注入、成功與失敗路徑及憑證遮蔽缺少測試。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 13, + "endLine": 72, + "problem": "`redactSecrets`/`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 CI log,但遮罩規則與截斷策略沒有測試。這是失敗路徑才會觸發的行為,若正規表示式或輸出拼接被改壞,可能不是漏診斷就是把敏感字串帶進 log。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecrets/agentFailureDetail 對 stderr/stdout、遮罩、控制字元、截斷與敏感字串缺少測試的問題。" + }, + { + "addedAt": "2026/07/20 17:54:00", + "prNumber": 6, + "reviewer": "Maya", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 169, + "endLine": 175, + "problem": "新增 `addIssueDependency` API wrapper,但沒有看到測試固定 endpoint 與 payload。這種薄封裝看似簡單,卻很容易在 issue/PR 編號、`index` 欄位或 repo owner/name 組合上出錯。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向測試,與本條相同。" } ] -- 2.53.0 From 83dd56f37f27e254d5c4176ecab397d87f4c5c5b Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 18:04:23 +0800 Subject: [PATCH 49/79] =?UTF-8?q?fix(resolveMergeBase):=20deepen=20HEAD=20?= =?UTF-8?q?=E6=94=B9=E4=BB=A5=E7=9B=AE=E5=89=8D=20HEAD=20=E7=9A=84=20SHA?= =?UTF-8?q?=20=E8=A3=9C=E6=8A=93=20PR=20head=20=E6=AD=B7=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原策略 fetch 遠端符號 HEAD,會被伺服器解析為遠端預設分支, 只加深預設分支歷史、補不到目前 checkout 的 PR head,淺層時仍可能算不出 merge-base。 改先 rev-parse 取得 HEAD 的 commit SHA,以該 SHA 加深 PR head 側歷史。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/gitrepo.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index 2e3e6f9..7eda1e1 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -133,9 +133,12 @@ function resolveMergeBase(cwd, baseRef) { // 資料驅動的補抓策略:先以固定深度分批加深 base 與 HEAD(每步後重試 merge-base,成功即回傳); // 只有仍失敗且為淺層 repo 時,才把成本最高的 --unshallow(下載完整歷史)當最後手段, // 避免大型/長壽 repo 只為找共同祖先就無謂拉全史。 + // 加深 HEAD 側須以「目前 HEAD 的 commit SHA」補抓——遠端符號 `HEAD` 由伺服器解析為 + // 遠端預設分支(非目前 checkout 的 PR head),只加深它並不會補到 PR head 的歷史。 + const headSha = gitTrim(cwd, 'rev-parse', 'HEAD'); const strategies = [ ['deepen base', 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`], - ['deepen HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD'], + ['deepen PR HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', headSha], ]; if (gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true') { strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']); -- 2.53.0 From 0c108ab7f914f2a03e57260212191ad16f34d402 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 18:04:23 +0800 Subject: [PATCH 50/79] =?UTF-8?q?refactor(ai-review):=20=E9=87=8D=E6=96=B0?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E7=95=99=E8=A8=80=E9=96=89=E5=8C=85=E4=B8=A6?= =?UTF-8?q?=E6=8A=BD=E5=87=BA=E8=A8=BA=E6=96=B7=E8=BC=B8=E5=87=BA=E9=95=B7?= =?UTF-8?q?=E5=BA=A6=E5=B8=B8=E6=95=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - postComment → queueOrPostComment:涵蓋建問題模式下可能只暫存不立即發布的語義。 - ensureIssueCreated → createIssueAndFlushBufferedComments:明示建立 issue 並清空暫存留言的完整行為。 - review.js agentFailureDetail 的 500 字上限抽為具名常數 AGENT_DIAGNOSTIC_OUTPUT_LIMIT。 純內部命名與可讀性調整,無外部行為變更。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/index.js | 20 ++++++++++---------- src/lib/review.js | 6 ++++-- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/index.js b/src/index.js index 15f2be6..8131148 100644 --- a/src/index.js +++ b/src/index.js @@ -198,7 +198,7 @@ async function main() { * 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與 * 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。 */ - const postComment = async (body) => { + const queueOrPostComment = async (body) => { if (ctx.createIssue) { if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body); issueBuffer.push(body); @@ -217,7 +217,7 @@ async function main() { * 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。 * @returns {Promise} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。 */ - const ensureIssueCreated = async (labelIds = []) => { + const createIssueAndFlushBufferedComments = async (labelIds = []) => { issue = await gitea.createIssue(ctx, { title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), @@ -245,7 +245,7 @@ async function main() { } log('步驟3', 'INF', `選用工具:${tool.name}(${tool.version})。`); const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`; - await postComment( + await queueOrPostComment( templates.toolComment({ toolName: tool.name, version: tool.version, @@ -270,7 +270,7 @@ async function main() { if (ctx.createIssue) { log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。'); } else { - await postComment(templates.nothingToReviewComment(ignoredCount)); + await queueOrPostComment(templates.nothingToReviewComment(ignoredCount)); // 已成功產生本回合結果留言(無可審查變更),此時才把舊留言標為過時(本回合留言已排除)。 await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); } @@ -286,20 +286,20 @@ async function main() { const diffRows = review.collectDiffRows({ cwd, files, base, gitrepo }); await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); - await postComment(templates.diffComment(diffRows, ignoredCount)); + await queueOrPostComment(templates.diffComment(diffRows, ignoredCount)); // ── 步驟 5:攻擊方角色登場留言 ───────────────────────────────────────── const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles')); const attackers = attackersOf(roles); const defenders = defendersOf(roles); log('步驟5', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); - await postComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers })); + await queueOrPostComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers })); // ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); // ── 步驟 7:防守方角色登場留言 ───────────────────────────────────────── - await postComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); + await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); // ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); @@ -335,7 +335,7 @@ async function main() { } catch (err) { log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`); } - await ensureIssueCreated(labelIds); + await createIssueAndFlushBufferedComments(labelIds); } else { // 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。 log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。'); @@ -365,13 +365,13 @@ async function main() { if (ctx.createIssue) { await review.postOthersToIssue({ ctx, gitea, issueNumber: issue.number, others }); } else { - await postComment(templates.othersComment(others)); + await queueOrPostComment(templates.othersComment(others)); log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); } } // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─ - // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → ensureIssueCreated),此處不再補掛。 + // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。 if (ctx.createIssue && issue) { await gitea.createIssueComment( ctx, diff --git a/src/lib/review.js b/src/lib/review.js index 0125a22..f348117 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -66,13 +66,15 @@ function agentFailureDetail(res) { // 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描; // 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。 const INPUT_LIMIT = 2_000; + // 每段診斷片段(stderr/stdout)寫入日誌的字元上限,兩段共用同一政策,抽為具名常數避免兩處各寫一個魔術數字。 + const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500; // 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時 // 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的 // -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。 const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT)); - if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); + if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT)); - if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); + if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); if (parts.length === 0) { parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); } -- 2.53.0 From cc9c8afed758ec9e0c4c7d903d9580cbeae96ed8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 18:04:23 +0800 Subject: [PATCH 51/79] =?UTF-8?q?docs(ai-review):=20=E5=90=8C=E6=AD=A5=20J?= =?UTF-8?q?SDoc=20=E5=B0=8D=20main()=20=E9=96=89=E5=8C=85=E6=94=B9?= =?UTF-8?q?=E5=90=8D=E7=9A=84=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 配合 postComment/ensureIssueCreated 改名,更新 gitea.js 與 templates.js JSDoc 內對兩個閉包的引用名稱。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/gitea.js | 4 ++-- src/lib/templates.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/gitea.js b/src/lib/gitea.js index ab4ed6b..d99e4ce 100644 --- a/src/lib/gitea.js +++ b/src/lib/gitea.js @@ -119,7 +119,7 @@ function createCommentOnIssue(ctx, issueNumber, body) { * 依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 * @remarks 使用情境:AI review 各步驟把審查摘要、角色登場、問題彙整等內容 - * 以一般留言形式張貼到本次 PR 上(`main()` 的 `postComment` 閉包即以本函式實作)。 + * 以一般留言形式張貼到本次 PR 上(`main()` 的 `queueOrPostComment` 閉包即以本函式實作)。 */ function createIssueComment(ctx, body) { return createCommentOnIssue(ctx, ctx.prNumber, body); @@ -156,7 +156,7 @@ function listLabels(ctx) { * @returns {Promise} 建立成功的 issue 物件(含 `number`、`title`、 * `html_url` 等欄位,依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 的 `ensureIssueCreated` + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 的 `createIssueAndFlushBufferedComments` * 以 PR 標題/描述為 issue 標題與本文,並帶入 `review.selectLabels` 事先挑好的標籤 id * 呼叫本函式一次建立追蹤問題的 issue(連同標籤),之後再把審查內容逐條留言到該 issue。 */ diff --git a/src/lib/templates.js b/src/lib/templates.js index 3476aa8..f02e30b 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -302,7 +302,7 @@ function othersComment(findings) { * @param {string} [params.prBody] - PR 描述原文;nullish 或 trim 後為空時輸出佔位文字。 * @returns {string} 完整 issue 本文 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:建問題模式下 `main()`(src/index.js)的 `ensureIssueCreated` 建立 issue 時, + * 使用情境:建問題模式下 `main()`(src/index.js)的 `createIssueAndFlushBufferedComments` 建立 issue 時, * 以「標題=PR 標題、本文=本函式輸出」呼叫 `gitea.createIssue`, * 讓 issue 讀者能從本文回溯到觸發審查的 PR,再從下方留言逐條查看問題明細。 */ -- 2.53.0 From d8dd5ce92e26b92c0b5064c25147882d20a2b5df Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 18:04:23 +0800 Subject: [PATCH 52/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E8=A8=98=E9=8C=84=20issue=20#10=20=E8=AA=A4=E5=A0=B1=E6=8E=92?= =?UTF-8?q?=E9=99=A4=E4=B8=A6=E5=9B=9E=E5=AF=AB=E5=BE=85=E4=BA=BA=E5=B7=A5?= =?UTF-8?q?=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - exclusions.json 新增一條誤報(gitrepo.js:276 使用者名稱固定 ai-review-bot,依管理員指示)。 - 新增 findings 檔追蹤 4 條待人工處理問題(步驟編號硬編碼、兩處缺測試、debug 輸出取捨)。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitea/ai-review/exclusions.json | 11 ++++ .../findings/2026-07-20-18:00:02.json | 65 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-20-18:00:02.json diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index fd34057..4cc0480 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1197,5 +1197,16 @@ "endLine": 175, "problem": "新增 `addIssueDependency` API wrapper,但沒有看到測試固定 endpoint 與 payload。這種薄封裝看似簡單,卻很容易在 issue/PR 編號、`index` 欄位或 repo owner/name 組合上出錯。", "reason": "Paladin:可排除(重複)。歷史 findings 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向測試,與本條相同。" + }, + { + "addedAt": "2026/07/20 18:00:02", + "prNumber": 6, + "reviewer": "🔮 Mage", + "severity": "嚴重", + "file": "src/lib/gitrepo.js", + "startLine": 276, + "endLine": 276, + "problem": "提供 pushToken 時仍把 Git HTTP 使用者名稱固定為 ai-review-bot;若 PAT 屬於其他帳號,伺服器會以錯誤的帳號/PAT 組合驗證,導致 push 失敗。", + "reason": "誤報(管理員於 issue #10 留言明確指示列為誤報)。Gitea 的 HTTP Basic 認證以密碼欄(token/PAT)判定身分,使用者名稱欄不影響認證結果,故固定為 ai-review-bot 不會造成 push 失敗。" } ] diff --git a/.gitea/ai-review/findings/2026-07-20-18:00:02.json b/.gitea/ai-review/findings/2026-07-20-18:00:02.json new file mode 100644 index 0000000..e1cd27e --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-20-18:00:02.json @@ -0,0 +1,65 @@ +{ + "generatedAt": "2026/07/20 18:00:02", + "commitSha": "d354f30c270e99baa9dcfc6aa76510710c0193ca", + "prNumber": null, + "tool": { + "name": "code-review-resolve", + "version": "0.0.8", + "model": "(工具預設)" + }, + "findings": [ + { + "id": "F001", + "reviewer": "🧰 Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 257, + "endLine": 307, + "problem": "流程步驟編號同時硬編碼在 log 字串、區段註解、JSDoc、README 與多個函式庫中。插入或調整一個步驟就必須跨大量檔案全面改號,容易讓文件與實際紀錄不一致。", + "suggestion": "程式內改用穩定的語意階段名稱(如 diff、attack、defend、publish),由單一流程定義集中決定顯示順序;JSDoc 以階段名稱互相引用,README 流程圖由同一份階段資料產生或僅在文件層維護展示編號。", + "suggestedCode": "const PHASE = Object.freeze({\n RESOLVE_OLD: '清理舊留言',\n DETECT_TOOL: '偵測工具',\n COLLECT_DIFF: '整理差異',\n ATTACK: '攻擊方審查',\n DEFEND: '防守方裁決',\n});\n\nlog(PHASE.COLLECT_DIFF, 'INF', `變更檔案 ${allFiles.length} 個…`);" + }, + { + "id": "F002", + "reviewer": "🧪 Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 252, + "endLine": 289, + "problem": "commitAndPushFindings 的推送行為(有無變更、認證方式、空 commit 防護、是否觸發 CI)缺少測試證明。此為本次變更的核心行為,卻沒有任何測試覆蓋。", + "suggestion": "mock git 命令,斷言:無 staged diff 時回傳 false 且不執行 commit/push;有變更時以認證方式推送到正確 refspec 與分支。注意:原 finding 描述的 pushToken 對比 origin 雙軌邏輯已於重構後移除(現行一律以 token 經 pushWithCredential 認證推送),撰寫測試前需依現行程式碼重新界定情境。", + "suggestedCode": "" + }, + { + "id": "F003", + "reviewer": "🗡️ Assassin", + "focus": "security", + "badge": "🗡️", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 64, + "endLine": 80, + "problem": "agentFailureDetail 於 AI CLI 失敗時會把(經 redactSecrets 盡力遮罩的)stderr/stdout 片段寫入 CI log。redactSecrets 屬盡力遮罩,無法可靠辨識 PII、短密碼或私鑰片段;長期保存且多人可讀的 CI log 有洩漏風險。", + "suggestion": "屬安全(避免洩漏)與可除錯性的設計取捨:現行程式碼已於註解明確權衡並選擇「附上遮罩後輸出以利除錯」。是否改為只記錄退出碼/訊號/逾時狀態+隨機診斷 ID(內容級診斷改寫入有存取控制與短保存期的獨立 artifact)需由維護者裁示,故保留現行行為、標為待人工處理。", + "suggestedCode": "if (verbose) {\n parts.push('已啟用除錯;為避免洩漏原始碼、PII 或憑證,CLI 輸出仍不寫入日誌');\n}" + }, + { + "id": "F004", + "reviewer": "🧪 Maya", + "focus": "testing", + "badge": "🧪", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 171, + "endLine": 215, + "problem": "addLabelsToIssue 與 addIssueDependency 缺少契約測試驗證 endpoint、HTTP method 與 request body;相依關係方向由 URL 與 body 決定,參數次序寫反時粗略 mock 的主流程測試不易察覺。", + "suggestion": "補 Gitea client 單元測試:labels 為空或缺少時不呼叫 API 並回傳 null;有 labels 時送出正確陣列;相依 API 以 PR 編號置於 URL、追蹤 issue 編號置於 index,並帶入正確 owner/repo。", + "suggestedCode": "" + } + ], + "excluded": [] +} -- 2.53.0 From 0c29daa01434d6b6fa1b20ce81c513c75b1b1c6a Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 10:07:27 +0000 Subject: [PATCH 53/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 4cc0480..dbde87a 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1208,5 +1208,115 @@ "endLine": 276, "problem": "提供 pushToken 時仍把 Git HTTP 使用者名稱固定為 ai-review-bot;若 PAT 屬於其他帳號,伺服器會以錯誤的帳號/PAT 組合驗證,導致 push 失敗。", "reason": "誤報(管理員於 issue #10 留言明確指示列為誤報)。Gitea 的 HTTP Basic 認證以密碼欄(token/PAT)判定身分,使用者名稱欄不影響認證結果,故固定為 ai-review-bot 不會造成 push 失敗。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 73, + "endLine": 76, + "problem": "攻擊者可以把惡意內容塞進 PR diff,誘導 AI CLI 在失敗時把環境資訊、原始提示、程式碼片段或秘密印到 stdout/stderr;這裡預設把 stderr/stdout 片段寫進 CI log。`redactSecrets` 只是盡力遮罩,擋不住短 token、雲端 access key、JWT 片段、email、PR 內容中的 PII,等於把不可信子程序輸出變成長期保存且多人可讀的洩漏面。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI 失敗時將 stderr/stdout 寫入 CI log、黑名單式遮罩不足,以及機密或個資外洩風險提出相同問題。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 199, + "endLine": 237, + "problem": "`main()` 內新增兩個帶完整 JSDoc 的閉包,篇幅與抽象程度已不像局部小工具;主流程原本應像總譜一樣清楚推進,現在在步驟開始前先插入大段支線說明,閱讀節奏被拉長。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 `main()` 內新增兩個帶完整 JSDoc 的閉包,讓留言路由與 issue 建立細節打斷主流程閱讀。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "action.yml", + "startLine": 3, + "endLine": 4, + "problem": "檔頭的用途句過長,且「更新時間」仍停在 `2026/07/17 18:49:58`,與本次檔案變更時間脈絡不一致。文件開場若時間與篇幅都失準,後續讀者很難相信這份註解仍被細心維護。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 `action.yml` 檔頭手寫更新時間過期、易與實際變更不同步提出相同問題。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 223, + "problem": "`main()` 現在同時負責審查流程編排、PR 留言、issue 暫存、issue 建立與暫存留言 flush。這些模式差異靠 `ctx.createIssue`、`issueBuffer`、`issue` 這幾個閉包狀態散在後續流程判斷;半年後要新增第三種輸出目的地或調整步驟順序時,很容易漏改某個分支,造成留言發錯位置或暫存內容被丟棄。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 `main()` 內留言路由、issue 狀態、緩衝佇列與發布職責耦合的問題。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 309, + "endLine": 388, + "problem": "建問題模式的生命週期被拆散在多個區塊:先依 `kept.length` 建 issue、再分別處理 severe/others、最後回貼 PR 連結與設定 dependency。這些區塊都隱含「只要有 severe 或 others,`issue` 一定存在」的前置條件,但前置條件沒有被型別或函式邊界保護,只靠讀者追完整個流程才能確認。後續若有人改了 `kept` 分組、靜默通過規則或 issue 建立條件,這段會很容易產生 null issue 或部分內容漏發。", + "reason": "Paladin:可排除(重複)。此條仍指向建問題模式的 issue 生命週期與跨區塊可變狀態耦合,與 F007 及歷史 Leo findings 所述同一設計問題重複。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 183, + "endLine": 389, + "problem": "建問題模式新增了完整分流行為,但這次變更沒有看到對應測試驗證。這裡不只是換留言目的地,而是新增「先暫存留言、確定有保留問題才建 issue、無保留問題靜默通過、嚴重問題才掛 PR 相依、PR 回貼 issue 連結」等多個分支;若其中任一條件判斷錯,可能造成 PR 沒有審查結果、issue 漏留言,或警告問題誤阻擋合併。", + "reason": "Paladin:可排除(重複)。歷史 Maya findings 已涵蓋建問題模式暫存留言、無保留問題靜默通過、問題分流、PR 回貼 issue 連結與 dependency 等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 102, + "endLine": 158, + "problem": "`resolveMergeBase` 新增多段 fetch fallback 與診斷彙整,但沒有測試覆蓋淺層 checkout、fetch 失敗、每次補抓後立即重試 merge-base、以及最終失敗時錯誤訊息與 `cause` 的行為。這段是 PR diff 基準的核心邏輯,未驗證時很容易在 shallow clone 或 PR head 歷史不足時漏審/誤審。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 `resolveMergeBase` 多階段 fetch、淺層與非淺層路徑、fetch 失敗後降級、停止條件與最終診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 268, + "endLine": 325, + "problem": "`commitAndPushFindings` 改成一律透過 `pushWithCredential` 用 PAT extraheader 推送,且宣稱會清掉 checkout 自動 token、避免 token 進 argv、失敗時不外洩 URL/憑證;但這些安全與觸發 CI 的關鍵行為沒有測試驗證。未測的失敗路徑尤其危險,因為一旦環境變數組錯,可能推送失敗或回到自動 token 而不觸發下一輪檢查。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 `commitAndPushFindings`/`pushWithCredential` 的推送認證路徑、token 不進 argv、錯誤不外洩憑證與 CI 觸發相關行為缺少測試。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 13, + "endLine": 74, + "problem": "`agentFailureDetail` 現在會把 AI CLI 的 stderr/stdout 片段寫進 CI log,並依賴 `redactSecrets` 遮罩機密與控制字元;這是新增的失敗診斷行為,但沒有看到測試驗證邊界與失敗輸出。若遮罩規則漏掉,測試沒守住就可能把 token、Authorization header 或含換行的偽造 log 直接輸出。", + "reason": "Paladin:可排除(重複)。歷史 findings 已記錄 `agentFailureDetail`/`redactSecrets` 對 Authorization、token、URL 帳密、控制字元、截斷與空輸出等邊界缺少測試。" + }, + { + "addedAt": "2026/07/20 18:07:26", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitea.js", + "startLine": 169, + "endLine": 195, + "problem": "新增 `addIssueDependency` 封裝 Gitea issue dependency endpoint,但沒有對 endpoint、HTTP method 與 body 語意補測試。這個 API 的方向性很重要:是讓 PR 相依於追蹤 issue;若 body 的 `index` 或 URL 上的 issue number 寫反,測試沒抓到就會變成錯誤的阻擋關係,甚至完全沒有阻擋效果。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出 `addIssueDependency` 缺少 endpoint、HTTP method、payload 與相依方向的契約測試,與本條指控相同。" } ] -- 2.53.0 From d7cecc8fa858abd63e5ba43f80d6545a55ff3495 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 18:33:01 +0800 Subject: [PATCH 54/79] =?UTF-8?q?chore(ai-review=20findings):=20=E5=AF=AB?= =?UTF-8?q?=E5=9B=9E=E8=AD=B0=E9=A1=8C=20#11=E2=80=93#23=20=E5=8F=AA?= =?UTF-8?q?=E4=BE=86=E8=87=AA=E8=AD=B0=E9=A1=8C=E7=9A=84=E5=BE=85=E4=BA=BA?= =?UTF-8?q?=E5=B7=A5=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 以 --issue all 處理 13 個建問題模式追蹤議題,逐條對照目前程式碼與 exclusions.json 後: ✅ 已解決 18 條(現行程式碼已修)、🚫 誤報 28 條(已列入 exclusions,不重複新增)、 ⏭️ 待人工 44 條(設計/效能/慣例取捨,依主題去重寫回 42 條追蹤)。各議題已留言處理進度並關閉。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../findings/2026-07-20-18:31:07.json | 601 ++++++++++++++++++ 1 file changed, 601 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-20-18:31:07.json diff --git a/.gitea/ai-review/findings/2026-07-20-18:31:07.json b/.gitea/ai-review/findings/2026-07-20-18:31:07.json new file mode 100644 index 0000000..1584784 --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-20-18:31:07.json @@ -0,0 +1,601 @@ +{ + "generatedAt": "2026/07/20 18:31:07", + "commitSha": "0c29daa01434d6b6fa1b20ce81c513c75b1b1c6a", + "prNumber": null, + "tool": { + "name": "code-review-resolve", + "version": "0.0.9", + "model": "(工具預設)" + }, + "findings": [ + { + "id": "F001", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 119, + "endLine": 139, + "problem": "流程步驟編號被當成跨模組識別值,散落在 `index.js`、各 library 的日誌與 JSDoc、README 流程圖及功能表。這次僅因插入並延後一步,就必須同步修改大量 `步驟2`~`步驟8` 字串,而且實際執行順序已變成 1、3~8、2、9~10;未來再調整流程時非常容易讓文件、日誌與程式碼脫節。", + "suggestion": "以穩定的語意階段名稱取代硬編碼序號,例如 `TOOL_DETECTION`、`COLLECT_DIFF`、`RESOLVE_OLD_COMMENTS`,由單一常數表集中決定顯示名稱;README 的流程順序則從同一份定義產生,或至少不要在各函式文件重複紀錄易變的數字。", + "suggestedCode": "```\nconst PHASE = Object.freeze({\n FAST_RESULT: '快速回報',\n DETECT_TOOL: '偵測 AI 工具',\n COLLECT_DIFF: '整理變更',\n RESOLVE_OLD: '處理舊留言',\n PUBLISH_FINDINGS: '發布審查結果',\n});\n\nlog(PHASE.DETECT_TOOL, 'INF', `選用工具:${tool.name}。`);\n```", + "sourceIssue": 11 + }, + { + "id": "F002", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "警告", + "file": "readme.md", + "startLine": 59, + "endLine": 130, + "problem": "功能索引把分支名稱與原始碼行號硬編碼在數十個連結中;本次僅因程式碼增行,就必須人工把大量 `#L...` 全面更新,已直接顯示這份文件存在高同步成本。之後任一檔案前段增刪程式碼,都會讓這些連結再次漂移,而且指向會持續變動的 `develop` 分支,使舊版 README 與實際連結內容無法穩定對應。", + "suggestion": "不要手動維護原始碼行號。若只需導覽,連到檔案並由右欄既有章節錨點提供函式級定位;若必須精確指向定義,應由 AST/文件產生工具在 CI 自動建立索引,並連到固定 commit SHA 或版本 tag。至少增加連結檢查,避免半年後整張功能表悄悄失準。", + "suggestedCode": "```\n| 功能名稱 | 功能描述 |\n| --- | --- |\n| [gitrepo.resolveMergeBase](src/lib/gitrepo.js) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |\n```", + "sourceIssue": 12 + }, + { + "id": "F003", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 13, + "endLine": 82, + "problem": "`agentFailureDetail()` 同時負責解析程序失敗、決定 debug 政策、讀取全域環境變數、截斷輸出及遮罩機密。尤其直接讀取 `process.env.ACTIONS_STEP_DEBUG` 形成隱藏相依,測試不同輸出政策時必須修改程序全域狀態;日後若其他呼叫端需要不同診斷層級,也只能繼續往這個函式堆條件。", + "suggestion": "把診斷政策改成明確參數,並將「錯誤中繼資料整理」與「輸出片段清理」拆成小函式;在 `main` 或 context 載入階段解析環境設定後注入。這能讓各種 exit code、signal、空輸出與 verbose 模式以純輸入輸出直接測試。", + "suggestedCode": "```\nfunction agentFailureDetail(res, { includeStdout = false, inputLimit = 2000, outputLimit = 500 } = {}) {\n const parts = failureMetadata(res && res.error);\n appendSanitizedOutput(parts, 'stderr', res && res.stderr, inputLimit, outputLimit);\n if (includeStdout) {\n appendSanitizedOutput(parts, 'stdout', res && res.output, inputLimit, outputLimit);\n }\n return parts.length ? parts.join('|') : 'AI CLI 執行失敗(無診斷輸出)';\n}\n```", + "sourceIssue": 12 + }, + { + "id": "F004", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 42, + "endLine": 61, + "problem": "`agentFailureDetail` 的文件自相矛盾:開頭宣稱「原始輸出預設隱藏」,後文卻明言預設附上經遮罩的 `stderr` 與 `stdout` 片段;另提到 `ACTIONS_STEP_DEBUG`,函式內卻沒有相應分支。註解與實作各唱各的調,維護者無法從文件判斷實際日誌行為。", + "suggestion": "統一文件敘述為實際行為:預設輸出經遮罩且限長的診斷片段;若目前並未依 `ACTIONS_STEP_DEBUG` 改變輸出,就移除該段說明,或待真正實作開關後再補上。", + "suggestedCode": "", + "sourceIssue": 13 + }, + { + "id": "F005", + "reviewer": "Rogue", + "focus": "", + "badge": "⚡", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 216, + "endLine": 218, + "problem": "在 `ensureIssueCreated` 函式中,使用 `for...of` 迴圈搭配 `await` 來逐條對 Gitea API 發送留言請求。由於網路請求存在延遲(每次 RTT 約 100-300ms),在迴圈內阻塞式等待會導致整體執行時間隨留言數量線性增加,浪費大量 CPU 週期與網路連線資源。", + "suggestion": "可以將 `issueBuffer` 中的所有情境留言合併為單一 Markdown 留言發送,僅需一次 API 呼叫;或者使用 `Promise.all` 將這些無相依性的留言請求並行化發送,大幅降低總延遲。", + "suggestedCode": "```\nif (issueBuffer.length > 0) {\n const combinedBody = issueBuffer.join('\\n\\n---\\n\\n');\n await gitea.createCommentOnIssue(ctx, issue.number, combinedBody);\n }\n issueBuffer.length = 0;\n```", + "sourceIssue": 14 + }, + { + "id": "F006", + "reviewer": "Rogue", + "focus": "", + "badge": "⚡", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 134, + "endLine": 134, + "problem": "在檢查是否為淺層 repository (shallow repository) 時,呼叫了外部子行程執行 `git rev-parse --is-shallow-repository`。建立與啟動 OS 子行程是非常昂貴的操作,會白白浪費數十毫秒的 CPU 週期與系統資源。", + "suggestion": "Git 在淺層 clone 時會在 `.git` 目錄下建立一個 `shallow` 檔案。我們可以使用 Node.js 內建的 `fs.existsSync` 進行本地檔案檢查,不需啟動額外的 Git 子行程,執行速度可快上百倍。", + "suggestedCode": "```\nconst fs = require('fs');\n// ...\nif (fs.existsSync(path.join(cwd, '.git', 'shallow'))) {\n strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']);\n}\n```", + "sourceIssue": 14 + }, + { + "id": "F007", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 293, + "endLine": 305, + "problem": "為了防止 Git 推送失敗時在例外訊息中回顯包含 Token 與遠端 URL 的命令列參數,pushWithCredential 的 catch 區塊直接拋出一個固定的 Error('推送審查結果 commit 失敗...')。但這樣一來,它完全吞掉了原始的錯誤(例如 non-fast-forward 非快轉、分支保護規則阻擋、或連線逾時),六個月後的維護者在 CI log 中看到此錯誤時,完全無從判斷失敗的原因。", + "suggestion": "建議在保留安全遮罩的前提下,保留原始 exception 的排錯線索。例如可以檢查並安全地過濾 err.message 或 err.stderr 中所有的敏感字串(如 Token/URL),然後將其作為新錯誤的 cause 屬性或附加訊息傳遞下去。", + "suggestedCode": "```\n} catch (err) {\n // 過濾敏感資訊後保留錯誤細節\n const safeMessage = err.message ? redactSecrets(err.message) : '未知錯誤';\n const error = new Error(`推送審查結果 commit 失敗(${safeMessage})。`);\n error.cause = err;\n throw error;\n }\n```", + "sourceIssue": 14 + }, + { + "id": "F008", + "reviewer": "Assassin", + "focus": "", + "badge": "🗡️", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 299, + "endLine": 299, + "problem": "在 `pushWithCredential` 中,環境變數 `GIT_CONFIG_KEY_0` 被動態拼接為 `http.${remoteUrl}.extraheader`。如果 `remoteUrl` 來自外部或未經嚴格驗證的 context,且 URL 中包含特殊字元(如點號、路徑分隔符號或引號等),可能會導致 Git 配置解析錯誤,或在特定平台下引發 Git 配置參數注入風險。", + "suggestion": "由於該執行程序僅為一次性的 `git push` 操作,可直接將該憑證應用於所有 HTTP 請求,將 `GIT_CONFIG_KEY_0` 設定為靜態的 `http.extraheader`,以避免動態拼接 URL 所帶來的注入風險。", + "suggestedCode": "```\nGIT_CONFIG_KEY_0: 'http.extraheader',\n```", + "sourceIssue": 14 + }, + { + "id": "F009", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 308, + "endLine": 368, + "problem": "新增或修改的步驟分隔註解(如步驟 8 分組、步驟 2 延後、步驟 9、步驟 10、及建問題模式收束)其尾隨的水平分隔線(─)長度不一或僅存單一字元,破壞了專案既有程式碼中整齊劃一的長分隔線視覺排版,視覺上顯得雜亂、走調。", + "suggestion": "補足尾隨的水平線 ─,使其與鄰近步驟分隔註解的長度(約 70~80 字元寬度)與視覺風格保持一致,維持排版的美觀。", + "suggestedCode": "```\n// ── 步驟 8(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ────────────────\n```", + "sourceIssue": 14 + }, + { + "id": "F010", + "reviewer": "Rogue", + "focus": "", + "badge": "⚡", + "severity": "建議", + "file": "src/index.js", + "startLine": 366, + "endLine": 382, + "problem": "在建問題模式收束時,先 `await gitea.createIssueComment` 再 `await gitea.addIssueDependency`,這兩個 Gitea API 呼叫是獨立且無資料相依性的,卻以序列(Sequential)方式執行,白白浪費了一次網路往返(RTT)的等待時間。", + "suggestion": "使用 `Promise.all` 同時發起這兩個請求,並行處理以減少整體 execution 的等待時間。", + "suggestedCode": "```\nconst commentPromise = gitea.createIssueComment(\n ctx,\n templates.issueLinkComment({\n issueNumber: issue.number,\n issueUrl: issue.html_url,\n severeCount: severe.length,\n otherCount: others.length,\n })\n );\n const dependencyPromise = gitea.addIssueDependency(ctx, ctx.prNumber, issue.number)\n .then(() => log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`))\n .catch((err) => log('建問題', 'WRN', `設定 PR 相依失敗:${err.message}。`));\n \n await Promise.all([commentPromise, dependencyPromise]);\n```", + "sourceIssue": 14 + }, + { + "id": "F011", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 33, + "endLine": 34, + "problem": "在 redactSecrets 函式中,針對 authorization 以及其他憑證關鍵字(如 token、secret、password 等)的敏感資訊遮蔽,分別使用了兩條結構極為相似的正規表示式進行替換。這造成了重複的替換邏輯與額外的處理開銷,程式碼的旋律顯得不夠俐落。", + "suggestion": "建議將這兩條正規表示式合併為單一表達式,消除重複的 replace 呼叫,使程式碼更加簡潔優雅且提升運行效率。", + "suggestedCode": "```\n.replace(/((?:authorization|api[_-]?key|token|password|secret|bearer)\\s*[:=]\\s*)\\S+/gi, '$1***')\n```", + "sourceIssue": 14 + }, + { + "id": "F012", + "reviewer": "Rogue", + "focus": "", + "badge": "⚡", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 48, + "endLine": 53, + "problem": "在 `agentFailureDetail` 之中,進行 stderr 與 stdout 的遮罩處理時,是先截斷至 2,000 字元,然後執行多次複雜的 `redactSecrets` 正規表示式替換,最後再截斷至 500 字元輸出。這會造成 1,500 字元的複雜 regex 運算結果在下一步被直接丟棄,白白浪費了 CPU 進行字串比對與取代的週期。", + "suggestion": "應在呼叫 `redactSecrets` 之前,就先將字串截斷至目標長度(500 字元),再進行遮罩,可大幅減少 regex 運算負擔。", + "suggestedCode": "```\nconst stderr = redactSecrets(String((res && res.stderr) || '').slice(0, 500));\n if (stderr) parts.push(`stderr:${stderr}`);\n const stdout = redactSecrets(String((res && res.output) || '').slice(0, 500));\n if (stdout) parts.push(`stdout:${stdout}`);\n```", + "sourceIssue": 14 + }, + { + "id": "F013", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/templates.js", + "startLine": 335, + "endLine": 337, + "problem": "issueFindingComment 函式的 @remarks 文件註解中,說明其使用情境為『建問題模式下 review.postSevereToIssue 把每條嚴重 finding... 作為問題明細的追蹤紀錄』。然而實際上,非嚴重的警告與建議(others)也會透過 review.postOthersToIssue 呼叫此模板進行發布,導致文件描述不夠完整。", + "suggestion": "修正 @remarks 的使用情境說明,將 postOthersToIssue 亦併入描述中,使 JSDoc 文件能如實且精準地反映實際程式碼的呼叫情境。", + "suggestedCode": "```\n* 使用情境:建問題模式下 `review.postSevereToIssue` 與 `review.postOthersToIssue`(src/lib/review.js)把保留的各級問題明細\\n * 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上,\\n * 作為問題明細的追蹤紀錄。\n```", + "sourceIssue": 14 + }, + { + "id": "F014", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "建議", + "file": "readme.md", + "startLine": 56, + "endLine": 132, + "problem": "README 的功能表大量手動維護 `src/branch/develop/...#Lxx` 深連結,這次光是分支與行號就改了整排。這類文件會隨任何插入註解、重排函式、換預設分支而失準,未來維護者必須在改程式時同步更新一大段文件,維護成本偏高。", + "suggestion": "改成不依賴行號的相對連結,或用文件產生腳本從原始碼 JSDoc 自動產出這張表。若仍要指向 Gitea,建議至少移除 `#Lxx`,或集中定義分支名稱,避免每次改分支都要全表搜尋替換。", + "suggestedCode": "", + "sourceIssue": 15 + }, + { + "id": "F015", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 190, + "endLine": 197, + "problem": "`dependency` 作為參數名太薄,與 `issueNumber` 並列時看不出它也是 issue 編號。讀到 `addIssueDependency(ctx, ctx.prNumber, issue.number)` 時,語意要靠上下文補拍子。", + "suggestion": "改用更完整的名稱,例如 `dependencyIssueNumber` 或 `blockingIssueNumber`,讓「誰被誰阻擋」在簽名裡就清楚成形。", + "suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", + "sourceIssue": 15 + }, + { + "id": "F016", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 28, + "endLine": 37, + "problem": "這段註解的旋律前後打架:開頭說「原始輸出預設隱藏」,後文卻說失敗時預設附上遮罩後的 stderr/stdout 片段。讀者才剛建立心智模型,下一拍就被改調。", + "suggestion": "請讓摘要句與實際行為一致,明確說明「原始輸出不直接輸出,但會輸出遮罩與截斷後的診斷片段」。", + "suggestedCode": "```\n* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,並附上遮罩、去控制字元且截斷後的 stderr/stdout 片段。\n```", + "sourceIssue": 15 + }, + { + "id": "F017", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/templates.js", + "startLine": 334, + "endLine": 341, + "problem": "`issueFindingComment` 的註解只提到 `review.postSevereToIssue`,但主流程也以 `postOthersToIssue` 發布警告與建議。文件把共用模板寫成嚴重問題專用,讀起來像少了一個聲部。", + "suggestion": "把 remarks 改成同時涵蓋嚴重、警告與建議的 issue 留言產生器,避免維護者誤以為它只服務嚴重 finding。", + "suggestedCode": "", + "sourceIssue": 15 + }, + { + "id": "F018", + "reviewer": "Assassin", + "focus": "", + "badge": "🗡️", + "severity": "嚴重", + "file": "src/lib/gitrepo.js", + "startLine": 102, + "endLine": 149, + "problem": "攻擊者可以透過提交惡意 Pull Request,將 `baseRef`(PR 目標分支)命名為包含路徑穿越(Path Traversal)的字串,例如 `../../hooks/pre-push`。由於 `resolveMergeBase` 直接將 `baseRef` 拼接至 `git fetch` 的 Refspec 參數中(例如 `+refs/heads/${baseRef}:refs/remotes/origin/${baseRef}`),這將導致 `git fetch` 寫入至 `.git/refs/remotes/origin/../../hooks/pre-push`(即 `.git/hooks/pre-push`)。這會覆寫或建立 Git Hook,並在後續執行 git 操作時自動觸發該惡意 Hook,從而造成遠端程式碼執行(RCE)。同樣地,`commitAndPushFindings` 函數中的 `headRef` 也存在類似的拼接風險。", + "suggestion": "在將 `baseRef` 與 `headRef` 傳入 git 指令之前,應進行嚴格的合法性檢查。建議使用正則表達式限制分支名稱僅能包含安全的字元(如英數字、斜線、底線、連字號、句點),且絕對不得含有 `..` 或以 `-` 開頭,必要時亦可使用 `git check-ref-format` 命令先行驗證該分支名稱是否安全。", + "suggestedCode": "```\nfunction resolveMergeBase(cwd, baseRef) {\n // 嚴格的分支名稱白名單檢查,防止路徑穿越與參數注入\n const safeBranchRegex = /^(?!-)(?!.*?\\.\\.)[a-zA-Z0-9/_.-]+$/;\n if (!safeBranchRegex.test(baseRef)) {\n throw new Error(`偵測到不合法的分支名稱: ${baseRef}`);\n }\n const remoteBase = `origin/${baseRef}`;\n```", + "sourceIssue": 16 + }, + { + "id": "F019", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 13, + "endLine": 72, + "problem": "`redactSecrets()` 與 `agentFailureDetail()` 是低階日誌診斷/遮罩邏輯,現在放在 `review.js` 這個負責 diff 整理與審查決策的模組頂端。這會讓 `review.js` 的職責繼續膨脹:未來若其他模組也要安全輸出 CLI 錯誤,只能複製這段或反向依賴 review 模組,邊界會越來越不清楚。", + "suggestion": "把這兩個函式搬到專門的工具模組,例如 `src/lib/diagnostics.js` 或 `src/lib/log-redaction.js`,並由 `review.js` 引入。這樣遮罩規則可集中測試與重用,`review.js` 也能維持在「審查流程資料處理」的邊界內。", + "suggestedCode": "", + "sourceIssue": 17 + }, + { + "id": "F020", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 41, + "endLine": 45, + "problem": "這段註解的旋律前後走調:摘要先說「原始輸出預設隱藏」,下一段卻說失敗時「預設附上 stderr 與 stdout」。讀者還沒進函式本體,文件本身就已經互相拉扯。", + "suggestion": "請讓摘要與實作同拍,直接說明會輸出經遮罩與限長的診斷片段;若真的要隱藏原始輸出,也應同步改實作。", + "suggestedCode": "```\n* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,並附上經遮罩與限長的 stderr/stdout 片段。\n```", + "sourceIssue": 17 + }, + { + "id": "F021", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/templates.js", + "startLine": 334, + "endLine": 338, + "problem": "`issueFindingComment` 的文件只唱「嚴重 finding」,但新版流程也讓警告與建議逐條發到 issue。函式名稱是通用的,註解卻把用途寫窄,後續讀者會誤以為它只服務嚴重問題。", + "suggestion": "把註解改成涵蓋所有 finding 等級,讓文件與函式名稱、呼叫情境保持一致。", + "suggestedCode": "```\n* 使用情境:建問題模式下,`review.postSevereToIssue` 與 `review.postOthersToIssue`\n * 會把各等級 finding 以本函式產生留言內容、經 `gitea.createCommentOnIssue`\n * 發布到追蹤 issue 上,作為問題明細的追蹤紀錄。\n```", + "sourceIssue": 17 + }, + { + "id": "F022", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "警告", + "file": "readme.md", + "startLine": 76, + "endLine": 132, + "problem": "README 內大量函式清單同時硬編分支名稱與行號錨點,這次 diff 已經整批從 `master` 改成 `develop` 並同步調整行號。這類文件和原始碼結構高度重複,後續只要插入幾行程式,文件連結就會失準,維護者必須靠人工記得同步整張表。", + "suggestion": "改成不含行號的穩定檔案連結,或把這份 API/功能表改由 JSDoc/腳本產生。若一定要保留行號,建議把產生流程寫入 npm script,避免每次程式碼位移都人工批次修改 README。", + "suggestedCode": "", + "sourceIssue": 18 + }, + { + "id": "F023", + "reviewer": "Mage", + "focus": "", + "badge": "🔮", + "severity": "警告", + "file": "src/index.js", + "startLine": 374, + "endLine": 387, + "problem": "建問題模式下只要有任何保留 finding 就會建立 issue,且後續一律把 PR 設為相依於該 issue;但收尾結果仍是 `severe.length === 0 ? 'success' : 'failure'`。最小情境:攻擊方只產生 1 條「建議」,`severe.length` 為 0,action commit `[success]`,但 PR 被 issue dependency 擋住無法合併。這讓「success=可通過」與「非嚴重問題也阻擋合併」兩個語義互相矛盾。", + "suggestion": "明確對齊語義:若只有嚴重問題才應阻擋合併,則只在 `severe.length > 0` 時建立 dependency;若所有保留問題都要阻擋合併,則 result/exit code 不應只看嚴重問題。", + "suggestedCode": "```\nif (severe.length > 0) {\n try {\n await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number);\n log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`);\n } catch (err) {\n log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`);\n }\n}\n```", + "sourceIssue": 18 + }, + { + "id": "F024", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 179, + "endLine": 181, + "problem": "`issueBuffer` 的旋律太含糊:讀者會以為裡面放的是 issue,實際上暫存的是尚未送出的留言 body。命名沒有把資料形狀唱清楚。", + "suggestion": "改成能描述內容與用途的名稱,例如 `pendingIssueCommentBodies`,並同步調整註解與迴圈變數。", + "suggestedCode": "```\nconst pendingIssueCommentBodies = [];\nlet issue = null;\n```", + "sourceIssue": 18 + }, + { + "id": "F025", + "reviewer": "Assassin", + "focus": "", + "badge": "🗡️", + "severity": "警告", + "file": "action.yml", + "startLine": 21, + "endLine": 24, + "problem": "這裡建議呼叫端傳入「能觸發 CI 的 PAT」作為 action token。攻擊者最愛這種長效、可推送、可觸發 workflow 的憑證:只要此 action 在不受信任 PR 上執行,或 PR 能影響 action/workflow 執行內容,惡意變更就可能讀取 `INPUT_TOKEN`、推送結果 commit、再藉由可觸發 CI 的身分製造後續執行鏈。自動 token 原本不觸發 CI 是一道防線,這個建議等於要求使用者把防線拆掉。", + "suggestion": "不要泛稱建議使用可觸發 CI 的 PAT。文件與介面應明確要求最小權限、repo 限定、短效或可輪替 token,並禁止在 fork/不受信任 PR context 暴露 PAT。更穩的設計是分離 API 留言 token 與 push token,且只有在明確受信任事件或受保護分支才允許 push token 存在;否則拒絕 commit/push,只做留言或 artifact。", + "suggestedCode": "", + "sourceIssue": 19 + }, + { + "id": "F026", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "建議", + "file": "readme.md", + "startLine": 59, + "endLine": 132, + "problem": "README 的功能列表手動維護了大量 `src/branch/develop/...#Lxx` 深連結與行號。這次 PR 已經一次改動數十個 branch/line anchor,代表文件和原始碼行號高度耦合;下一次只要插入幾行程式,文件就會悄悄過期,維護者很難知道哪些連結還準。", + "suggestion": "避免在手寫 README 綁定行號,改連到函式所在檔案或穩定章節錨點;若必須保留行號,請把這段改成產生式文件,讓 CI 或腳本從原始碼/JSDoc 重新生成,減少人工同步成本。", + "suggestedCode": "", + "sourceIssue": 19 + }, + { + "id": "F027", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 181, + "endLine": 185, + "problem": "`issue` 這個變數名太素,像樂譜上只寫「音符」卻不說是哪一聲部。此處承載的是建問題模式建立出的追蹤 issue,後面還會與 PR issue 編號、Gitea issue API 參數交錯出現,名稱過泛會讓閱讀節奏變濁。", + "suggestion": "改成能表明角色的名稱,例如 `trackingIssue`。對應的 `ensureIssueCreated` 也可改為 `ensureTrackingIssueCreated`,讓閉包狀態與用途一眼對上。", + "suggestedCode": "```\nconst issueBuffer = [];\nlet trackingIssue = null;\n\nconst postComment = async (body) => {\n if (ctx.createIssue) {\n if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n issueBuffer.push(body);\n return null;\n }\n const created = await gitea.createIssueComment(ctx, body);\n currentRunCommentIds.add(created.id);\n return created;\n};\n```", + "sourceIssue": 19 + }, + { + "id": "F028", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 14, + "endLine": 75, + "problem": "`review.js` 這次新增 `redactSecrets()` 與 `agentFailureDetail()`,但這兩個函式處理的是 AI CLI 執行失敗診斷與機密遮罩,責任更接近 `agents.js` 或共用 log/sanitize 工具。現在審查結果整理模組同時負責 diff、裁決、issue 發文與 CLI 診斷格式,模組邊界越來越鬆;之後其他地方若也要記錄 agent 失敗,很容易複製一份遮罩邏輯或反向依賴 `review.js`。", + "suggestion": "將這兩個函式移到 `src/lib/agents.js`(例如匯出 `formatAgentFailure()`),或新增 `src/lib/sanitize.js`/`src/lib/diagnostics.js`。`review.js` 只消費格式化後的錯誤摘要,避免讓審查編排模組承擔 CLI 診斷細節。", + "suggestedCode": "", + "sourceIssue": 19 + }, + { + "id": "F029", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/templates.js", + "startLine": 388, + "endLine": 405, + "problem": "`issueLinkComment` 產生的是 PR 上唯一的建問題模式回貼留言,但名稱少了 PR 的聲部;同檔已有 `issueBody`、`issueFindingComment`,乍看會以為這也是 issue 內留言模板,命名層次不夠分明。", + "suggestion": "改名為 `prIssueLinkComment` 或 `trackingIssueLinkComment`,讓模板的投遞位置與用途直接寫在名稱裡,避免與 issue 內文、issue finding 留言混成一團。", + "suggestedCode": "```\nfunction prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {\n return `${MARK}\n## 🔍 AI Code Review|已建立追蹤問題\n\n本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`;\n}\n```", + "sourceIssue": 19 + }, + { + "id": "F030", + "reviewer": "Rogue", + "focus": "", + "badge": "⚡", + "severity": "警告", + "file": "src/index.js", + "startLine": 369, + "endLine": 370, + "problem": "建問題模式把所有警告/建議改成逐條發 issue 留言,這裡會把 `others.length` 放大成 N 次遠端 POST;正常模式同一批資料只產生 1 則彙整表格留言。只要 AI 回出數十條警告,CI 時間就會被 API round-trip 線性吃掉,還更容易撞上 Gitea rate limit 或暫時性網路延遲。", + "suggestion": "警告/建議維持批次彙整成單一留言;只有嚴重問題需要逐條追蹤時再拆開。若產品需求一定要逐條回覆,至少在 `postOthersToIssue` 內用有上限的並行池,不要一筆等一筆。", + "suggestedCode": "```\nif (others.length > 0) {\n if (ctx.createIssue) {\n await gitea.createCommentOnIssue(ctx, issue.number, templates.othersComment(others));\n log('步驟10', 'INF', `警告+建議表格留言已發布到 issue(${others.length} 條)。`);\n } else {\n await postComment(templates.othersComment(others));\n log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);\n }\n}\n```", + "sourceIssue": 20 + }, + { + "id": "F031", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "建議", + "file": "readme.md", + "startLine": 56, + "endLine": 132, + "problem": "README 的功能表大量硬編遠端分支名稱與行號,這次只是從 `master` 改成 `develop` 並同步行號,但這種文件很容易在下一次函式移動、預設分支更名或重排時再次整批失準。未來維護者會被迫反覆做低價值的連結校正,文件也可能在沒人注意時指到錯誤位置。", + "suggestion": "若 README 是 repo 內文件,優先改成相對路徑連結,並避免固定行號;若必須保留行號,建議用產生腳本統一輸出這張表,讓分支名與行號只從單一來源計算。", + "suggestedCode": "", + "sourceIssue": 21 + }, + { + "id": "F032", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 183, + "endLine": 224, + "problem": "`issue` 與 `issueBuffer` 的命名過於泛泛;在 Gitea 裡 PR 也是 issue,追蹤問題也是 issue,單靠 `issue` 這個名字無法唱出它究竟是哪一個聲部。", + "suggestion": "建議改成更具語義的名稱,例如 `trackingIssue`、`trackingIssueCommentBuffer`,讓讀者不用回讀 create-issue 模式的整段脈絡。", + "suggestedCode": "```\nlet trackingIssue = null;\nconst trackingIssueCommentBuffer = [];\n```", + "sourceIssue": 21 + }, + { + "id": "F033", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 190, + "endLine": 194, + "problem": "`addIssueDependency(ctx, issueNumber, dependency)` 的兩個參數名稱太相似,且 `dependency` 少了 issue 語義。這支 API 的方向性本來就容易讀錯,命名再模糊就像兩個音符共用同一個名字。", + "suggestion": "建議把參數改成能表達方向的名稱,例如 `blockedIssueNumber` 與 `blockingIssueNumber`,呼叫端也會更清楚是誰被誰擋住。", + "suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", + "sourceIssue": 21 + }, + { + "id": "F034", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 36, + "endLine": 43, + "problem": "`agentFailureDetail` 的 JSDoc 先說「原始輸出預設隱藏」,下一段又說「預設附上 stderr 與 stdout 片段」。同一段說明前後轉調,讀者會搞不清楚失敗診斷到底會不會輸出 CLI 內容。", + "suggestion": "請統一描述:若設計是輸出已遮罩片段,就刪掉「預設隱藏」;若設計是隱藏原始輸出,就把後段改成條件式說明。", + "suggestedCode": "", + "sourceIssue": 21 + }, + { + "id": "F035", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/templates.js", + "startLine": 334, + "endLine": 338, + "problem": "`issueFindingComment` 是通用 finding 留言模板,但更新後的說明只寫 `review.postSevereToIssue` 與「嚴重 finding」。然而主流程也將警告/建議逐條發到 issue,這段文件把模板唱窄了,和實際用途不一致。", + "suggestion": "把 remarks 改成涵蓋嚴重、警告與建議的通用 issue finding 留言,避免日後維護者誤以為此模板只能用於嚴重問題。", + "suggestedCode": "", + "sourceIssue": 21 + }, + { + "id": "F036", + "reviewer": "Rogue", + "focus": "", + "badge": "⚡", + "severity": "警告", + "file": "src/index.js", + "startLine": 305, + "endLine": 305, + "problem": "攻擊方沒有找出任何 finding 時,這裡還是照樣啟動防守方 `runDefenders`。空陣列沒有東西可裁決,卻會多跑一輪 AI CLI/sub agent、讀 exclusions/history、組 prompt;每個乾淨 PR 都被偷走 1 次防守方呼叫的 CPU、等待時間與 token。", + "suggestion": "在 `findings.length === 0` 時直接略過防守方裁決,令 `kept/excluded` 都是空陣列,直接進入保存結果與收尾。這不是微優化,是整輪 AI 呼叫直接歸零。", + "suggestedCode": "```\nconst { kept, excluded } = findings.length === 0\n ? { kept: [], excluded: [] }\n : await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });\n```", + "sourceIssue": 22 + }, + { + "id": "F037", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/gitrepo.js", + "startLine": 310, + "endLine": 310, + "problem": "`pushWithCredential` 的參數名叫 `secret`,但同一檔其他區段與呼叫端都稱它為 `token`;同一個旋律忽然換調,讀者需要多花心力確認這是不是另一種憑證。", + "suggestion": "沿用既有命名,把 `secret` 改成 `token`,並同步調整 JSDoc 與 `Buffer.from` 內的引用。", + "suggestedCode": "```\nfunction pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) {\n const basic = Buffer.from(`ai-review-bot:${token}`).toString('base64');\n // ...\n}\n```", + "sourceIssue": 22 + }, + { + "id": "F038", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 31, + "endLine": 46, + "problem": "`agentFailureDetail` 的註解前後語意不一致:開頭寫「原始輸出預設隱藏」,但後段又說預設會附上遮罩後的 stderr/stdout;最後還提到用 `ACTIONS_STEP_DEBUG=true` 取得原始輸出,但程式碼沒有任何 debug flag 分支。這種文件與實作脫節,會讓未來維護者誤判 CI log 會暴露多少診斷內容。", + "suggestion": "把註解改成符合目前實作:預設輸出限長且遮罩後的 stderr/stdout;若要支援 debug 模式,再補實作分支。不要在註解承諾程式沒有做的行為。", + "suggestedCode": "", + "sourceIssue": 22 + }, + { + "id": "F039", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/templates.js", + "startLine": 337, + "endLine": 340, + "problem": "`issueFindingComment` 是一般 finding 留言模板,但註解只寫 `review.postSevereToIssue` 的嚴重問題用途;和主流程中「警告+建議也逐條發到 issue」的描述不一致,註解像只唱了半段副歌。", + "suggestion": "把 remarks 改成涵蓋嚴重、警告與建議的共用用途,避免後續維護者誤以為此模板只服務嚴重問題。", + "suggestedCode": "", + "sourceIssue": 22 + }, + { + "id": "F040", + "reviewer": "Assassin", + "focus": "", + "badge": "🗡️", + "severity": "警告", + "file": "action.yml", + "startLine": 19, + "endLine": 22, + "problem": "這段新增說明鼓勵呼叫端傳入「能觸發 CI 的 PAT」。攻擊者最喜歡這種長效、高權限、可觸發 workflow 的憑證:若 action 跑在不可信 PR、AI CLI 被 prompt injection 誘導讀環境變數,或同 repo PR 可改動本 action 程式碼,就可能把 PAT 外送或濫用成寫入 repo/觸發 CI 的跳板。", + "suggestion": "不要把長效 PAT 當建議預設。改用最小權限、短效的 GitHub App/Gitea App token,並明確禁止在不可信 fork PR 傳入可寫 token。若目標只是回報檢查結果,優先用 status/check API 寫結果,不要靠 PAT push 再觸發下一輪 CI。", + "suggestedCode": "```\ndescription: 'Gitea API token(PR/issue 留言與 push findings 用;請使用最小權限、短效 token,勿在不可信 PR 傳入長效 PAT)'\n```", + "sourceIssue": 23 + }, + { + "id": "F041", + "reviewer": "Leo", + "focus": "", + "badge": "🧰", + "severity": "建議", + "file": "readme.md", + "startLine": 58, + "endLine": 131, + "problem": "README 的功能表把分支名稱與行號大量硬編在外部 URL 裡,這次已經需要整批 `master` 改 `develop` 並同步多個 `#Lxx`。這類文件會隨任何程式碼插行、函式移動或預設分支變更而失準,維護成本會線性累積,最後讀者點到的文件比沒有文件更誤導。", + "suggestion": "改用 repo 相對連結、不固定行號,或把這段功能表改由腳本從 JSDoc 自動產生。若需要連到特定實作,優先連到檔案或錨點,避免每次重排程式碼都要同步更新幾十個行號。", + "suggestedCode": "```\n| log.taipeiNow | [src/lib/log.js](src/lib/log.js) | 取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串 |\n| review.runAttackers | [src/lib/review.js](src/lib/review.js) | 攻擊方 sub agent 並行找問題並合併列表 |\n```", + "sourceIssue": 23 + }, + { + "id": "F042", + "reviewer": "Bard", + "focus": "", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 31, + "endLine": 40, + "problem": "這段註解的旋律前後失和:開頭寫「原始輸出預設隱藏」,下一句卻說預設附上 stderr 與 stdout 片段。讀者尚未進入程式碼,就已被兩個互相拉扯的描述絆住。", + "suggestion": "請讓文件只唱一個調性:若目前設計是預設輸出遮罩後的診斷片段,就刪掉「原始輸出預設隱藏」或改成「原始輸出會先遮罩與截斷」。", + "suggestedCode": "", + "sourceIssue": 23 + } + ], + "excluded": [] +} -- 2.53.0 From 05153bba8e2180875675737283ba2ec376420710 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 10:36:03 +0000 Subject: [PATCH 55/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 154 +++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index dbde87a..d86f869 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1318,5 +1318,159 @@ "endLine": 195, "problem": "新增 `addIssueDependency` 封裝 Gitea issue dependency endpoint,但沒有對 endpoint、HTTP method 與 body 語意補測試。這個 API 的方向性很重要:是讓 PR 相依於追蹤 issue;若 body 的 `index` 或 URL 上的 issue number 寫反,測試沒抓到就會變成錯誤的阻擋關係,甚至完全沒有阻擋效果。", "reason": "Paladin:可排除(重複)。歷史 findings 已指出 `addIssueDependency` 缺少 endpoint、HTTP method、payload 與相依方向的契約測試,與本條指控相同。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 68, + "endLine": 75, + "problem": "AI CLI 失敗時會把 stderr/stdout 片段預設寫進 CI log。攻擊者只要讓工具失敗,且讓 prompt、diff、CLI 錯誤或環境診斷中夾帶未被正規式涵蓋的秘密格式(例如 JWT、雲端憑證、私鑰片段、較短 token、含符號的密碼或 PII),就能把機密永久留在多人可讀的 workflow log。`redactSecrets` 是盡力遮罩,不足以作為洩漏邊界。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出 AI CLI 失敗時將 stderr/stdout 寫入 CI log、黑名單式遮罩可繞過,並可能洩漏機密或個資;本條是同一風險。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "警告", + "file": "action.yml", + "startLine": 22, + "endLine": 25, + "problem": "文件改成建議呼叫端傳入可觸發 CI 的 PAT。若 workflow 在 PR head 上執行此 action,或 action 程式碼可被 PR 修改,攻擊者可以把這顆 PAT 當成獵物:改寫 action、讓工具輸出、或藉由執行流程把 token 外送。自動 token 原本的防遞迴限制被 PAT 繞開後,等於把更高權限、更危險的憑證交給不可信變更。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 action.yml 建議使用可觸發 CI 的 PAT,導致不可信 PR 或可修改 action 程式碼時產生憑證外洩與濫用風險提出相同問題。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "action.yml", + "startLine": 3, + "endLine": 3, + "problem": "檔案標頭的「更新時間」仍停在 `2026/07/17 18:49:58`,但本次變更內容與提供的檔案最後更新時間已是 `2026/07/20`。這種手寫時間戳像樂譜上的舊拍號,讀者會懷疑文件到底是否同步。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 action.yml 檔頭手寫更新時間與實際版本不一致,屬同一問題。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "readme.md", + "startLine": 3, + "endLine": 3, + "problem": "README 的更新時間也停在 `2026/07/17 18:49:58`,與本次文件內容大幅改動不相稱。對讀者而言,這個欄位現在不是資訊,而是噪音。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 README 檔頭手寫更新時間與實際更新不符,屬同一問題。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 7, + "endLine": 7, + "problem": "啟動 banner 的更新時間仍是 `2026/07/17 18:49:58`,但同檔本次新增了大量流程與註解。執行時印出的版本感與實際程式節奏不一致,像開場音還停在舊調。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 src/index.js 啟動橫幅硬編碼更新時間失準,屬同一問題。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "readme.md", + "startLine": 42, + "endLine": 50, + "problem": "mermaid 圖用 `S1 -> S3 -> ... -> S8 -> S2 -> S9` 表示延後的步驟 2,節點代號與視覺流程反向交錯。雖然顯示文字能說明「步驟 2 延後」,但閱讀原始 Markdown 時節奏很拗,維護者很容易在後續增修時接錯線。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋流程步驟編號硬編碼於 README 與流程圖,造成文件與流程順序高耦合;本條只是針對 Mermaid 節點 ID 的同類表現。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 182, + "endLine": 238, + "problem": "`queueOrPostComment` 與 `createIssueAndFlushBufferedComments` 這兩段閉包夾在主流程前奏中,註解、狀態變數與模式分流交織在一起,讓 `main()` 的旋律還沒進入步驟 3 就先變成長篇宣敘。可讀性負擔集中在主流程,後面的步驟編排也因此更難掃描。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 main() 在步驟 3 前被 issueBuffer、issue 與兩個閉包函式打斷,留言路由與 issue 建立細節混入主流程,屬同一可讀性問題。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 407, + "problem": "`main()` 這次把留言路由、issue 暫存、標籤挑選、issue 建立、問題發布、PR 回貼、相依設定全部塞進同一個流程函式。半年後要改「建問題模式」其中一段時,維護者必須同時理解 `currentRunCommentIds`、`issueBuffer`、`issue` 閉包狀態與一般模式/建問題模式的分支時序,這會讓錯誤很容易藏在流程順序裡,也很難針對單一行為做單元測試。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列、標籤、相依關係與發布職責耦合的問題。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 32, + "endLine": 48, + "problem": "`agentFailureDetail` 的文件和實作描述互相打架:前面寫「原始輸出預設隱藏」,下一段又寫預設附上經遮罩的 stderr/stdout,後面還提到 `ACTIONS_STEP_DEBUG=true`,但實作沒有任何 debug 開關。這種註解漂移會讓未來維護者誤判 CI log 會不會輸出 agent 內容,進而在除錯或調整遮罩策略時做錯決策。", + "reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 agentFailureDetail 文件宣稱預設隱藏原始輸出,卻又描述預設輸出 stderr/stdout 且提到未實作的 ACTIONS_STEP_DEBUG 分支。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "readme.md", + "startLine": 76, + "endLine": 131, + "problem": "README 的功能列表把分支名稱與原始碼行號硬編在數十個連結裡;這次只是程式碼位移就必須大面積同步改 `#Lxx`。這種文件形態維護成本很高,未來只要函式上方增減幾行,文件就會悄悄指到錯誤位置。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出 README 功能列表硬編分支名稱與行號連結,維護成本高且容易漂移。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 320, + "endLine": 386, + "problem": "建問題模式這次改成「有保留問題才建 issue」、「先暫存工具/diff/角色留言」、「嚴重問題才加相依」、「無保留問題靜默通過」,但 diff 沒看到對應測試。這些是使用者可觀察到的流程分支,尤其 `kept.length === 0`、只有警告/建議、有嚴重問題、`addIssueDependency` 失敗降級,都還沒有被驗證。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的 issue 建立條件、暫存留言、嚴重與非嚴重分流、無 finding 靜默通過、標籤或相依 API 失敗等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 102, + "endLine": 147, + "problem": "`resolveMergeBase` 新增多段 fetch/deepen/unshallow 重試策略與診斷錯誤,但沒有看到測試覆蓋淺層 checkout 的失敗路徑。這段邏輯若順序或 refspec 錯,會直接讓審查抓不到正確 diff 基準。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、deepen、unshallow、停止條件與最終失敗診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 310, + "endLine": 340, + "problem": "`pushWithCredential` 改成用 `GIT_CONFIG_*` 注入 PAT、先清掉 checkout 的 extraheader,且失敗時改丟固定錯誤;這是認證與 CI 觸發的關鍵行為,但缺少測試驗證環境變數、refspec 與錯誤遮蔽。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushWithCredential/commitAndPushFindings 的 token 認證推送路徑、refspec、環境變數注入與錯誤遮蔽缺少測試。" + }, + { + "addedAt": "2026/07/20 18:36:02", + "prNumber": 6, + "reviewer": "Maya", + "severity": "建議", + "file": "src/lib/review.js", + "startLine": 13, + "endLine": 73, + "problem": "新增的 `redactSecrets`/`agentFailureDetail` 會把 AI CLI 的 stderr/stdout 寫進 CI log,雖然有遮罩與限長,但沒有測試驗證常見機密格式、控制字元與長輸出邊界是否真的被處理。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出 redactSecrets/agentFailureDetail 對常見憑證格式、URL 帳密、控制字元、截斷與空輸出等邊界缺少測試。" } ] -- 2.53.0 From 5ac73f290e31cc6bf36fff047829ef251a51468c Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 18:43:06 +0800 Subject: [PATCH 56/79] =?UTF-8?q?refactor(review):=20=E8=A8=BA=E6=96=B7?= =?UTF-8?q?=E8=BC=B8=E5=87=BA=E9=95=B7=E5=BA=A6=E5=B8=B8=E6=95=B8=E6=8F=90?= =?UTF-8?q?=E5=8D=87=E7=82=BA=E6=A8=A1=E7=B5=84=E5=B1=A4=E7=B4=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 依議題 #24 Bard 建議,將 agentFailureDetail 內的 INPUT_LIMIT/ AGENT_DIAGNOSTIC_OUTPUT_LIMIT 移至模組頂層,與既有 PER_FILE_DIFF_LIMIT/ TOTAL_DIFF_LIMIT 並列集中管理;值與行為不變。 Co-Authored-By: Claude Opus 4.8 (1M context) --- src/lib/review.js | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/lib/review.js b/src/lib/review.js index f348117..cbb2c7a 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -13,6 +13,14 @@ const templates = require('./templates'); const PER_FILE_DIFF_LIMIT = 16_000; const TOTAL_DIFF_LIMIT = 160_000; +// AI CLI 失敗診斷輸出政策常數(agentFailureDetail 使用):與上方送審上限並列於模組頂層, +// 集中管理長度政策,避免藏在函式中段。 +// INPUT_LIMIT:遮罩前先截去的輸入上限——先截再跑 redactSecrets,避免對數 MB 失敗輸出跑整份 +// O(k×n) 正規掃描;2000 字已足以涵蓋跨界機密樣式。 +const INPUT_LIMIT = 2_000; +// AGENT_DIAGNOSTIC_OUTPUT_LIMIT:每段診斷片段(stderr/stdout)寫入日誌的字元上限,兩段共用同一政策。 +const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500; + /** * 遮罩單行診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。 * @@ -63,11 +71,7 @@ function agentFailureDetail(res) { else if (err.code) parts.push(`code ${err.code}`); else if (err.signal) parts.push(`signal ${err.signal}`); } - // 先截去過長輸入再遮罩,避免對數 MB 的失敗輸出跑整份 O(k×n) 正規掃描; - // 2000 字上限已足以涵蓋跨界機密樣式,最終仍截為 500 字。 - const INPUT_LIMIT = 2_000; - // 每段診斷片段(stderr/stdout)寫入日誌的字元上限,兩段共用同一政策,抽為具名常數避免兩處各寫一個魔術數字。 - const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500; + // 先截去過長輸入(INPUT_LIMIT)再遮罩,最終仍截為 AGENT_DIAGNOSTIC_OUTPUT_LIMIT;長度政策常數見模組頂層。 // 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時 // 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的 // -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。 -- 2.53.0 From a8561b4257bca14b94d85268a1a61ce25c0a7b15 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 10:45:36 +0000 Subject: [PATCH 57/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index d86f869..b8466f1 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1472,5 +1472,115 @@ "endLine": 73, "problem": "新增的 `redactSecrets`/`agentFailureDetail` 會把 AI CLI 的 stderr/stdout 寫進 CI log,雖然有遮罩與限長,但沒有測試驗證常見機密格式、控制字元與長輸出邊界是否真的被處理。", "reason": "Paladin:可排除(重複)。歷史 findings 已指出 redactSecrets/agentFailureDetail 對常見憑證格式、URL 帳密、控制字元、截斷與空輸出等邊界缺少測試。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/review.js", + "startLine": 82, + "endLine": 85, + "problem": "攻擊者只要讓 AI CLI 失敗,就有機會把 CLI 的 stderr/stdout 片段寫進 CI log。這些輸出可能回顯送審 diff、prompt、環境診斷、token、JWT、雲端金鑰、私鑰片段或 PII;目前 `redactSecrets` 只是盡力遮罩,漏掉未列舉格式時,秘密會被長期保存在多人可讀的 workflow log。這違反「回應不得含 PII」的邊界,也把失敗路徑變成資料外洩通道。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 AI CLI 失敗時將 stderr/stdout 片段寫入 CI log、黑名單式遮罩不足,以及機密或 PII 外洩風險提出相同問題。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "警告", + "file": "action.yml", + "startLine": 17, + "endLine": 23, + "problem": "這裡鼓勵呼叫端傳入「能觸發 CI 的 PAT」作為 action token。攻擊者若能提交 PR 並讓 workflow 在不可信程式碼上執行,就會盯上這個高權限 PAT:任何後續工具、腳本、AI CLI 或被 PR 影響的輸出路徑只要有一處外洩,就能拿到可 push、可留言、可觸發 CI 的長效憑證。自動 token 不重觸發 CI 是防遞迴與降權邊界,直接建議 PAT 等於要求使用者拆掉這道邊界。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出 action.yml 鼓勵使用可觸發 CI 的 PAT,會在不可信 PR 或受 PR 影響流程中擴大高權限憑證外洩與濫用風險。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 397, + "problem": "`main()` 這次把留言路由、issue 暫存與建立、標籤挑選、PR 相依設定、舊留言清理、嚴重/非嚴重發布策略全部揉進同一個流程函式。六個月後要調整其中一個落地模式時,維護者必須同時理解 `ctx.createIssue`、`issueBuffer`、`issue`、`currentRunCommentIds` 與步驟順序的隱含關係,很容易在新增一個留言點時漏掉「一般模式要記 id、建問題模式要暫存或發 issue」這類規則。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 生命週期、緩衝佇列、發布策略與審查編排耦合,並提出抽離 publisher 類邊界的相同方向。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 21, + "endLine": 78, + "problem": "`redactSecrets()` / `agentFailureDetail()` 新增了不少診斷輸出政策與遮罩規則,但目前是未匯出的私有函式。這段邏輯牽涉多種 token 格式、URL 帳密、控制字元與長度截斷,未來一改 regex 就可能讓 CI log 變得難除錯或遮罩失效;若只能透過攻擊方/防守方整段流程間接驗證,測試成本會很高。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecrets/agentFailureDetail 缺少直接測試、遮罩與截斷邊界難以驗證,以及診斷/遮罩職責放在 review.js 造成模組邊界不清的問題。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 184, + "endLine": 406, + "problem": "建問題模式這次新增了多個未驗證的流程分支:留言先暫存到 issueBuffer、確定 kept.length > 0 才建 issue、無保留問題/無可審查變更時靜默通過、嚴重問題才建立 PR 對 issue 的 dependency。這些都是會影響 PR 留言、issue 建立與合併阻擋的核心行為,但 diff 沒看到對應測試;目前如果某個分支漏發、誤發到 PR,或 issue 為 null 時仍呼叫 postSevereToIssue,都不會被試煉攔下來。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、kept 為空靜默通過、有問題才建 issue、嚴重與非嚴重分流、dependency 與 PR 回貼等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 102, + "endLine": 155, + "problem": "resolveMergeBase 新增了淺層 checkout 的多段 fetch fallback、每次成功 fetch 後重試 merge-base、以及最終診斷錯誤,但沒有看到測試覆蓋這些邊界與失敗路徑。這段行為依賴 git 指令順序;只要 deepen base 成功後沒有立刻回傳、HEAD 補抓用錯 ref、或全部失敗時沒有保留診斷,PR diff 基準就可能錯或完全中斷,現在沒有測試能驗證。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 的多階段 fetch、shallow/非 shallow 分支、成功停止條件、降級策略與最終診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 270, + "endLine": 330, + "problem": "pushWithCredential 改成一律用 GIT_CONFIG_* 注入 Basic extraheader,並刻意清掉 checkout 持久化的自動 token;這是高風險失敗路徑與防洩漏行為,但 diff 沒有測試確認 token 不會出現在 argv、失敗錯誤不含遠端 URL/token、以及 extraheader 的兩筆設定順序正確。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 commitAndPushFindings/pushWithCredential 的認證推送策略、GIT_CONFIG 注入、token 遮蔽與失敗路徑缺少測試。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 13, + "endLine": 75, + "problem": "agentFailureDetail/redactSecrets 新增了把 AI CLI stderr/stdout 寫入 CI log 的行為,且依賴遮罩、控制字元清理與長度截斷來避免外洩;但沒有看到測試驗證 token、Authorization、URL 內嵌帳密、長字串與換行控制字元都會被處理。這種失敗診斷若測試只看快樂路徑,很容易在某種輸出格式下把秘密寫進 log。", + "reason": "Paladin:可排除(重複)。歷史 findings 已逐項記錄 agentFailureDetail/redactSecrets 對 Authorization、token、URL 帳密、控制字元、截斷與 debug 開關缺少測試。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Maya", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 168, + "endLine": 194, + "problem": "addIssueDependency 新增了 Gitea endpoint 包裝,但沒有測試確認路徑與 body 的語義:URL 上的 issueNumber 是被阻擋的 PR/issue,body.index 才是 dependency。這個方向如果寫反,流程仍可能收到 2xx 或難以從單次手動測試看出錯誤。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出 addIssueDependency 缺少 endpoint、HTTP method、payload 與相依方向測試,與本條指控相同。" + }, + { + "addedAt": "2026/07/20 18:45:35", + "prNumber": 6, + "reviewer": "Rogue", + "severity": "警告", + "file": "src/index.js", + "startLine": 365, + "endLine": 370, + "problem": "建問題模式下把 `others` 交給 `review.postOthersToIssue` 逐條發 issue 留言,這是在拿遠端 API latency 燒時間。警告/建議通常可能比嚴重問題多很多,若有 50 條、每次 Gitea API 往返 200ms,光留言就可能多花 10 秒以上,還會增加 rate limit 壓力。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出建問題模式將警告/建議逐條發成 issue 留言,會造成 O(n) 遠端 POST、增加 CI 時間與 rate limit 壓力。" } ] -- 2.53.0 From 6a26984bee2b5376787ee13143721b10fc138428 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 13:46:39 +0800 Subject: [PATCH 58/79] =?UTF-8?q?fix(ai-review):=20=E5=BC=B7=E5=8C=96?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E9=98=B2=E8=AD=B7=E8=88=87=E5=BB=BA=E5=95=8F?= =?UTF-8?q?=E9=A1=8C=E7=95=99=E8=A8=80=E8=99=95=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 8 ++++---- src/lib/gitrepo.js | 46 ++++++++++++++++++++++++++++++++++++++++---- src/lib/review.js | 42 +++++++++++++++++++++------------------- src/lib/templates.js | 6 +++--- 4 files changed, 71 insertions(+), 31 deletions(-) diff --git a/src/index.js b/src/index.js index 8131148..95b9493 100644 --- a/src/index.js +++ b/src/index.js @@ -210,7 +210,7 @@ async function main() { }; /** * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立), - * 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 + * 並把 `issueBuffer` 內暫存的情境留言批次寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 * 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。 * * @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選); @@ -224,9 +224,9 @@ async function main() { labels: labelIds, }); log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`); - for (const body of issueBuffer) { - await gitea.createCommentOnIssue(ctx, issue.number, body); - } + await Promise.all( + issueBuffer.map((body) => gitea.createCommentOnIssue(ctx, issue.number, body)), + ); issueBuffer.length = 0; }; diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index 7eda1e1..a9278a3 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -4,6 +4,34 @@ const { execFileSync } = require('child_process'); // git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。 +/** + * 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。 + * + * @param {string} refName - 使用者或事件 payload 提供的分支名稱。 + * @param {string} fieldName - 錯誤訊息中的欄位名稱。 + * @returns {string} 原樣回傳通過驗證的分支名稱。 + * @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。 + * @remarks + * 使用情境:`resolveMergeBase` 的 `baseRef` 與 `commitAndPushFindings` 的 + * `headRef` 會被組進 refspec;先驗證可避免惡意 payload 影響本地 refs 路徑。 + */ +function assertSafeBranchRef(refName, fieldName) { + const value = String(refName || '').trim(); + if (!value) throw new Error(`${fieldName} 不可為空。`); + if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) { + throw new Error(`${fieldName} 不是安全的分支名稱:${value}`); + } + try { + execFileSync('git', ['check-ref-format', '--branch', value], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch { + throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`); + } + return value; +} + /** * 同步執行 git 指令並回傳原始 stdout 輸出。 * @@ -102,6 +130,7 @@ function latestCommitSubject(cwd) { * 避免把 base 分支後續演進誤算進 diff。 */ function resolveMergeBase(cwd, baseRef) { + baseRef = assertSafeBranchRef(baseRef, 'baseRef'); const remoteBase = `origin/${baseRef}`; const diagnostics = []; // 執行一個 fetch 策略並記錄成敗(只記策略名與成敗,不含 git 原始輸出,避免洩漏遠端資訊)。 @@ -251,6 +280,7 @@ function fileLastUpdatedIso(cwd, file) { * 例外把命令列(含 token)回顯到 CI log 或程序清單。 */ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, serverUrl, repository }) { + headRef = assertSafeBranchRef(headRef, 'headRef'); const current = gitTrim(cwd, 'rev-parse', 'HEAD'); if (headSha && current !== headSha) { git(cwd, 'checkout', '--detach', headSha); @@ -294,17 +324,22 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。 - * @param {string} secret - 具 push 權限的 token/PAT(作為 Basic 認證的密碼)。 + * @param {string} token - 具 push 權限的 token/PAT(作為 Basic 認證的密碼)。 * @param {string} refspec - push 的 refspec(形如 `HEAD:refs/heads/`)。 * @param {string} serverUrl - Gitea 伺服器根網址(用於定位 checkout 持久化 extraheader 的 scope)。 * @returns {void} 成功即返回;失敗拋出不含 URL/argv/token 的固定錯誤。 * @throws {Error} 推送失敗時拋出固定訊息(已隱藏遠端 URL 與認證資訊)。 * @remarks 本函式未匯出,僅供 {@link commitAndPushFindings} 使用。 */ -function pushWithCredential(cwd, remoteUrl, secret, refspec, serverUrl) { - const basic = Buffer.from(`ai-review-bot:${secret}`).toString('base64'); +function pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) { + const server = new URL(serverUrl); + const remote = new URL(remoteUrl); + if (remote.origin !== server.origin || !remote.pathname.endsWith('.git')) { + throw new Error('推送遠端 URL 與 Gitea 伺服器不相符,已停止推送。'); + } + const basic = Buffer.from(`ai-review-bot:${token}`).toString('base64'); // checkout 持久化自動 token 的 scope 為 `http./.extraheader`(結尾帶斜線)。 - const headerScope = `http.${serverUrl.replace(/\/+$/, '')}/.extraheader`; + const headerScope = `http.${server.origin}/.extraheader`; try { execFileSync('git', ['push', remoteUrl, refspec], { cwd, @@ -333,4 +368,7 @@ module.exports = { fileDiff, fileLastUpdatedIso, commitAndPushFindings, + __test: { + assertSafeBranchRef, + }, }; diff --git a/src/lib/review.js b/src/lib/review.js index cbb2c7a..a326caa 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -36,7 +36,7 @@ const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500; function redactSecrets(text) { return String(text ?? '') .replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ') - .replace(/(authorization\s*[:=]\s*)\S+/gi, '$1***') + .replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, '$1***') .replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***') .replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@') .replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***') @@ -48,10 +48,9 @@ function redactSecrets(text) { * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。 * * 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或 - * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。權衡「可除錯性」後:本函式 - * 於失敗時**預設**附上經 {@link redactSecrets} 遮罩且去除控制字元的 **stderr 與 stdout** - * 片段(各先截去過長輸入再取前 500 字)——只印 exit code 幾乎無從判斷 CLI 為何失敗, - * 且部分 CLI(如 claude-code 的 `-p` 模式)將錯誤寫到 stdout 而非 stderr。純函式、不拋例外。 + * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。因此本函式預設只輸出退出碼、 + * 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets} + * 遮罩且去除控制字元的 stderr/stdout 片段(各先截去過長輸入再取前 500 字)。 * * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res * `runAgent` 的回傳物件。 @@ -59,7 +58,7 @@ function redactSecrets(text) { * @remarks * 使用情境:{@link runAttackers}/{@link runDefenders}/{@link fillPurposes}/{@link selectLabels} * 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗; - * 需要原始輸出診斷時,於 workflow 設定 secret `ACTIONS_STEP_DEBUG=true` 再重跑。 + * 需要輸出片段輔助診斷時,於 workflow 設定 `ACTIONS_STEP_DEBUG=true` 再重跑。 * 本函式未匯出,僅供模組內部使用。 */ function agentFailureDetail(res) { @@ -71,14 +70,13 @@ function agentFailureDetail(res) { else if (err.code) parts.push(`code ${err.code}`); else if (err.signal) parts.push(`signal ${err.signal}`); } - // 先截去過長輸入(INPUT_LIMIT)再遮罩,最終仍截為 AGENT_DIAGNOSTIC_OUTPUT_LIMIT;長度政策常數見模組頂層。 - // 預設即附上「經 redactSecrets 遮罩+去控制字元+限長」的 stderr 與 stdout 片段——CLI 失敗時 - // 只印 exit code 幾乎無從除錯(見 test-claude 秒失敗案例);且部分 CLI(如 claude-code 的 - // -p 模式)會把錯誤寫到 stdout 而非 stderr,故兩者都輸出。redactSecrets 為盡力防線。 - const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT)); - if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); - const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT)); - if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); + // 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。 + if (process.env.ACTIONS_STEP_DEBUG === 'true') { + const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT)); + if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); + const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT)); + if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); + } if (parts.length === 0) { parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); } @@ -739,9 +737,9 @@ ${JSON.stringify(brief)} * 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。 */ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { - for (const finding of severe) { - await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding)); - } + await Promise.all( + severe.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))), + ); log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`); } @@ -765,9 +763,9 @@ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { * 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。 */ async function postOthersToIssue({ ctx, gitea, issueNumber, others }) { - for (const finding of others) { - await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding)); - } + await Promise.all( + others.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))), + ); log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}。`); } @@ -933,4 +931,8 @@ module.exports = { postOthersToIssue, resolveOldComments, postSevereComments, + __test: { + agentFailureDetail, + redactSecrets, + }, }; diff --git a/src/lib/templates.js b/src/lib/templates.js index f02e30b..c970629 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -334,9 +334,9 @@ ${body || '(PR 無描述)'} * @param {string} [finding.suggestedCode] - 建議寫法程式碼;有值才輸出「建議寫法」區塊。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:建問題模式下 `review.postSevereToIssue`(src/lib/review.js)把每條嚴重 finding - * 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上, - * 作為問題明細的追蹤紀錄。 + * 使用情境:建問題模式下 `review.postSevereToIssue` 與 `review.postOthersToIssue` + * 把每條 finding 以本函式產生留言內容、經 `gitea.createCommentOnIssue` + * 發布到追蹤 issue 上,作為問題明細的追蹤紀錄。 */ function issueFindingComment(finding) { const emoji = SEVERITY_EMOJI[finding.severity] || '🔵'; -- 2.53.0 From 55b349da073fe5a7bcb4b170852750750d3bcdbb Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 13:46:43 +0800 Subject: [PATCH 59/79] =?UTF-8?q?test(ai-review):=20=E8=A3=9C=E4=B8=8A?= =?UTF-8?q?=E5=AE=89=E5=85=A8=E8=88=87=20Gitea=20=E5=A5=91=E7=B4=84?= =?UTF-8?q?=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 3 +- test/gitea.test.js | 73 +++++++++++++++++++++++++++++++++++++++++++ test/gitrepo.test.js | 24 ++++++++++++++ test/review.test.js | 74 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 test/gitea.test.js create mode 100644 test/gitrepo.test.js create mode 100644 test/review.test.js diff --git a/package.json b/package.json index 72286b2..862506f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,8 @@ "description": "AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings", "main": "src/index.js", "scripts": { - "build": "ncc build src/index.js -o dist" + "build": "ncc build src/index.js -o dist", + "test": "node --test" }, "author": "Jeffery", "license": "MIT" diff --git a/test/gitea.test.js b/test/gitea.test.js new file mode 100644 index 0000000..f5f6469 --- /dev/null +++ b/test/gitea.test.js @@ -0,0 +1,73 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const gitea = require('../src/lib/gitea'); + +function withFetchStub(handler, fn) { + const originalFetch = global.fetch; + const calls = []; + global.fetch = async (url, options = {}) => { + calls.push({ url, options }); + return handler(url, options); + }; + return Promise.resolve() + .then(() => fn(calls)) + .finally(() => { + global.fetch = originalFetch; + }); +} + +function jsonResponse(data, ok = true, status = 200) { + return { + ok, + status, + async text() { + return JSON.stringify(data); + }, + }; +} + +test('addIssueDependency 使用正確 endpoint、method 與 IssueMeta body', async () => { + const ctx = { + apiBase: 'https://gitea.example.test/api/v1', + token: 'hidden', + owner: 'owner', + repo: 'repo', + }; + + await withFetchStub(() => jsonResponse({ ok: true }), async (calls) => { + await gitea.addIssueDependency(ctx, 12, 34); + + assert.equal(calls.length, 1); + assert.equal(calls[0].url, 'https://gitea.example.test/api/v1/repos/owner/repo/issues/12/dependencies'); + assert.equal(calls[0].options.method, 'POST'); + assert.equal(calls[0].options.headers.Authorization, 'token hidden'); + assert.deepEqual(JSON.parse(calls[0].options.body), { + index: 34, + owner: 'owner', + repo: 'repo', + }); + }); +}); + +test('createIssue 空 labels 不送出 labels 欄位', async () => { + const ctx = { + apiBase: 'https://gitea.example.test/api/v1', + token: 'hidden', + owner: 'owner', + repo: 'repo', + }; + + await withFetchStub(() => jsonResponse({ number: 5 }), async (calls) => { + await gitea.createIssue(ctx, { title: 'title', body: 'body', labels: [] }); + + assert.equal(calls[0].url, 'https://gitea.example.test/api/v1/repos/owner/repo/issues'); + assert.equal(calls[0].options.method, 'POST'); + assert.deepEqual(JSON.parse(calls[0].options.body), { + title: 'title', + body: 'body', + }); + }); +}); diff --git a/test/gitrepo.test.js b/test/gitrepo.test.js new file mode 100644 index 0000000..baa5895 --- /dev/null +++ b/test/gitrepo.test.js @@ -0,0 +1,24 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const gitrepo = require('../src/lib/gitrepo'); + +test('assertSafeBranchRef 接受一般分支名稱', () => { + assert.equal(gitrepo.__test.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123'); +}); + +test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => { + assert.throws( + () => gitrepo.__test.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'), + /不是安全的分支名稱/, + ); +}); + +test('resolveMergeBase 會在 git fetch 前拒絕不安全 baseRef', () => { + assert.throws( + () => gitrepo.resolveMergeBase(process.cwd(), '../../hooks/pre-push'), + /不是安全的分支名稱/, + ); +}); diff --git a/test/review.test.js b/test/review.test.js new file mode 100644 index 0000000..8d10353 --- /dev/null +++ b/test/review.test.js @@ -0,0 +1,74 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const test = require('node:test'); + +const review = require('../src/lib/review'); + +test('agentFailureDetail 預設不輸出 stderr/stdout 片段', () => { + const oldDebug = process.env.ACTIONS_STEP_DEBUG; + delete process.env.ACTIONS_STEP_DEBUG; + try { + const detail = review.__test.agentFailureDetail({ + ok: false, + error: Object.assign(new Error('boom'), { code: 1 }), + stderr: 'token=super-secret-value', + output: 'stdout with password=hidden', + }); + assert.match(detail, /exit 1/); + assert.doesNotMatch(detail, /super-secret-value|password|stdout|stderr/); + } finally { + if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG; + else process.env.ACTIONS_STEP_DEBUG = oldDebug; + } +}); + +test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => { + const oldDebug = process.env.ACTIONS_STEP_DEBUG; + process.env.ACTIONS_STEP_DEBUG = 'true'; + try { + const detail = review.__test.agentFailureDetail({ + ok: false, + error: Object.assign(new Error('boom'), { code: 2 }), + stderr: 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890', + output: 'token=abcdefghijklmnopqrstuvwxyz1234567890TOKEN', + }); + assert.match(detail, /exit 2/); + assert.match(detail, /stderr:Authorization: \*\*\*/); + assert.match(detail, /stdout:token=\*\*\*/); + assert.doesNotMatch(detail, /abcdefghijklmnopqrstuvwxyz/); + } finally { + if (oldDebug === undefined) delete process.env.ACTIONS_STEP_DEBUG; + else process.env.ACTIONS_STEP_DEBUG = oldDebug; + } +}); + +test('postOthersToIssue 批次送出 issue 留言', async () => { + const calls = []; + let active = 0; + let maxActive = 0; + const fakeGitea = { + async createCommentOnIssue(ctx, issueNumber, body) { + active += 1; + maxActive = Math.max(maxActive, active); + calls.push({ ctx, issueNumber, body }); + await new Promise((resolve) => setTimeout(resolve, 20)); + active -= 1; + return { id: calls.length }; + }, + }; + + await review.postOthersToIssue({ + ctx: { token: 'hidden' }, + gitea: fakeGitea, + issueNumber: 7, + others: [ + { severity: '警告', reviewer: 'Maya', file: 'a.js', startLine: 1, endLine: 1, problem: 'p1', suggestion: 's1' }, + { severity: '建議', reviewer: 'Bard', file: 'b.js', startLine: 2, endLine: 2, problem: 'p2', suggestion: 's2' }, + ], + }); + + assert.equal(calls.length, 2); + assert.equal(calls[0].issueNumber, 7); + assert.ok(maxActive > 1); +}); -- 2.53.0 From e0ae6f886fa40a50137d215709ef52ee4b159680 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 13:46:48 +0800 Subject: [PATCH 60/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E5=9B=9E=E5=AF=AB=E5=B7=B2=E8=99=95=E7=90=86=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../findings/2026-07-20-15:20:13.json | 39 --- .../findings/2026-07-20-15:57:46.json | 65 ----- .../findings/2026-07-20-17:37:54.json | 13 - .../findings/2026-07-20-18:00:02.json | 26 -- .../findings/2026-07-20-18:31:07.json | 252 ------------------ 5 files changed, 395 deletions(-) diff --git a/.gitea/ai-review/findings/2026-07-20-15:20:13.json b/.gitea/ai-review/findings/2026-07-20-15:20:13.json index 964b1d0..29e0728 100644 --- a/.gitea/ai-review/findings/2026-07-20-15:20:13.json +++ b/.gitea/ai-review/findings/2026-07-20-15:20:13.json @@ -8,19 +8,6 @@ "model": "(工具預設)" }, "findings": [ - { - "id": "F001", - "reviewer": "Leo", - "focus": "maintainability", - "badge": "🧰", - "severity": "警告", - "file": "src/index.js", - "startLine": 210, - "endLine": 224, - "problem": "`ensureIssueCreated` 同時建立 issue、修改外層 `issue` 狀態、逐筆清空 `issueBuffer`,但整段流程沒有可重入或冪等機制。若 issue 建立成功後,寫入其中一則暫存留言時失敗,主流程會中止;重跑後又會建立另一個 issue,留下內容不完整的孤兒 issue。這種依賴閉包可變狀態的半完成狀態,半年後要加入重試、續傳或測試失敗情境都會很痛苦。", - "suggestion": "把「建立追蹤 issue 並沖刷留言」抽成獨立、可注入 Gitea client 的服務函式,明確回傳 issue 與已寫入進度;建立前以 PR 編號或隱藏識別標記查找既有追蹤 issue,讓重跑能接續而非重複建立。至少也應保留已建立的 issue 編號並在錯誤訊息中回報,避免留下無法追蹤的半成品。", - "suggestedCode": "" - }, { "id": "F002", "reviewer": "Maya", @@ -34,19 +21,6 @@ "suggestion": "新增主流程測試並 mock Gitea、agent 與 git 操作,至少涵蓋:`kept=[]` 時不建立 issue 且不留言;僅嚴重問題;僅警告/建議;混合問題;建立 issue 或寫入暫存留言失敗;標籤查詢、AI 選標籤及補掛標籤失敗時仍回貼 issue 連結。除了呼叫次數,也應斷言 API 呼叫順序、目標 issue 編號及留言內容。", "suggestedCode": "" }, - { - "id": "F003", - "reviewer": "Maya", - "focus": "testing", - "badge": "🧪", - "severity": "警告", - "file": "src/lib/gitea.js", - "startLine": 188, - "endLine": 191, - "problem": "新增的 `addLabelsToIssue` 沒有對應測試,尚未驗證空值捷徑與實際 API 請求格式。這個函式位於新建問題流程的收尾路徑;若 endpoint、HTTP method 或 `{ labels }` payload 不符預期,追蹤 issue 將無法取得標籤,而空陣列是否真的不發出請求也未被保護。", - "suggestion": "新增單元測試,分別傳入 `undefined`、`null`、空陣列及多個 label id;斷言前三者回傳 `null` 且完全不呼叫 API,多個 id 時以 POST 呼叫正確的 owner/repo/issue endpoint 並傳送 `{ labels: [...] }`,另驗證 API 拋錯會原樣往上傳遞。", - "suggestedCode": "" - }, { "id": "F004", "reviewer": "Maya", @@ -59,19 +33,6 @@ "problem": "`resolveMergeBase` 新增多階段 fetch 與淺層 checkout 修復邏輯,但沒有看到測試驗證成功、降級與最終失敗路徑。尤其初次 merge-base 失敗後,淺層與非淺層 repository 會走不同路徑,且多個 `tryGit` 失敗會被刻意吞掉;若參數、refspec 或重試順序有誤,只會在實際 CI checkout 深度不足時才暴露。", "suggestion": "以 stub 的 git 執行器或暫存 repository 補齊案例:首次 merge-base 成功;淺層 repository 經 `--unshallow` 後成功;`--unshallow` 失敗但 `--deepen=1000` 後成功;非淺層首次失敗後重試成功;所有策略失敗時拋出含 `baseRef` 且保留原始 `cause` 的錯誤。並斷言 base/head fetch 的 refspec 與執行順序。", "suggestedCode": "" - }, - { - "id": "F005", - "reviewer": "Maya", - "focus": "testing", - "badge": "🧪", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 17, - "endLine": 45, - "problem": "新增的 agent 失敗診斷涵蓋逾時、數字或字串 exit code、signal、空輸出及 500 字截斷等多個邊界,但沒有測試鎖定輸出。這些資訊只在失敗路徑出現,正常審查不會自然覆蓋;日後修改時可能悄悄遺失真正的 CLI 錯誤內容,或破壞單行與截斷約束。", - "suggestion": "將摘要邏輯匯出供測試,或透過失敗的 `runAttackers`/`runDefenders`/`fillPurposes` 測試間接斷言 log。至少覆蓋 killed、數字 exit code、字串 code、signal、stderr 與 stdout 同時存在、超過 500 字、完全無資訊,以及 `res` 為 null/undefined 的案例。", - "suggestedCode": "" } ], "excluded": [] diff --git a/.gitea/ai-review/findings/2026-07-20-15:57:46.json b/.gitea/ai-review/findings/2026-07-20-15:57:46.json index 66f479e..119763d 100644 --- a/.gitea/ai-review/findings/2026-07-20-15:57:46.json +++ b/.gitea/ai-review/findings/2026-07-20-15:57:46.json @@ -8,32 +8,6 @@ "model": "(工具預設)" }, "findings": [ - { - "id": "F001", - "reviewer": "Assassin", - "focus": "security", - "badge": "🗡️", - "severity": "嚴重", - "file": "src/lib/review.js", - "startLine": 72, - "endLine": 81, - "problem": "開啟 ACTIONS_STEP_DEBUG=true 後,會把攻擊者可間接操控的 AI CLI stderr/stdout(經黑名單式 redactSecrets 遮罩)寫入長期保存的 CI log。短 token、JWT、含標點或空白的密碼、非典型金鑰及 PII 仍可能繞過遮罩。建議即使除錯模式也只記錄退出碼/signal/逾時/診斷 ID,若需原文則寫入受限、短期保存、需授權取得的安全 artifact。", - "suggestion": "CI log 不輸出 AI CLI 原文,即使 debug 模式;需要原文時改寫入存取受限的安全 artifact,並套用允許清單式結構化診斷與 PII/機密掃描。此為安全性與可除錯性的政策取捨:現行已刻意保留 debug-gated 遮罩輸出,是否完全移除需維護者裁示。", - "suggestedCode": "function agentFailureDetail(res) {\n const err = res && res.error;\n if (err && err.killed) return '已逾時終止';\n if (err && typeof err.code === 'number') return `exit ${err.code}`;\n if (err && err.signal) return `signal ${err.signal}`;\n return 'AI CLI 執行失敗(原始輸出已隱藏)';\n}" - }, - { - "id": "F002", - "reviewer": "Leo", - "focus": "maintainability", - "badge": "🧰", - "severity": "警告", - "file": "src/index.js", - "startLine": 176, - "endLine": 218, - "problem": "main() 內新增 issueBuffer、可變 issue、postComment 與 ensureIssueCreated 閉包,後續多處依 ctx.createIssue 分支。留言路由、issue 生命週期、標籤、相依關係與審查編排共享同一批可變狀態;未來增加發布目的地或重試策略時須同步理解並修改整個超長主流程,測試也只能透過 main() 間接覆蓋。", - "suggestion": "抽出具明確介面的發布器(如 PrReviewPublisher 與 IssueReviewPublisher),封裝留言暫存、issue 建立、沖刷、問題明細與收束關聯;main() 只呼叫一致的 publishContext/publishFindings/finalize,便於分別注入假的 Gitea client 測試兩種模式並移除散落的模式判斷。屬大範圍重構+設計取捨,需維護者確認方向。", - "suggestedCode": "" - }, { "id": "F003", "reviewer": "Maya", @@ -60,19 +34,6 @@ "suggestion": "以參數化測試覆蓋 findings 四種組合,斷言 API 呼叫順序/次數/issue number/統計;並分別讓 listLabels/selectLabels/createIssue/問題留言/PR 連結留言/addIssueDependency 拋錯,驗證哪些中止、哪些僅記警告續行;零 findings 時斷言所有寫入 API 皆不呼叫。屬測試架構決策。", "suggestedCode": "" }, - { - "id": "F005", - "reviewer": "Maya", - "focus": "testing", - "badge": "🧪", - "severity": "警告", - "file": "src/lib/gitea.js", - "startLine": 171, - "endLine": 215, - "problem": "新增的問題相依 API 包裝沒有測試驗證 endpoint、HTTP method 與 payload。特別是 addIssueDependency 容易顛倒的「PR 相依於 issue」方向未被斷言;若 index 或 body 欄位放反,合併阻擋語意會相反。(原併列的 addLabelsToIssue 已於本次移除。)", - "suggestion": "mock 底層 API,對相依關係使用不同的 PR/issue 編號,精確斷言 URL 指向 PR、body.index 指向阻擋來源 issue,並覆蓋 API 非 2xx 時錯誤原樣往上拋出的案例。屬測試架構決策。", - "suggestedCode": "" - }, { "id": "F006", "reviewer": "Maya", @@ -99,19 +60,6 @@ "suggestion": "mock git 執行器與 URL/env 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。", "suggestedCode": "" }, - { - "id": "F008", - "reviewer": "Maya", - "focus": "testing", - "badge": "🧪", - "severity": "警告", - "file": "src/lib/review.js", - "startLine": 16, - "endLine": 80, - "problem": "redactSecrets/agentFailureDetail 直接決定 CI 日誌是否洩漏內容及失敗診斷是否可用,但沒有測試覆蓋。空值、控制字元、Authorization/Bearer、URL 帳密、各種 token 樣式、截斷,以及 ACTIONS_STEP_DEBUG 大小寫與未啟用時隱藏 stdout/stderr 等邊界尚未驗證。", - "suggestion": "為這兩個純函式補單元測試(必要時受控匯出或抽獨立模組),以假憑證逐一測試遮罩規則與換行注入並斷言輸出不含原始秘密;保存還原 ACTIONS_STEP_DEBUG 驗證預設/true/混合大小寫/逾時/exit code/signal/無 error/超長輸出。屬測試架構決策。", - "suggestedCode": "" - }, { "id": "F009", "reviewer": "Bard", @@ -124,19 +72,6 @@ "problem": "push-token 的用途與退回行為在區塊註解、欄位描述、required 與 default 註解中反覆說明,且單行 description 過長,資訊雖完整但重複,日後修改語意易只改到一處。", "suggestion": "保留一段「為何需要 PAT」的必要背景,其餘讓欄位名稱、required、default 自行表意,將 description 收斂成呼叫端真正需要知道的契約。註:本專案採 doc-funcs 高密度註解慣例,是否精簡屬慣例取捨,需維護者確認。", "suggestedCode": " # 專用推送 PAT;以 PAT 推送可重新觸發 CI。留空時沿用 token。\n push-token:\n description: '推送審查結果 commit 的 PAT(留空時沿用 token)'\n required: false\n default: ''" - }, - { - "id": "F010", - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "src/index.js", - "startLine": 203, - "endLine": 226, - "problem": "ensureIssueCreated 之名帶有「已存在便沿用」的冪等語意,實際卻無條件建立新 issue 並悄悄改寫外層 issue;名稱、行為與副作用不一致,閱讀呼叫處易形成錯誤預期。", - "suggestion": "若此函式只允許呼叫一次,改用直接表達「建立並沖刷暫存留言」的名稱並回傳建立結果,由呼叫端明確指派 issue,讓資料流一眼可見。註:本項與 F002(抽出 publisher 大重構)指向同一段核心流程、維護者正審視中,宜與該重構一併處理,避免重複改動。", - "suggestedCode": "const createIssueAndFlushBuffer = async (labelIds = []) => {\n const createdIssue = await gitea.createIssue(ctx, {\n title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`,\n body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),\n labels: labelIds,\n });\n for (const body of issueBuffer) {\n await gitea.createCommentOnIssue(ctx, createdIssue.number, body);\n }\n issueBuffer.length = 0;\n return createdIssue;\n};\nissue = await createIssueAndFlushBuffer(labelIds);" } ], "excluded": [] diff --git a/.gitea/ai-review/findings/2026-07-20-17:37:54.json b/.gitea/ai-review/findings/2026-07-20-17:37:54.json index c760ca0..d413d8d 100644 --- a/.gitea/ai-review/findings/2026-07-20-17:37:54.json +++ b/.gitea/ai-review/findings/2026-07-20-17:37:54.json @@ -21,19 +21,6 @@ "suggestion": "共用函式 JSDoc 改以語意階段名稱描述、不引用易變動的數字;日誌集中定義階段名稱或由單一流程描述產生編號;README 流程圖也以語意名稱為主。屬跨檔重構+設計取捨。", "suggestedCode": "const STAGE = Object.freeze({\n TOOL_DETECTION: '偵測工具',\n DIFF_COLLECTION: '整理差異',\n ATTACK_REVIEW: '攻擊方審查',\n DEFENSE_REVIEW: '防守方裁決',\n});\nlog(STAGE.DIFF_COLLECTION, 'INF', message);" }, - { - "id": "F002", - "reviewer": "Maya", - "focus": "testing", - "badge": "🧪", - "severity": "警告", - "file": "src/lib/gitea.js", - "startLine": 172, - "endLine": 215, - "problem": "建立 issue 相依關係的 API 封裝(addIssueDependency)沒有對應測試:尚未驗證 URL 的 issue 編號與 {index, owner, repo} payload 正確、以及非 2xx 錯誤是否原樣往上傳遞。(原併提的 addLabelsToIssue 已於先前 commit 移除。)", - "suggestion": "mock 底層 API,驗證 addIssueDependency 的 URL issue 編號與 payload,加入 4xx/5xx 拋錯案例,並搭配主流程測試確認相依失敗會被降級而不阻斷審查。屬測試架構決策(專案無測試框架)。", - "suggestedCode": "" - }, { "id": "F003", "reviewer": "Leo", diff --git a/.gitea/ai-review/findings/2026-07-20-18:00:02.json b/.gitea/ai-review/findings/2026-07-20-18:00:02.json index e1cd27e..2cf7b42 100644 --- a/.gitea/ai-review/findings/2026-07-20-18:00:02.json +++ b/.gitea/ai-review/findings/2026-07-20-18:00:02.json @@ -33,32 +33,6 @@ "problem": "commitAndPushFindings 的推送行為(有無變更、認證方式、空 commit 防護、是否觸發 CI)缺少測試證明。此為本次變更的核心行為,卻沒有任何測試覆蓋。", "suggestion": "mock git 命令,斷言:無 staged diff 時回傳 false 且不執行 commit/push;有變更時以認證方式推送到正確 refspec 與分支。注意:原 finding 描述的 pushToken 對比 origin 雙軌邏輯已於重構後移除(現行一律以 token 經 pushWithCredential 認證推送),撰寫測試前需依現行程式碼重新界定情境。", "suggestedCode": "" - }, - { - "id": "F003", - "reviewer": "🗡️ Assassin", - "focus": "security", - "badge": "🗡️", - "severity": "警告", - "file": "src/lib/review.js", - "startLine": 64, - "endLine": 80, - "problem": "agentFailureDetail 於 AI CLI 失敗時會把(經 redactSecrets 盡力遮罩的)stderr/stdout 片段寫入 CI log。redactSecrets 屬盡力遮罩,無法可靠辨識 PII、短密碼或私鑰片段;長期保存且多人可讀的 CI log 有洩漏風險。", - "suggestion": "屬安全(避免洩漏)與可除錯性的設計取捨:現行程式碼已於註解明確權衡並選擇「附上遮罩後輸出以利除錯」。是否改為只記錄退出碼/訊號/逾時狀態+隨機診斷 ID(內容級診斷改寫入有存取控制與短保存期的獨立 artifact)需由維護者裁示,故保留現行行為、標為待人工處理。", - "suggestedCode": "if (verbose) {\n parts.push('已啟用除錯;為避免洩漏原始碼、PII 或憑證,CLI 輸出仍不寫入日誌');\n}" - }, - { - "id": "F004", - "reviewer": "🧪 Maya", - "focus": "testing", - "badge": "🧪", - "severity": "建議", - "file": "src/lib/gitea.js", - "startLine": 171, - "endLine": 215, - "problem": "addLabelsToIssue 與 addIssueDependency 缺少契約測試驗證 endpoint、HTTP method 與 request body;相依關係方向由 URL 與 body 決定,參數次序寫反時粗略 mock 的主流程測試不易察覺。", - "suggestion": "補 Gitea client 單元測試:labels 為空或缺少時不呼叫 API 並回傳 null;有 labels 時送出正確陣列;相依 API 以 PR 編號置於 URL、追蹤 issue 編號置於 index,並帶入正確 owner/repo。", - "suggestedCode": "" } ], "excluded": [] diff --git a/.gitea/ai-review/findings/2026-07-20-18:31:07.json b/.gitea/ai-review/findings/2026-07-20-18:31:07.json index 1584784..f7e6496 100644 --- a/.gitea/ai-review/findings/2026-07-20-18:31:07.json +++ b/.gitea/ai-review/findings/2026-07-20-18:31:07.json @@ -36,48 +36,6 @@ "suggestedCode": "```\n| 功能名稱 | 功能描述 |\n| --- | --- |\n| [gitrepo.resolveMergeBase](src/lib/gitrepo.js) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |\n```", "sourceIssue": 12 }, - { - "id": "F003", - "reviewer": "Leo", - "focus": "", - "badge": "🧰", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 13, - "endLine": 82, - "problem": "`agentFailureDetail()` 同時負責解析程序失敗、決定 debug 政策、讀取全域環境變數、截斷輸出及遮罩機密。尤其直接讀取 `process.env.ACTIONS_STEP_DEBUG` 形成隱藏相依,測試不同輸出政策時必須修改程序全域狀態;日後若其他呼叫端需要不同診斷層級,也只能繼續往這個函式堆條件。", - "suggestion": "把診斷政策改成明確參數,並將「錯誤中繼資料整理」與「輸出片段清理」拆成小函式;在 `main` 或 context 載入階段解析環境設定後注入。這能讓各種 exit code、signal、空輸出與 verbose 模式以純輸入輸出直接測試。", - "suggestedCode": "```\nfunction agentFailureDetail(res, { includeStdout = false, inputLimit = 2000, outputLimit = 500 } = {}) {\n const parts = failureMetadata(res && res.error);\n appendSanitizedOutput(parts, 'stderr', res && res.stderr, inputLimit, outputLimit);\n if (includeStdout) {\n appendSanitizedOutput(parts, 'stdout', res && res.output, inputLimit, outputLimit);\n }\n return parts.length ? parts.join('|') : 'AI CLI 執行失敗(無診斷輸出)';\n}\n```", - "sourceIssue": 12 - }, - { - "id": "F004", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "警告", - "file": "src/lib/review.js", - "startLine": 42, - "endLine": 61, - "problem": "`agentFailureDetail` 的文件自相矛盾:開頭宣稱「原始輸出預設隱藏」,後文卻明言預設附上經遮罩的 `stderr` 與 `stdout` 片段;另提到 `ACTIONS_STEP_DEBUG`,函式內卻沒有相應分支。註解與實作各唱各的調,維護者無法從文件判斷實際日誌行為。", - "suggestion": "統一文件敘述為實際行為:預設輸出經遮罩且限長的診斷片段;若目前並未依 `ACTIONS_STEP_DEBUG` 改變輸出,就移除該段說明,或待真正實作開關後再補上。", - "suggestedCode": "", - "sourceIssue": 13 - }, - { - "id": "F005", - "reviewer": "Rogue", - "focus": "", - "badge": "⚡", - "severity": "嚴重", - "file": "src/index.js", - "startLine": 216, - "endLine": 218, - "problem": "在 `ensureIssueCreated` 函式中,使用 `for...of` 迴圈搭配 `await` 來逐條對 Gitea API 發送留言請求。由於網路請求存在延遲(每次 RTT 約 100-300ms),在迴圈內阻塞式等待會導致整體執行時間隨留言數量線性增加,浪費大量 CPU 週期與網路連線資源。", - "suggestion": "可以將 `issueBuffer` 中的所有情境留言合併為單一 Markdown 留言發送,僅需一次 API 呼叫;或者使用 `Promise.all` 將這些無相依性的留言請求並行化發送,大幅降低總延遲。", - "suggestedCode": "```\nif (issueBuffer.length > 0) {\n const combinedBody = issueBuffer.join('\\n\\n---\\n\\n');\n await gitea.createCommentOnIssue(ctx, issue.number, combinedBody);\n }\n issueBuffer.length = 0;\n```", - "sourceIssue": 14 - }, { "id": "F006", "reviewer": "Rogue", @@ -106,20 +64,6 @@ "suggestedCode": "```\n} catch (err) {\n // 過濾敏感資訊後保留錯誤細節\n const safeMessage = err.message ? redactSecrets(err.message) : '未知錯誤';\n const error = new Error(`推送審查結果 commit 失敗(${safeMessage})。`);\n error.cause = err;\n throw error;\n }\n```", "sourceIssue": 14 }, - { - "id": "F008", - "reviewer": "Assassin", - "focus": "", - "badge": "🗡️", - "severity": "警告", - "file": "src/lib/gitrepo.js", - "startLine": 299, - "endLine": 299, - "problem": "在 `pushWithCredential` 中,環境變數 `GIT_CONFIG_KEY_0` 被動態拼接為 `http.${remoteUrl}.extraheader`。如果 `remoteUrl` 來自外部或未經嚴格驗證的 context,且 URL 中包含特殊字元(如點號、路徑分隔符號或引號等),可能會導致 Git 配置解析錯誤,或在特定平台下引發 Git 配置參數注入風險。", - "suggestion": "由於該執行程序僅為一次性的 `git push` 操作,可直接將該憑證應用於所有 HTTP 請求,將 `GIT_CONFIG_KEY_0` 設定為靜態的 `http.extraheader`,以避免動態拼接 URL 所帶來的注入風險。", - "suggestedCode": "```\nGIT_CONFIG_KEY_0: 'http.extraheader',\n```", - "sourceIssue": 14 - }, { "id": "F009", "reviewer": "Bard", @@ -148,20 +92,6 @@ "suggestedCode": "```\nconst commentPromise = gitea.createIssueComment(\n ctx,\n templates.issueLinkComment({\n issueNumber: issue.number,\n issueUrl: issue.html_url,\n severeCount: severe.length,\n otherCount: others.length,\n })\n );\n const dependencyPromise = gitea.addIssueDependency(ctx, ctx.prNumber, issue.number)\n .then(() => log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`))\n .catch((err) => log('建問題', 'WRN', `設定 PR 相依失敗:${err.message}。`));\n \n await Promise.all([commentPromise, dependencyPromise]);\n```", "sourceIssue": 14 }, - { - "id": "F011", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 33, - "endLine": 34, - "problem": "在 redactSecrets 函式中,針對 authorization 以及其他憑證關鍵字(如 token、secret、password 等)的敏感資訊遮蔽,分別使用了兩條結構極為相似的正規表示式進行替換。這造成了重複的替換邏輯與額外的處理開銷,程式碼的旋律顯得不夠俐落。", - "suggestion": "建議將這兩條正規表示式合併為單一表達式,消除重複的 replace 呼叫,使程式碼更加簡潔優雅且提升運行效率。", - "suggestedCode": "```\n.replace(/((?:authorization|api[_-]?key|token|password|secret|bearer)\\s*[:=]\\s*)\\S+/gi, '$1***')\n```", - "sourceIssue": 14 - }, { "id": "F012", "reviewer": "Rogue", @@ -176,20 +106,6 @@ "suggestedCode": "```\nconst stderr = redactSecrets(String((res && res.stderr) || '').slice(0, 500));\n if (stderr) parts.push(`stderr:${stderr}`);\n const stdout = redactSecrets(String((res && res.output) || '').slice(0, 500));\n if (stdout) parts.push(`stdout:${stdout}`);\n```", "sourceIssue": 14 }, - { - "id": "F013", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/templates.js", - "startLine": 335, - "endLine": 337, - "problem": "issueFindingComment 函式的 @remarks 文件註解中,說明其使用情境為『建問題模式下 review.postSevereToIssue 把每條嚴重 finding... 作為問題明細的追蹤紀錄』。然而實際上,非嚴重的警告與建議(others)也會透過 review.postOthersToIssue 呼叫此模板進行發布,導致文件描述不夠完整。", - "suggestion": "修正 @remarks 的使用情境說明,將 postOthersToIssue 亦併入描述中,使 JSDoc 文件能如實且精準地反映實際程式碼的呼叫情境。", - "suggestedCode": "```\n* 使用情境:建問題模式下 `review.postSevereToIssue` 與 `review.postOthersToIssue`(src/lib/review.js)把保留的各級問題明細\\n * 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上,\\n * 作為問題明細的追蹤紀錄。\n```", - "sourceIssue": 14 - }, { "id": "F014", "reviewer": "Leo", @@ -218,48 +134,6 @@ "suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", "sourceIssue": 15 }, - { - "id": "F016", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 28, - "endLine": 37, - "problem": "這段註解的旋律前後打架:開頭說「原始輸出預設隱藏」,後文卻說失敗時預設附上遮罩後的 stderr/stdout 片段。讀者才剛建立心智模型,下一拍就被改調。", - "suggestion": "請讓摘要句與實際行為一致,明確說明「原始輸出不直接輸出,但會輸出遮罩與截斷後的診斷片段」。", - "suggestedCode": "```\n* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,並附上遮罩、去控制字元且截斷後的 stderr/stdout 片段。\n```", - "sourceIssue": 15 - }, - { - "id": "F017", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/templates.js", - "startLine": 334, - "endLine": 341, - "problem": "`issueFindingComment` 的註解只提到 `review.postSevereToIssue`,但主流程也以 `postOthersToIssue` 發布警告與建議。文件把共用模板寫成嚴重問題專用,讀起來像少了一個聲部。", - "suggestion": "把 remarks 改成同時涵蓋嚴重、警告與建議的 issue 留言產生器,避免維護者誤以為它只服務嚴重 finding。", - "suggestedCode": "", - "sourceIssue": 15 - }, - { - "id": "F018", - "reviewer": "Assassin", - "focus": "", - "badge": "🗡️", - "severity": "嚴重", - "file": "src/lib/gitrepo.js", - "startLine": 102, - "endLine": 149, - "problem": "攻擊者可以透過提交惡意 Pull Request,將 `baseRef`(PR 目標分支)命名為包含路徑穿越(Path Traversal)的字串,例如 `../../hooks/pre-push`。由於 `resolveMergeBase` 直接將 `baseRef` 拼接至 `git fetch` 的 Refspec 參數中(例如 `+refs/heads/${baseRef}:refs/remotes/origin/${baseRef}`),這將導致 `git fetch` 寫入至 `.git/refs/remotes/origin/../../hooks/pre-push`(即 `.git/hooks/pre-push`)。這會覆寫或建立 Git Hook,並在後續執行 git 操作時自動觸發該惡意 Hook,從而造成遠端程式碼執行(RCE)。同樣地,`commitAndPushFindings` 函數中的 `headRef` 也存在類似的拼接風險。", - "suggestion": "在將 `baseRef` 與 `headRef` 傳入 git 指令之前,應進行嚴格的合法性檢查。建議使用正則表達式限制分支名稱僅能包含安全的字元(如英數字、斜線、底線、連字號、句點),且絕對不得含有 `..` 或以 `-` 開頭,必要時亦可使用 `git check-ref-format` 命令先行驗證該分支名稱是否安全。", - "suggestedCode": "```\nfunction resolveMergeBase(cwd, baseRef) {\n // 嚴格的分支名稱白名單檢查,防止路徑穿越與參數注入\n const safeBranchRegex = /^(?!-)(?!.*?\\.\\.)[a-zA-Z0-9/_.-]+$/;\n if (!safeBranchRegex.test(baseRef)) {\n throw new Error(`偵測到不合法的分支名稱: ${baseRef}`);\n }\n const remoteBase = `origin/${baseRef}`;\n```", - "sourceIssue": 16 - }, { "id": "F019", "reviewer": "Leo", @@ -274,34 +148,6 @@ "suggestedCode": "", "sourceIssue": 17 }, - { - "id": "F020", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 41, - "endLine": 45, - "problem": "這段註解的旋律前後走調:摘要先說「原始輸出預設隱藏」,下一段卻說失敗時「預設附上 stderr 與 stdout」。讀者還沒進函式本體,文件本身就已經互相拉扯。", - "suggestion": "請讓摘要與實作同拍,直接說明會輸出經遮罩與限長的診斷片段;若真的要隱藏原始輸出,也應同步改實作。", - "suggestedCode": "```\n* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,並附上經遮罩與限長的 stderr/stdout 片段。\n```", - "sourceIssue": 17 - }, - { - "id": "F021", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/templates.js", - "startLine": 334, - "endLine": 338, - "problem": "`issueFindingComment` 的文件只唱「嚴重 finding」,但新版流程也讓警告與建議逐條發到 issue。函式名稱是通用的,註解卻把用途寫窄,後續讀者會誤以為它只服務嚴重問題。", - "suggestion": "把註解改成涵蓋所有 finding 等級,讓文件與函式名稱、呼叫情境保持一致。", - "suggestedCode": "```\n* 使用情境:建問題模式下,`review.postSevereToIssue` 與 `review.postOthersToIssue`\n * 會把各等級 finding 以本函式產生留言內容、經 `gitea.createCommentOnIssue`\n * 發布到追蹤 issue 上,作為問題明細的追蹤紀錄。\n```", - "sourceIssue": 17 - }, { "id": "F022", "reviewer": "Leo", @@ -414,20 +260,6 @@ "suggestedCode": "```\nfunction prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {\n return `${MARK}\n## 🔍 AI Code Review|已建立追蹤問題\n\n本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`;\n}\n```", "sourceIssue": 19 }, - { - "id": "F030", - "reviewer": "Rogue", - "focus": "", - "badge": "⚡", - "severity": "警告", - "file": "src/index.js", - "startLine": 369, - "endLine": 370, - "problem": "建問題模式把所有警告/建議改成逐條發 issue 留言,這裡會把 `others.length` 放大成 N 次遠端 POST;正常模式同一批資料只產生 1 則彙整表格留言。只要 AI 回出數十條警告,CI 時間就會被 API round-trip 線性吃掉,還更容易撞上 Gitea rate limit 或暫時性網路延遲。", - "suggestion": "警告/建議維持批次彙整成單一留言;只有嚴重問題需要逐條追蹤時再拆開。若產品需求一定要逐條回覆,至少在 `postOthersToIssue` 內用有上限的並行池,不要一筆等一筆。", - "suggestedCode": "```\nif (others.length > 0) {\n if (ctx.createIssue) {\n await gitea.createCommentOnIssue(ctx, issue.number, templates.othersComment(others));\n log('步驟10', 'INF', `警告+建議表格留言已發布到 issue(${others.length} 條)。`);\n } else {\n await postComment(templates.othersComment(others));\n log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);\n }\n}\n```", - "sourceIssue": 20 - }, { "id": "F031", "reviewer": "Leo", @@ -470,34 +302,6 @@ "suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", "sourceIssue": 21 }, - { - "id": "F034", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 36, - "endLine": 43, - "problem": "`agentFailureDetail` 的 JSDoc 先說「原始輸出預設隱藏」,下一段又說「預設附上 stderr 與 stdout 片段」。同一段說明前後轉調,讀者會搞不清楚失敗診斷到底會不會輸出 CLI 內容。", - "suggestion": "請統一描述:若設計是輸出已遮罩片段,就刪掉「預設隱藏」;若設計是隱藏原始輸出,就把後段改成條件式說明。", - "suggestedCode": "", - "sourceIssue": 21 - }, - { - "id": "F035", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/templates.js", - "startLine": 334, - "endLine": 338, - "problem": "`issueFindingComment` 是通用 finding 留言模板,但更新後的說明只寫 `review.postSevereToIssue` 與「嚴重 finding」。然而主流程也將警告/建議逐條發到 issue,這段文件把模板唱窄了,和實際用途不一致。", - "suggestion": "把 remarks 改成涵蓋嚴重、警告與建議的通用 issue finding 留言,避免日後維護者誤以為此模板只能用於嚴重問題。", - "suggestedCode": "", - "sourceIssue": 21 - }, { "id": "F036", "reviewer": "Rogue", @@ -512,48 +316,6 @@ "suggestedCode": "```\nconst { kept, excluded } = findings.length === 0\n ? { kept: [], excluded: [] }\n : await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });\n```", "sourceIssue": 22 }, - { - "id": "F037", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/gitrepo.js", - "startLine": 310, - "endLine": 310, - "problem": "`pushWithCredential` 的參數名叫 `secret`,但同一檔其他區段與呼叫端都稱它為 `token`;同一個旋律忽然換調,讀者需要多花心力確認這是不是另一種憑證。", - "suggestion": "沿用既有命名,把 `secret` 改成 `token`,並同步調整 JSDoc 與 `Buffer.from` 內的引用。", - "suggestedCode": "```\nfunction pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) {\n const basic = Buffer.from(`ai-review-bot:${token}`).toString('base64');\n // ...\n}\n```", - "sourceIssue": 22 - }, - { - "id": "F038", - "reviewer": "Leo", - "focus": "", - "badge": "🧰", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 31, - "endLine": 46, - "problem": "`agentFailureDetail` 的註解前後語意不一致:開頭寫「原始輸出預設隱藏」,但後段又說預設會附上遮罩後的 stderr/stdout;最後還提到用 `ACTIONS_STEP_DEBUG=true` 取得原始輸出,但程式碼沒有任何 debug flag 分支。這種文件與實作脫節,會讓未來維護者誤判 CI log 會暴露多少診斷內容。", - "suggestion": "把註解改成符合目前實作:預設輸出限長且遮罩後的 stderr/stdout;若要支援 debug 模式,再補實作分支。不要在註解承諾程式沒有做的行為。", - "suggestedCode": "", - "sourceIssue": 22 - }, - { - "id": "F039", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/templates.js", - "startLine": 337, - "endLine": 340, - "problem": "`issueFindingComment` 是一般 finding 留言模板,但註解只寫 `review.postSevereToIssue` 的嚴重問題用途;和主流程中「警告+建議也逐條發到 issue」的描述不一致,註解像只唱了半段副歌。", - "suggestion": "把 remarks 改成涵蓋嚴重、警告與建議的共用用途,避免後續維護者誤以為此模板只服務嚴重問題。", - "suggestedCode": "", - "sourceIssue": 22 - }, { "id": "F040", "reviewer": "Assassin", @@ -581,20 +343,6 @@ "suggestion": "改用 repo 相對連結、不固定行號,或把這段功能表改由腳本從 JSDoc 自動產生。若需要連到特定實作,優先連到檔案或錨點,避免每次重排程式碼都要同步更新幾十個行號。", "suggestedCode": "```\n| log.taipeiNow | [src/lib/log.js](src/lib/log.js) | 取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串 |\n| review.runAttackers | [src/lib/review.js](src/lib/review.js) | 攻擊方 sub agent 並行找問題並合併列表 |\n```", "sourceIssue": 23 - }, - { - "id": "F042", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 31, - "endLine": 40, - "problem": "這段註解的旋律前後失和:開頭寫「原始輸出預設隱藏」,下一句卻說預設附上 stderr 與 stdout 片段。讀者尚未進入程式碼,就已被兩個互相拉扯的描述絆住。", - "suggestion": "請讓文件只唱一個調性:若目前設計是預設輸出遮罩後的診斷片段,就刪掉「原始輸出預設隱藏」或改成「原始輸出會先遮罩與截斷」。", - "suggestedCode": "", - "sourceIssue": 23 } ], "excluded": [] -- 2.53.0 From 24b248884fb17e313c9b78f8142aaddfcce86ff5 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Tue, 21 Jul 2026 05:50:00 +0000 Subject: [PATCH 61/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 110 +++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index b8466f1..b9bc1f1 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1582,5 +1582,115 @@ "endLine": 370, "problem": "建問題模式下把 `others` 交給 `review.postOthersToIssue` 逐條發 issue 留言,這是在拿遠端 API latency 燒時間。警告/建議通常可能比嚴重問題多很多,若有 50 條、每次 Gitea API 往返 200ms,光留言就可能多花 10 秒以上,還會增加 rate limit 壓力。", "reason": "Paladin:可排除(重複)。歷史 finding 已指出建問題模式將警告/建議逐條發成 issue 留言,會造成 O(n) 遠端 POST、增加 CI 時間與 rate limit 壓力。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "警告", + "file": "action.yml", + "startLine": 19, + "endLine": 24, + "problem": "文件鼓勵呼叫端傳入「能觸發 CI 的 PAT」。這把原本自動 token 的防遞迴與範圍限制換成長效、較高權限的秘密。若 workflow 會在不受信任的 PR 程式碼或可被 PR 修改的 action 版本上執行,攻擊者可以在 CI 中讀取或外送 PAT,接著用它 push commit、改 issue、或觸發更多 workflow。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 action.yml 建議使用可觸發 CI 的 PAT 所帶來的長效高權限憑證風險提出相同問題。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "action.yml", + "startLine": 2, + "endLine": 3, + "problem": "檔頭 `更新時間` 仍寫著 `2026/07/17 18:49:58`,但本次變更描述顯示此檔最後更新為 `2026/07/20 17:24:18`。文件時間戳若成了舊拍子,讀者會開始懷疑整份 manifest 的註解是否也同樣過期。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 action.yml 檔頭手動更新時間容易過期且與實際內容不同。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 7, + "endLine": 7, + "problem": "啟動 banner 的 `更新時間` 被改成 `2026/07/17 18:49:58`,但這次主流程明顯新增了建 issue、延後解決舊留言等大量段落。這個時間戳像留在舊譜上的小節號,和目前程式內容不再合拍。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 src/index.js 啟動 banner 的手動更新時間與程式實際變更不同,屬同一問題。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 390, + "problem": "`main()` 這次把建問題模式的暫存留言、issue 建立、標籤挑選、PR 回貼、相依設定、一般模式舊留言清理全部塞進同一個流程函式。半年後要改任何一個輸出通道時,維護者必須重新推演 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds` 與步驟 2 延後執行之間的狀態關係,這會讓主流程變成難以安全修改的編排巨石。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。main() 內建問題模式狀態、留言路由、issue 建立與發布職責耦合,已由既有排除事項與歷史 findings 涵蓋。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 199, + "endLine": 232, + "problem": "`queueOrPostComment` 透過閉包同時讀寫 `ctx.createIssue`、`issue`、`issueBuffer`、`currentRunCommentIds`,回傳型別也會依狀態變成 `Object|null`。這種隱性狀態機短期能跑,但未來要追「這則留言到底會發到 PR、issue,還是只進 buffer」時,必須靠讀整個 `main()` 的執行順序才能理解。", + "reason": "Paladin:可排除(重複)。queueOrPostComment 的閉包狀態與留言目的地隱性分流,與既有 main() 留言路由、issue 狀態與緩衝佇列耦合問題相同。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "readme.md", + "startLine": 76, + "endLine": 131, + "problem": "README 的功能列表繼續維護大量硬編碼到 `develop` 分支與精確行號的連結。這次 diff 已經為了行號漂移更新一整片表格;之後任何新增註解、移動函式或插入測試都會讓文件連結過期,維護成本會隨每次重構累積。", + "reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 README 大量硬編 develop 分支與精確行號連結,會隨程式碼移動失準。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 409, + "endLine": 418, + "problem": "建問題模式下若有嚴重問題但 `exclusions.json` 沒變更,`filesToCommit` 會是空陣列,流程會走到「略過 commit/push」後直接 `return 0`。最小重現:`create-issue=true`、攻擊方保留 1 條 `severity: '嚴重'`、防守方沒有排除任何 finding,因此 `exclusionsChanged=false`。此時本輪檢查成功,且沒有推出 `[ai-review-bot][failure]` commit 觸發下一輪步驟 1 回報失敗;若 issue dependency API 未啟用或設定失敗,嚴重問題不會阻擋合併。", + "reason": "Paladin:可排除(重複)。與 F001 指涉同一個建問題模式下嚴重 finding 可能未產生可阻擋失敗訊號、最後回傳成功的問題。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 323, + "endLine": 390, + "problem": "建問題模式的核心流程新增了多個可觀察行為,但目前測試沒有驗證:有保留問題才建立 issue、無保留問題靜默通過、暫存的工具/diff/角色留言會 flush 到 issue、PR 只回貼 issue 連結、且只有嚴重問題才呼叫 `addIssueDependency`。這些都是本次 PR 新增/改動的主要行為,沒有試煉過就等於還沒完成。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式有問題才建 issue、無問題靜默通過、暫存留言 flush、PR 回貼與嚴重問題相依設定等核心分支缺少測試。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 130, + "endLine": 190, + "problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每步後重試 merge-base、淺層 repo 才 `--unshallow`、以及失敗診斷彙整,但新增測試只驗證不安全 `baseRef` 會被拒絕,沒有覆蓋這些成功與失敗路徑。這裡最怕的是淺層 checkout 或 PR head 歷史不足時,實際 CI 才發現 fetch 順序或 refspec 錯了。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 的多階段 fetch、淺層/非淺層分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/21 13:49:59", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 303, + "endLine": 358, + "problem": "推送結果 commit 的認證方式改成 `pushWithCredential`:不走 origin、自訂 `GIT_CONFIG_*` extraheader、先清掉 checkout token、URL origin 必須相符、失敗時隱藏遠端與 token。這些安全與 CI 觸發相關行為目前完全沒有測試;現有 `gitrepo.test.js` 只測分支名稱驗證。", + "reason": "Paladin:可排除(重複)。歷史 findings 已記錄 commitAndPushFindings/pushWithCredential 的 token 認證推送路徑、推送目標、錯誤遮蔽與安全行為缺少測試。" } ] -- 2.53.0 From 555611db074eb75a7ad633b7a55d991c29df2203 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 14:24:04 +0800 Subject: [PATCH 62/79] =?UTF-8?q?fix(ai-review):=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=BB=BA=E5=95=8F=E9=A1=8C=E6=A8=A1=E5=BC=8F=E5=9A=B4=E9=87=8D?= =?UTF-8?q?=E7=B5=90=E6=9E=9C=20fail-open?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 25 ++++++----- src/lib/diagnostics.js | 71 ++++++++++++++++++++++++++++++ src/lib/review.js | 98 +++++++++++------------------------------- 3 files changed, 110 insertions(+), 84 deletions(-) create mode 100644 src/lib/diagnostics.js diff --git a/src/index.js b/src/index.js index 95b9493..4e5cafd 100644 --- a/src/index.js +++ b/src/index.js @@ -210,7 +210,7 @@ async function main() { }; /** * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立), - * 並把 `issueBuffer` 內暫存的情境留言批次寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 + * 並把 `issueBuffer` 內暫存的情境留言依流程順序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 * 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。 * * @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選); @@ -224,9 +224,9 @@ async function main() { labels: labelIds, }); log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`); - await Promise.all( - issueBuffer.map((body) => gitea.createCommentOnIssue(ctx, issue.number, body)), - ); + for (const body of issueBuffer) { + await gitea.createCommentOnIssue(ctx, issue.number, body); + } issueBuffer.length = 0; }; @@ -302,7 +302,9 @@ async function main() { await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); // ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── - const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); + const { kept, excluded } = findings.length === 0 + ? { kept: [], excluded: [] } + : await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); review.sortFindings(kept); const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded }); @@ -398,12 +400,15 @@ async function main() { } // ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)─────── - // 一般模式:findings+exclusions.json;建問題模式:問題明細已在 issue 留言,只 commit exclusions.json。 + // 一般模式:findings+exclusions.json;建問題模式通常只 commit exclusions.json。 + // 若有嚴重問題,仍 commit findings 檔產生 [failure] 結果 commit,避免相依 API 不支援時 fail-open。 const result = severe.length === 0 ? 'success' : 'failure'; - const filesToCommit = ctx.createIssue ? [] : [relativePath]; - if (exclusionsChanged) { - filesToCommit.push(path.join('.gitea', 'ai-review', 'exclusions.json')); - } + const filesToCommit = review.resultFilesToCommit({ + createIssue: ctx.createIssue, + severeCount: severe.length, + relativePath, + exclusionsChanged, + }); if (filesToCommit.length > 0) { commitFindings({ cwd, ctx, files: filesToCommit, result }); } else { diff --git a/src/lib/diagnostics.js b/src/lib/diagnostics.js new file mode 100644 index 0000000..011cd28 --- /dev/null +++ b/src/lib/diagnostics.js @@ -0,0 +1,71 @@ +'use strict'; + +// AI CLI 失敗診斷與機密遮罩工具:供 review 流程記錄安全、限長的一行錯誤摘要。 + +// 遮罩前先截去的輸入上限,避免對數 MB 失敗輸出跑整份 O(k*n) 正規掃描。 +const AGENT_DIAGNOSTIC_INPUT_LIMIT = 2_000; +// 每段診斷片段(stderr/stdout)寫入日誌的字元上限。 +const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500; + +/** + * 遮罩診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。 + * + * 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽 + * `Authorization` 標頭、`token=`/`token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/ + * 長 hex/`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出 + * CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。 + * + * @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。 + * @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。 + */ +function redactSecrets(text) { + return String(text ?? '') + .replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ') + .replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, '$1***') + .replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***') + .replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@') + .replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***') + .replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***') + .trim(); +} + +/** + * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。 + * + * 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或 + * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。因此本函式預設只輸出退出碼、 + * 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets} + * 遮罩且去除控制字元的 stderr/stdout 片段。 + * + * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res + * `runAgent` 的回傳物件。 + * @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。 + */ +function agentFailureDetail(res) { + const parts = []; + const err = res && res.error; + if (err) { + if (err.killed) parts.push('已逾時終止'); + if (typeof err.code === 'number') parts.push(`exit ${err.code}`); + else if (err.code) parts.push(`code ${err.code}`); + else if (err.signal) parts.push(`signal ${err.signal}`); + } + // 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。 + if (process.env.ACTIONS_STEP_DEBUG === 'true') { + const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); + if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); + const stdout = redactSecrets(String((res && res.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); + if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); + } + if (parts.length === 0) { + parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); + } + return parts.join('|'); +} + +module.exports = { + AGENT_DIAGNOSTIC_INPUT_LIMIT, + AGENT_DIAGNOSTIC_OUTPUT_LIMIT, + redactSecrets, + agentFailureDetail, +}; diff --git a/src/lib/review.js b/src/lib/review.js index a326caa..db528f6 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -5,6 +5,7 @@ const path = require('path'); const { log, taipeiFromIso, taipeiNow } = require('./log'); const { runAgent, extractJson } = require('./agents'); +const { agentFailureDetail } = require('./diagnostics'); const templates = require('./templates'); // 審查流程核心:.reviewignore 過濾、diff 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。 @@ -13,76 +14,6 @@ const templates = require('./templates'); const PER_FILE_DIFF_LIMIT = 16_000; const TOTAL_DIFF_LIMIT = 160_000; -// AI CLI 失敗診斷輸出政策常數(agentFailureDetail 使用):與上方送審上限並列於模組頂層, -// 集中管理長度政策,避免藏在函式中段。 -// INPUT_LIMIT:遮罩前先截去的輸入上限——先截再跑 redactSecrets,避免對數 MB 失敗輸出跑整份 -// O(k×n) 正規掃描;2000 字已足以涵蓋跨界機密樣式。 -const INPUT_LIMIT = 2_000; -// AGENT_DIAGNOSTIC_OUTPUT_LIMIT:每段診斷片段(stderr/stdout)寫入日誌的字元上限,兩段共用同一政策。 -const AGENT_DIAGNOSTIC_OUTPUT_LIMIT = 500; - -/** - * 遮罩單行診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。 - * - * 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽 - * `Authorization` 標頭、`token=`/`token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/ - * 長 hex/`ghp_` 等 token 樣式。屬「盡力遮罩」——無法窮舉所有機密格式,作為輸出 - * CLI 診斷片段前的防線使用(見 {@link agentFailureDetail})。 - * - * @param {*} text - 待遮罩的原始文字(非字串會先以 `String()` 轉型)。 - * @returns {string} 已去控制字元並遮蔽常見機密樣式的單行文字。 - * @remarks 本函式未匯出,僅供模組內部使用。 - */ -function redactSecrets(text) { - return String(text ?? '') - .replace(/[\r\n\t\v\f\x00-\x1f\x7f]+/g, ' ') - .replace(/(authorization\s*[:=]\s*)(?:bearer\s+)?\S+/gi, '$1***') - .replace(/((?:api[_-]?key|token|password|secret|bearer)\s*[:=]\s*)\S+/gi, '$1***') - .replace(/(https?:\/\/)[^\s/:@]+:[^\s/@]+@/gi, '$1***:***@') - .replace(/\bgh[pousr]_[A-Za-z0-9]{16,}\b/g, '***') - .replace(/\b[A-Za-z0-9_-]{40,}\b/g, '***') - .trim(); -} - -/** - * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。 - * - * 安全考量:AI CLI 失敗時可能在 stderr/stdout 回顯提示內容、環境資訊、token、PII 或 - * 原始碼祕密,這些會被長期保存並供多人讀取的 CI log 收錄。因此本函式預設只輸出退出碼、 - * 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets} - * 遮罩且去除控制字元的 stderr/stdout 片段(各先截去過長輸入再取前 500 字)。 - * - * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res - * `runAgent` 的回傳物件。 - * @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。 - * @remarks - * 使用情境:{@link runAttackers}/{@link runDefenders}/{@link fillPurposes}/{@link selectLabels} - * 判定 `!res.ok` 時,以本函式把失敗細節寫進 WRN log,讓 CI 記錄能看出 AI CLI 為何失敗; - * 需要輸出片段輔助診斷時,於 workflow 設定 `ACTIONS_STEP_DEBUG=true` 再重跑。 - * 本函式未匯出,僅供模組內部使用。 - */ -function agentFailureDetail(res) { - const parts = []; - const err = res && res.error; - if (err) { - if (err.killed) parts.push('已逾時終止'); - if (typeof err.code === 'number') parts.push(`exit ${err.code}`); - else if (err.code) parts.push(`code ${err.code}`); - else if (err.signal) parts.push(`signal ${err.signal}`); - } - // 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。 - if (process.env.ACTIONS_STEP_DEBUG === 'true') { - const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT)); - if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); - const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT)); - if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); - } - if (parts.length === 0) { - parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); - } - return parts.join('|'); -} - /** * 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。 * @@ -626,6 +557,28 @@ function appendExclusions({ cwd, excluded, prNumber }) { return true; } +/** + * 依審查模式與結果決定收尾要提交的結果檔。 + * + * 一般模式永遠提交本回合 findings 檔;建問題模式通常把問題明細留在 issue,不提交 + * findings。例外是有嚴重問題時仍提交 findings 檔,讓 `[failure]` 結果 commit 一定能產生, + * 避免問題相依 API 不支援或設定失敗時 PR 缺少失敗檢查。 + * + * @param {Object} params - 解構參數。 + * @param {boolean} params.createIssue - 是否啟用建問題模式。 + * @param {number} params.severeCount - 嚴重 finding 數量。 + * @param {string} params.relativePath - 本回合保存的 findings 檔 repo 相對路徑。 + * @param {boolean} params.exclusionsChanged - exclusions.json 是否有實際異動。 + * @returns {string[]} 應交給 `commitFindings` 的 repo 相對路徑清單。 + */ +function resultFilesToCommit({ createIssue, severeCount, relativePath, exclusionsChanged }) { + const files = createIssue ? (severeCount > 0 ? [relativePath] : []) : [relativePath]; + if (exclusionsChanged) { + files.push(path.join('.gitea', 'ai-review', 'exclusions.json')); + } + return files; +} + /** * 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。 * @@ -926,13 +879,10 @@ module.exports = { runDefenders, sortFindings, appendExclusions, + resultFilesToCommit, selectLabels, postSevereToIssue, postOthersToIssue, resolveOldComments, postSevereComments, - __test: { - agentFailureDetail, - redactSecrets, - }, }; -- 2.53.0 From 8b50a0d6439cbb5bc6f664f6a9f6b03056576940 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 14:24:10 +0800 Subject: [PATCH 63/79] =?UTF-8?q?test(ai-review):=20=E8=A3=9C=E4=B8=8A?= =?UTF-8?q?=E5=BB=BA=E5=95=8F=E9=A1=8C=E6=A8=A1=E5=BC=8F=E7=B5=90=E6=9E=9C?= =?UTF-8?q?=E6=AA=94=E6=B8=AC=E8=A9=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/review.test.js | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/test/review.test.js b/test/review.test.js index 8d10353..d49e210 100644 --- a/test/review.test.js +++ b/test/review.test.js @@ -4,12 +4,13 @@ const assert = require('node:assert/strict'); const test = require('node:test'); const review = require('../src/lib/review'); +const diagnostics = require('../src/lib/diagnostics'); test('agentFailureDetail 預設不輸出 stderr/stdout 片段', () => { const oldDebug = process.env.ACTIONS_STEP_DEBUG; delete process.env.ACTIONS_STEP_DEBUG; try { - const detail = review.__test.agentFailureDetail({ + const detail = diagnostics.agentFailureDetail({ ok: false, error: Object.assign(new Error('boom'), { code: 1 }), stderr: 'token=super-secret-value', @@ -27,7 +28,7 @@ test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => { const oldDebug = process.env.ACTIONS_STEP_DEBUG; process.env.ACTIONS_STEP_DEBUG = 'true'; try { - const detail = review.__test.agentFailureDetail({ + const detail = diagnostics.agentFailureDetail({ ok: false, error: Object.assign(new Error('boom'), { code: 2 }), stderr: 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890', @@ -72,3 +73,27 @@ test('postOthersToIssue 批次送出 issue 留言', async () => { assert.equal(calls[0].issueNumber, 7); assert.ok(maxActive > 1); }); + +test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => { + assert.deepEqual( + review.resultFilesToCommit({ + createIssue: true, + severeCount: 1, + relativePath: '.gitea/ai-review/findings/run.json', + exclusionsChanged: false, + }), + ['.gitea/ai-review/findings/run.json'], + ); +}); + +test('resultFilesToCommit 在建問題模式無嚴重問題時只提交 exclusions 異動', () => { + assert.deepEqual( + review.resultFilesToCommit({ + createIssue: true, + severeCount: 0, + relativePath: '.gitea/ai-review/findings/run.json', + exclusionsChanged: true, + }), + ['.gitea/ai-review/exclusions.json'], + ); +}); -- 2.53.0 From 9e7f8a2a0a807c26e12e9bf0381e7decfb9e5568 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 14:24:14 +0800 Subject: [PATCH 64/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E5=9B=9E=E5=AF=AB=E6=9C=AC=E8=BC=AA=E5=B7=B2=E8=99=95=E7=90=86?= =?UTF-8?q?=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../findings/2026-07-20-18:31:07.json | 42 ------------------- 1 file changed, 42 deletions(-) diff --git a/.gitea/ai-review/findings/2026-07-20-18:31:07.json b/.gitea/ai-review/findings/2026-07-20-18:31:07.json index f7e6496..87139a2 100644 --- a/.gitea/ai-review/findings/2026-07-20-18:31:07.json +++ b/.gitea/ai-review/findings/2026-07-20-18:31:07.json @@ -134,20 +134,6 @@ "suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", "sourceIssue": 15 }, - { - "id": "F019", - "reviewer": "Leo", - "focus": "", - "badge": "🧰", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 13, - "endLine": 72, - "problem": "`redactSecrets()` 與 `agentFailureDetail()` 是低階日誌診斷/遮罩邏輯,現在放在 `review.js` 這個負責 diff 整理與審查決策的模組頂端。這會讓 `review.js` 的職責繼續膨脹:未來若其他模組也要安全輸出 CLI 錯誤,只能複製這段或反向依賴 review 模組,邊界會越來越不清楚。", - "suggestion": "把這兩個函式搬到專門的工具模組,例如 `src/lib/diagnostics.js` 或 `src/lib/log-redaction.js`,並由 `review.js` 引入。這樣遮罩規則可集中測試與重用,`review.js` 也能維持在「審查流程資料處理」的邊界內。", - "suggestedCode": "", - "sourceIssue": 17 - }, { "id": "F022", "reviewer": "Leo", @@ -232,20 +218,6 @@ "suggestedCode": "```\nconst issueBuffer = [];\nlet trackingIssue = null;\n\nconst postComment = async (body) => {\n if (ctx.createIssue) {\n if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n issueBuffer.push(body);\n return null;\n }\n const created = await gitea.createIssueComment(ctx, body);\n currentRunCommentIds.add(created.id);\n return created;\n};\n```", "sourceIssue": 19 }, - { - "id": "F028", - "reviewer": "Leo", - "focus": "", - "badge": "🧰", - "severity": "建議", - "file": "src/lib/review.js", - "startLine": 14, - "endLine": 75, - "problem": "`review.js` 這次新增 `redactSecrets()` 與 `agentFailureDetail()`,但這兩個函式處理的是 AI CLI 執行失敗診斷與機密遮罩,責任更接近 `agents.js` 或共用 log/sanitize 工具。現在審查結果整理模組同時負責 diff、裁決、issue 發文與 CLI 診斷格式,模組邊界越來越鬆;之後其他地方若也要記錄 agent 失敗,很容易複製一份遮罩邏輯或反向依賴 `review.js`。", - "suggestion": "將這兩個函式移到 `src/lib/agents.js`(例如匯出 `formatAgentFailure()`),或新增 `src/lib/sanitize.js`/`src/lib/diagnostics.js`。`review.js` 只消費格式化後的錯誤摘要,避免讓審查編排模組承擔 CLI 診斷細節。", - "suggestedCode": "", - "sourceIssue": 19 - }, { "id": "F029", "reviewer": "Bard", @@ -302,20 +274,6 @@ "suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", "sourceIssue": 21 }, - { - "id": "F036", - "reviewer": "Rogue", - "focus": "", - "badge": "⚡", - "severity": "警告", - "file": "src/index.js", - "startLine": 305, - "endLine": 305, - "problem": "攻擊方沒有找出任何 finding 時,這裡還是照樣啟動防守方 `runDefenders`。空陣列沒有東西可裁決,卻會多跑一輪 AI CLI/sub agent、讀 exclusions/history、組 prompt;每個乾淨 PR 都被偷走 1 次防守方呼叫的 CPU、等待時間與 token。", - "suggestion": "在 `findings.length === 0` 時直接略過防守方裁決,令 `kept/excluded` 都是空陣列,直接進入保存結果與收尾。這不是微優化,是整輪 AI 呼叫直接歸零。", - "suggestedCode": "```\nconst { kept, excluded } = findings.length === 0\n ? { kept: [], excluded: [] }\n : await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });\n```", - "sourceIssue": 22 - }, { "id": "F040", "reviewer": "Assassin", -- 2.53.0 From 941d7631296364d99498c2e8d14157f3cffba283 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Tue, 21 Jul 2026 06:27:43 +0000 Subject: [PATCH 65/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 99 ++++++ .../findings/2026-07-21-14:27:42.json | 318 ++++++++++++++++++ 2 files changed, 417 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-21-14:27:42.json diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index b9bc1f1..df97c7c 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1692,5 +1692,104 @@ "endLine": 358, "problem": "推送結果 commit 的認證方式改成 `pushWithCredential`:不走 origin、自訂 `GIT_CONFIG_*` extraheader、先清掉 checkout token、URL origin 必須相符、失敗時隱藏遠端與 token。這些安全與 CI 觸發相關行為目前完全沒有測試;現有 `gitrepo.test.js` 只測分支名稱驗證。", "reason": "Paladin:可排除(重複)。歷史 findings 已記錄 commitAndPushFindings/pushWithCredential 的 token 認證推送路徑、推送目標、錯誤遮蔽與安全行為缺少測試。" + }, + { + "addedAt": "2026/07/21 14:27:42", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/lib/diagnostics.js", + "startLine": 23, + "endLine": 23, + "problem": "攻擊者只要讓 AI CLI 在 debug 模式下把 `Authorization: token ` 或 `Authorization: Basic ` 印到 stderr/stdout,這條遮罩規則只會遮掉 `token`/`Basic` 這個 scheme,後面的真正憑證仍會進 CI log。這正好打穿本檔宣稱的「安全診斷」防線,PAT 或 API token 會被長期保存並暴露給可讀 log 的人。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出除錯診斷輸出中 Authorization 遮罩規則可能只遮到 scheme、留下後方憑證,並造成 CI log 洩漏,與本條同一問題。" + }, + { + "addedAt": "2026/07/21 14:27:42", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 181, + "endLine": 239, + "problem": "`main()` 這次把「PR 留言」、「issue 暫存」、「issue 建立後 flush」三種狀態直接塞進閉包變數 `currentRunCommentIds`、`issueBuffer`、`issue` 與 `queueOrPostComment()`。未來只要再新增一種落地方式或調整留言順序,維護者必須同時理解整個主流程的時序,否則很容易出現留言漏發、舊留言誤標、或 issue 尚未建立卻被使用的狀態錯誤。這段已經不是單純編排,而是隱含一個留言發布狀態機。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布狀態機耦合的同一問題。" + }, + { + "addedAt": "2026/07/21 14:27:42", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "readme.md", + "startLine": 56, + "endLine": 132, + "problem": "README 這次把大量連結從 `branch/master` 改成 `branch/develop`,同時保留許多硬編碼行號。這種文件會隨分支名稱、預設分支、或任一檔案插入幾行就整批失準;未來維護者每次調整程式都得同步修幾十個 URL,否則文件看起來精準、實際上卻導到錯誤位置。", + "reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 README 大量硬編 branch 與行號深連結,造成文件與原始碼高同步成本;本條為同一問題。" + }, + { + "addedAt": "2026/07/21 14:27:42", + "prNumber": 6, + "reviewer": "Mage", + "severity": "警告", + "file": "src/index.js", + "startLine": 217, + "endLine": 225, + "problem": "建問題模式先建立 issue,再逐則 flush `issueBuffer`,但中途任一留言 API 失敗會直接拋出,沒有回滾或冪等處理。最小重現:`gitea.createIssue` 成功建立 #10,第一則 buffered comment 成功,第二則因 500/timeout 失敗 → 主流程 exit 1,PR 沒有 issue 連結、也沒有結果 commit;重跑時又建立 #11,留下半成品 issue #10。這會在暫時性 API 錯誤下造成重複追蹤 issue 與狀態不一致。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。issue 建立後 flush 暫存留言失敗、重跑又建立重複 issue,正是已知排除事項與多筆歷史 findings 已記錄的問題。" + }, + { + "addedAt": "2026/07/21 14:27:42", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 241, + "problem": "建問題模式新增了「先暫存留言、確定有保留問題才建立 issue、再 flush 暫存留言」的流程,但目前新增測試只驗到低階 helper,沒有驗證主流程在 create-issue=true 時的留言去向與靜默通過行為。這裡若順序錯了,可能會把原本應該留在 issue 的工具/diff/角色留言發到 PR,或在無保留問題時留下不該出現的 PR 留言。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 create-issue 模式中留言先暫存、issue 建立後依序寫入、無 finding 靜默通過與一般模式留言目的地缺少主流程測試。" + }, + { + "addedAt": "2026/07/21 14:27:42", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 354, + "endLine": 395, + "problem": "新增的建問題模式會依嚴重度分流:嚴重問題改發到 issue、PR 回貼 issue 連結,且只有嚴重問題才呼叫 `addIssueDependency`。目前測試沒有覆蓋這個決策矩陣,等於沒有驗證「警告/建議不阻擋合併」與「嚴重問題要建立相依」這兩個關鍵行為。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的嚴重/非嚴重分流、PR 回貼、相依 API 降級與核心分支缺少測試;本條屬同一測試缺口。" + }, + { + "addedAt": "2026/07/21 14:27:42", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 130, + "endLine": 188, + "problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每次補抓後重試 merge-base、以及淺層 repo 才執行 `--unshallow` 的行為,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證這些成功/失敗路徑。這段是 diff 基準的核心,一旦 fallback 順序或條件壞掉,review 可能拿錯範圍或在 shallow checkout 直接失敗。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、shallow 與非 shallow 分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/21 14:27:42", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 331, + "endLine": 366, + "problem": "`pushWithCredential` 是這次結果 commit 能否用 PAT 觸發下一輪 CI 的關鍵,但目前沒有測試驗證它真的清掉 checkout 的自動 token、再注入 PAT Authorization,也沒有測失敗時不外洩 remote URL/token。這條失敗路徑沒被試煉過,可能讓 `[failure]` 結果 commit 無法觸發快速回報,或在錯誤訊息裡洩漏認證資訊。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushWithCredential/commitAndPushFindings 的認證推送策略、token 注入、推送目標與失敗時不外洩 token 或 URL 的測試缺口。" + }, + { + "addedAt": "2026/07/21 14:27:42", + "prNumber": 6, + "reviewer": "Maya", + "severity": "建議", + "file": "src/lib/diagnostics.js", + "startLine": 21, + "endLine": 32, + "problem": "`redactSecrets` 新增多個遮罩規則與控制字元單行化,但測試只透過 `agentFailureDetail` 間接覆蓋 Authorization 與 token 兩種格式。URL 內嵌帳密、長 token、GitHub token 樣式、控制字元注入與 null/undefined 邊界都還沒被直接驗證。", + "reason": "Paladin:可排除(重複)。歷史 findings 已記錄失敗診斷與 redactSecrets 對 Authorization、URL 帳密、token 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。" } ] diff --git a/.gitea/ai-review/findings/2026-07-21-14:27:42.json b/.gitea/ai-review/findings/2026-07-21-14:27:42.json new file mode 100644 index 0000000..a6e11cb --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-21-14:27:42.json @@ -0,0 +1,318 @@ +{ + "generatedAt": "2026/07/21 14:27:42", + "commitSha": "9e7f8a2a0a807c26e12e9bf0381e7decfb9e5568", + "prNumber": 6, + "tool": { + "name": "codex", + "version": "codex-cli 0.144.6", + "model": "gpt-5.5" + }, + "findings": [ + { + "reviewer": "Mage", + "focus": "logic", + "badge": "🔮", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 149, + "endLine": 153, + "problem": "這個變更把「本輪審查」改成即使有嚴重問題也回傳 0,並假設後續由 `[ai-review-bot][failure]` 結果 commit 觸發下一輪 CI 再失敗。最小重現:workflow 仍依 action.yml 常見用法傳入自動 token(或任一不會觸發 synchronize CI 的 token)→ AI 找到 1 條嚴重問題 → 本輪成功建立留言與 push 結果 commit,但該 push 不觸發下一輪 → 沒有任何檢查讀到 `[failure]` commit,PR 最終呈現通過。這是未驗證的外部時序假設;嚴重 finding 會被靜默放行。", + "suggestion": "不要讓失敗判定完全依賴下一輪 CI。若 `severe.length > 0`,本輪在完成留言與結果落地後仍應回傳 1;或至少提供明確 input 控制 direct-fail,並在無法驗證 token 會觸發 CI 時預設直接 fail。", + "suggestedCode": "", + "id": "F009", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。此條指控嚴重 finding 需仰賴下一輪 CI 才失敗,且自動 token 可能不觸發 CI,與既有 pushToken 測試缺口或 PAT 風險 finding 不屬同一處同一失效模式;未能排除。" + } + } + }, + { + "reviewer": "Mage", + "focus": "logic", + "badge": "🔮", + "severity": "警告", + "file": "src/lib/review.js", + "startLine": 707, + "endLine": 718, + "problem": "`postOthersToIssue` 以並行方式送出多則 issue 留言時,issue 上的實際留言順序取決於 API 回應與資料庫寫入完成順序,不保證等於已排序的 findings 順序。最小重現:兩條建議分別位於 `a.js:10` 與 `a.js:20`,第二個 API 較快完成,issue 會先出現第 20 行問題,再出現第 10 行問題;使用者逐檔逐行處理時順序會錯亂。", + "suggestion": "若 issue 留言順序是介面契約,逐則 `await gitea.createCommentOnIssue(...)` 發送;若要保留並行,需在每則留言標題加入穩定序號,例如 `2/5`,讓非同步完成不破壞閱讀順序。", + "suggestedCode": "", + "id": "F011", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。歷史 findings 未涵蓋 postOthersToIssue 並行送出導致 issue 留言順序不穩定;目前也無排除事項可套用,故保留。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "action.yml", + "startLine": 22, + "endLine": 24, + "problem": "manifest 的註解忽然奏起一整段實作細節:PAT、CI 觸發、主程式步驟 1 全擠在 input 說明旁,和同檔其他「介面用途」型註解的節奏不一致。讀者只是想知道 token 該填什麼,卻被迫聽完流程旁白。", + "suggestion": "把 action.yml 留給介面契約;細節移到 README 或主流程文件。此處可濃縮成「建議使用可觸發 CI 的 PAT」即可。", + "suggestedCode": "# 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。", + "id": "F002", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。已知排除事項與歷史 findings 主要涵蓋 push-token 說明重複或 PAT 安全風險;本條指向 action.yml 中 token input 註解混入主流程步驟細節,並非同一處同一問題,未能排除。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "readme.md", + "startLine": 40, + "endLine": 47, + "problem": "mermaid 圖的節點 ID 旋律走岔了:畫面標示是 3→4→5→...→8→2→9,但節點名稱卻用 S3、S4、S2 來回跳。即使流程語意想表達「步驟 2 延後」,節點 ID 與視覺順序不一致,會讓維護者在對照圖與文字時多繞一圈。", + "suggestion": "讓節點 ID 維持閱讀順序,將真正的流程步驟放在節點文字裡。例如用 N2、N3 或 A、B 這類中性 ID,避免 S2 看起來像應該排在 S1 後面。", + "suggestedCode": "S1 -->|未命中| N3[3 偵測 AI 工具並留言]\n N3 --> N4[4 讀 .reviewignore 整理 diff 並留言]\n N4 --> N5[5 攻擊方登場留言]\n N5 --> N6[6 攻擊方 sub agent 並行找問題]\n N6 --> N7[7 防守方登場留言]\n N7 --> N8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json]\n N8 --> N2[2 延後將舊留言標記解決(成功產生結果後才執行)]\n N2 --> S9[9 嚴重問題逐條掛行留言]", + "id": "F003", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。歷史 findings 雖有流程步驟硬編碼與文件同步成本問題,但本條聚焦 mermaid 節點 ID 與視覺流程順序不一致,屬不同可讀性指控,未命中既有排除。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 122, + "endLine": 139, + "problem": "`main()` 的 JSDoc 從函式說明變成流程章回。步驟、例外模式、issue 建立時機、相依關係、commit 規則全塞在同一段,和程式下方已存在的分段註解重複,讀起來像同一旋律被兩把琴同時彈奏。", + "suggestion": "JSDoc 保留函式職責、回傳值與關鍵模式差異即可;完整 10 步驟流程交給 README 或下方區塊註解。這會讓 `main()` 開頭更快進入正題。", + "suggestedCode": "", + "id": "F004", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。歷史 findings 有步驟編號散落與 main() 內閉包 JSDoc 過重等問題,但本條指向 main() 函式 JSDoc 與下方流程註解重複,範圍與主張不完全相同,證據不足以排除。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/diagnostics.js", + "startLine": 45, + "endLine": 47, + "problem": "`res` 這個參數名太短促,和檔內其他 `text`、`parts`、`stderr`、`stdout` 這些直白命名相比顯得含糊。診斷工具本該讓人少猜一點,這裡卻讓讀者先猜它是哪一種 result。", + "suggestion": "改用 `result` 或 `agentResult`,讓函式簽名本身就說清楚資料來源。", + "suggestedCode": "function agentFailureDetail(agentResult) {\n const parts = [];\n const err = agentResult && agentResult.error;", + "id": "F005", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。未見已知排除事項或歷史 finding 涵蓋 diagnostics.js 中 res 參數命名過短的問題;屬新的命名可讀性指控。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "test/gitea.test.js", + "startLine": 8, + "endLine": 8, + "problem": "`fn` 是一個太倉促的縮寫,放在測試輔助函式裡尤其刺耳;同一行已有 `handler` 這種完整命名,`fn` 顯得像漏拍的音符。", + "suggestion": "改成 `callback` 或 `run`,讓呼叫意圖更清楚,也和此專案偏完整語意的命名風格一致。", + "suggestedCode": "function withFetchStub(handler, callback) {", + "id": "F006", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。未見已知排除事項或歷史 finding 涵蓋 test/gitea.test.js 中 fn 測試輔助參數命名問題;屬新的命名可讀性指控。" + } + } + } + ], + "excluded": [ + { + "reviewer": "Assassin", + "focus": "security", + "badge": "🗡️", + "severity": "嚴重", + "file": "src/lib/diagnostics.js", + "startLine": 23, + "endLine": 23, + "problem": "攻擊者只要讓 AI CLI 在 debug 模式下把 `Authorization: token ` 或 `Authorization: Basic ` 印到 stderr/stdout,這條遮罩規則只會遮掉 `token`/`Basic` 這個 scheme,後面的真正憑證仍會進 CI log。這正好打穿本檔宣稱的「安全診斷」防線,PAT 或 API token 會被長期保存並暴露給可讀 log 的人。", + "suggestion": "把整個 Authorization header value 視為機密,不要只特判 Bearer。應遮掉 scheme 與 credential 全段;若要保留 scheme,也只能保留固定白名單字樣,不能留下後面的 token。", + "suggestedCode": ".replace(/(authorization\\s*[:=]\\s*)(?:\\S+\\s+)?\\S+/gi, '$1***')", + "id": "F001", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 finding 已指出除錯診斷輸出中 Authorization 遮罩規則可能只遮到 scheme、留下後方憑證,並造成 CI log 洩漏,與本條同一問題。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 181, + "endLine": 239, + "problem": "`main()` 這次把「PR 留言」、「issue 暫存」、「issue 建立後 flush」三種狀態直接塞進閉包變數 `currentRunCommentIds`、`issueBuffer`、`issue` 與 `queueOrPostComment()`。未來只要再新增一種落地方式或調整留言順序,維護者必須同時理解整個主流程的時序,否則很容易出現留言漏發、舊留言誤標、或 issue 尚未建立卻被使用的狀態錯誤。這段已經不是單純編排,而是隱含一個留言發布狀態機。", + "suggestion": "把留言目的地抽成小型模組,例如 `CommentSink` / `ReviewPublisher`,讓 `main()` 只負責流程編排:`publisher.postContext()`、`publisher.flushIssue()`、`publisher.resolveOldPrComments()`。建問題模式的暫存與 flush 可獨立測試,半年後改流程時不用在 `main()` 裡追閉包狀態。", + "suggestedCode": "", + "id": "F007", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(命中已知排除事項且重複)。既有紀錄已涵蓋 main() 內留言路由、issue 狀態、緩衝佇列與發布狀態機耦合的同一問題。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "readme.md", + "startLine": 56, + "endLine": 132, + "problem": "README 這次把大量連結從 `branch/master` 改成 `branch/develop`,同時保留許多硬編碼行號。這種文件會隨分支名稱、預設分支、或任一檔案插入幾行就整批失準;未來維護者每次調整程式都得同步修幾十個 URL,否則文件看起來精準、實際上卻導到錯誤位置。", + "suggestion": "改用 repo 相對連結並盡量避免行號,例如 `src/lib/log.js` 或 `src/lib/log.js#L20` 只保留少數真正需要精準定位的 API。若這段是產生文件,請把分支名稱與連結格式集中在產生器設定,不要把同一個 branch 字串散落在 README 表格裡。", + "suggestedCode": "", + "id": "F008", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已多次指出 README 大量硬編 branch 與行號深連結,造成文件與原始碼高同步成本;本條為同一問題。" + } + } + }, + { + "reviewer": "Mage", + "focus": "logic", + "badge": "🔮", + "severity": "警告", + "file": "src/index.js", + "startLine": 217, + "endLine": 225, + "problem": "建問題模式先建立 issue,再逐則 flush `issueBuffer`,但中途任一留言 API 失敗會直接拋出,沒有回滾或冪等處理。最小重現:`gitea.createIssue` 成功建立 #10,第一則 buffered comment 成功,第二則因 500/timeout 失敗 → 主流程 exit 1,PR 沒有 issue 連結、也沒有結果 commit;重跑時又建立 #11,留下半成品 issue #10。這會在暫時性 API 錯誤下造成重複追蹤 issue 與狀態不一致。", + "suggestion": "建立 issue 後立即寫入可重試識別資訊,重跑前先搜尋/復用既有本 PR 的 AI review issue;或在 flush 失敗時捕捉錯誤,將已建立 issue 補上失敗狀態與 PR 回連,再讓流程以可診斷方式收束。", + "suggestedCode": "", + "id": "F010", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(命中已知排除事項且重複)。issue 建立後 flush 暫存留言失敗、重跑又建立重複 issue,正是已知排除事項與多筆歷史 findings 已記錄的問題。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/index.js", + "startLine": 180, + "endLine": 241, + "problem": "建問題模式新增了「先暫存留言、確定有保留問題才建立 issue、再 flush 暫存留言」的流程,但目前新增測試只驗到低階 helper,沒有驗證主流程在 create-issue=true 時的留言去向與靜默通過行為。這裡若順序錯了,可能會把原本應該留在 issue 的工具/diff/角色留言發到 PR,或在無保留問題時留下不該出現的 PR 留言。", + "suggestion": "請補主流程層級測試,stub `gitea`、`review.runAttackers`、`review.runDefenders`、`agents.detectTool`,至少驗證:\n\n| 情境 | 應斷言 |\n| --- | --- |\n| `createIssue=true` 且 `kept.length > 0` | 建 issue 前的情境留言先暫存,issue 建立後依序寫入該 issue |\n| `createIssue=true` 且 `kept.length === 0` | 不建立 issue、不對 PR 留言、不呼叫 `resolveOldComments` |\n| 一般模式 | 情境留言仍發到 PR,且 id 被加入 `currentRunCommentIds` |", + "suggestedCode": "", + "id": "F012", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已涵蓋 create-issue 模式中留言先暫存、issue 建立後依序寫入、無 finding 靜默通過與一般模式留言目的地缺少主流程測試。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/index.js", + "startLine": 354, + "endLine": 395, + "problem": "新增的建問題模式會依嚴重度分流:嚴重問題改發到 issue、PR 回貼 issue 連結,且只有嚴重問題才呼叫 `addIssueDependency`。目前測試沒有覆蓋這個決策矩陣,等於沒有驗證「警告/建議不阻擋合併」與「嚴重問題要建立相依」這兩個關鍵行為。", + "suggestion": "請補 create-issue 模式的結果分流測試,至少涵蓋:\n\n| findings | 應斷言 |\n| --- | --- |\n| 只有警告/建議 | 建 issue、逐條留言、PR 回貼連結,但不呼叫 `addIssueDependency` |\n| 有嚴重問題 | 呼叫 `postSevereToIssue`、PR 回貼連結,並呼叫 `addIssueDependency(ctx, ctx.prNumber, issue.number)` |\n| `addIssueDependency` 拋錯 | 流程降級完成,不中斷收尾 commit |", + "suggestedCode": "", + "id": "F013", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式的嚴重/非嚴重分流、PR 回貼、相依 API 降級與核心分支缺少測試;本條屬同一測試缺口。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 130, + "endLine": 188, + "problem": "`resolveMergeBase` 新增了多段 fetch 補歷史策略、每次補抓後重試 merge-base、以及淺層 repo 才執行 `--unshallow` 的行為,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證這些成功/失敗路徑。這段是 diff 基準的核心,一旦 fallback 順序或條件壞掉,review 可能拿錯範圍或在 shallow checkout 直接失敗。", + "suggestion": "請把 git 執行層抽成可注入或以臨時 repo 測試,補齊:首次 merge-base 成功不跑 fallback、deepen base 後成功即停止、deepen PR HEAD 用目前 HEAD SHA、只有 shallow repo 才嘗試 `--unshallow`、全部策略失敗時錯誤訊息含診斷且保留 cause。", + "suggestedCode": "", + "id": "F014", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、shallow 與非 shallow 分支、停止條件、降級與最終失敗診斷缺少測試提出相同問題。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 331, + "endLine": 366, + "problem": "`pushWithCredential` 是這次結果 commit 能否用 PAT 觸發下一輪 CI 的關鍵,但目前沒有測試驗證它真的清掉 checkout 的自動 token、再注入 PAT Authorization,也沒有測失敗時不外洩 remote URL/token。這條失敗路徑沒被試煉過,可能讓 `[failure]` 結果 commit 無法觸發快速回報,或在錯誤訊息裡洩漏認證資訊。", + "suggestion": "請補 `commitAndPushFindings`/`pushWithCredential` 的測試,stub `execFileSync` 驗證:`git push` 使用不含帳密的 remote URL、`GIT_CONFIG_COUNT=2`、第 0 筆 extraheader 為空值、第 1 筆為 Basic Authorization、remote origin 不符時拒絕、push 失敗時拋出的錯誤不包含 token 或 remote URL。", + "suggestedCode": "", + "id": "F015", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已涵蓋 pushWithCredential/commitAndPushFindings 的認證推送策略、token 注入、推送目標與失敗時不外洩 token 或 URL 的測試缺口。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "建議", + "file": "src/lib/diagnostics.js", + "startLine": 21, + "endLine": 32, + "problem": "`redactSecrets` 新增多個遮罩規則與控制字元單行化,但測試只透過 `agentFailureDetail` 間接覆蓋 Authorization 與 token 兩種格式。URL 內嵌帳密、長 token、GitHub token 樣式、控制字元注入與 null/undefined 邊界都還沒被直接驗證。", + "suggestion": "請補 `redactSecrets` 的直接單元測試,包含:`https://user:pass@host`、`ghp_...`、40 字以上 token、含換行/tab 的假日誌注入、`null`/`undefined`。斷言重點是輸出為單行、敏感片段被 `***` 取代,且一般錯誤文字仍保留可診斷性。", + "suggestedCode": "", + "id": "F016", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已記錄失敗診斷與 redactSecrets 對 Authorization、URL 帳密、token 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。" + } + } + } + ] +} -- 2.53.0 From 9d05af647bb988ba120396c74f129f9400d32e52 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 14:39:42 +0800 Subject: [PATCH 66/79] =?UTF-8?q?fix(ai-review):=20=E5=B0=8D=E9=BD=8A?= =?UTF-8?q?=E5=BB=BA=E5=95=8F=E9=A1=8C=E6=A8=A1=E5=BC=8F=E7=95=99=E8=A8=80?= =?UTF-8?q?=E9=A0=86=E5=BA=8F=E8=88=87=E5=91=BD=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- action.yml | 4 +-- readme.md | 20 +++++------ src/index.js | 77 ++++++++++++++++-------------------------- src/lib/diagnostics.js | 10 +++--- src/lib/gitea.js | 14 ++++---- src/lib/review.js | 12 +++---- src/lib/templates.js | 4 +-- test/gitea.test.js | 4 +-- test/review.test.js | 4 +-- 9 files changed, 64 insertions(+), 85 deletions(-) diff --git a/action.yml b/action.yml index 68d0977..859e653 100644 --- a/action.yml +++ b/action.yml @@ -19,9 +19,7 @@ inputs: # Gitea API token:用於對 PR/issue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。 token: # 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。 - # 建議傳入「能觸發 CI 的 PAT」:以自動 token(gitea.token / GITHUB_TOKEN)推送的結果 commit 不會 - # 再觸發 CI,導致新 head 缺檢查而卡合併;改用 PAT 推送會讓 PR 的 synchronize 事件再觸發 CI, - # 由主程式步驟 1 快速回報([success]/[failure])廉價地把結果蓋到新 head。 + # 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。 description: 'Gitea API token(PR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)' # 必填:缺少 token 無法呼叫 Gitea API,action 無法運作。 required: true diff --git a/readme.md b/readme.md index 68799df..daf0b4f 100644 --- a/readme.md +++ b/readme.md @@ -38,16 +38,16 @@ jobs: ```mermaid flowchart TD S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure] - S1 -->|未命中| S3[3 偵測 AI 工具並留言] - S3 --> S4[4 讀 .reviewignore 整理 diff 並留言] - S4 --> S5[5 攻擊方登場留言] - S5 --> S6[6 攻擊方 sub agent 並行找問題] - S6 --> S7[7 防守方登場留言] - S7 --> S8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json] - S8 --> S2[2 延後將舊留言標記解決(成功產生結果後才執行)] - S2 --> S9[9 嚴重問題逐條掛行留言] - S9 --> S10[10 警告+建議彙整表格留言] - S10 --> E1[收尾 commit/push + exit code] + S1 -->|未命中| N3[3 偵測 AI 工具並留言] + N3 --> N4[4 讀 .reviewignore 整理 diff 並留言] + N4 --> N5[5 攻擊方登場留言] + N5 --> N6[6 攻擊方 sub agent 並行找問題] + N6 --> N7[7 防守方登場留言] + N7 --> N8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json] + N8 --> N2[2 延後將舊留言標記解決(成功產生結果後才執行)] + N2 --> N9[9 嚴重問題逐條掛行留言] + N9 --> N10[10 警告+建議彙整表格留言] + N10 --> E1[收尾 commit/push + exit code] ``` ## 專案列表 diff --git a/src/index.js b/src/index.js index 4e5cafd..1b63e3b 100644 --- a/src/index.js +++ b/src/index.js @@ -117,31 +117,11 @@ function commitFindings({ cwd, ctx, files, result }) { } /** - * AI code review 主流程:依固定 10 步驟執行多角色審查,回傳 process exit code。 + * AI code review 主流程:編排多角色審查、發布審查結果,並回傳 process exit code。 * - * 流程概要(步驟 2~10 描述一般模式;建問題模式差異見末段): - * 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審; - * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步)—— - * 此步延後到「本回合審查已成功產生結果、即將發布問題留言前」才執行,避免工具偵測/diff/ - * 攻防裁決任一失敗時舊結果先被清掉卻沒有新結果(一般模式); - * 3. 偵測 AI 工具(antigravity/codex/claude)並留言; - * 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, - * 一般模式 commit success、建問題模式略過 commit,回傳 0); - * 5–6. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題; - * 7–8. 防守方登場留言、裁決誤報後排序並保存 findings JSON, - * 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json; - * 9. 嚴重問題逐條掛在程式碼行上留言; - * 10. 警告+建議彙整為單一表格留言; - * 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言 - * 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後先挑好標籤、連同標籤一次建立 issue - * 並寫入暫存留言,嚴重問題與警告+建議再逐條發到該 issue,讓每條問題都能被個別回覆); - * 無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過); - * 收束時在 PR 回貼 issue 連結形成雙向關聯, - * 並「僅在有嚴重問題時」讓 PR 相依於該 issue(addIssueDependency,issue 關閉前 PR 無法合併; - * 需 repo 啟用問題相依功能)——僅有警告/建議時 issue 仍建立供追蹤,但不阻擋合併; - * 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、 - * 建問題模式只 commit exclusions.json、無檔案可 commit 時略過; - * commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。 + * 一般模式會把審查情境、嚴重問題與警告/建議發布到 PR,並在成功產生本回合結果後才把舊留言標為過時。 + * 建問題模式會把審查情境與每條 finding 發到追蹤 issue;沒有保留 finding 時不建立 issue、PR 也不留言。 + * 嚴重 finding 會寫入 failure 結果 commit,警告與建議只建立追蹤資訊,不直接阻擋合併。 * * @returns {Promise} process exit code:本輪「審查」一律回傳 0(不因嚴重問題直接讓檢查失敗—— * 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報); @@ -182,14 +162,14 @@ async function main() { // 本回合(一般模式)發出的 PR 留言 id:resolveOldComments 標註過時時要跳過這些。 const currentRunCommentIds = new Set(); - // 建問題模式:issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色) - // 先暫存於 issueBuffer,建立 issue 後一次寫入。 - const issueBuffer = []; - let issue = null; + // 建問題模式:追蹤 issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色) + // 先暫存於 pendingIssueCommentBodies,建立 issue 後一次寫入。 + const pendingIssueCommentBodies = []; + let trackingIssue = null; /** * 發布一則審查留言。依模式決定去向: * - 一般模式:發到 PR,並記錄留言 id 供 `resolveOldComments` 排除。 - * - 建問題模式:issue 已建立時發到 issue;尚未建立時先暫存到 `issueBuffer`。 + * - 建問題模式:追蹤 issue 已建立時發到 issue;尚未建立時先暫存到 `pendingIssueCommentBodies`。 * * @param {string} body 要發布的 Markdown 留言內容。 * @returns {Promise} 一般模式、或建問題模式且 issue 已建立時回傳 Gitea 留言物件; @@ -200,8 +180,8 @@ async function main() { */ const queueOrPostComment = async (body) => { if (ctx.createIssue) { - if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body); - issueBuffer.push(body); + if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body); + pendingIssueCommentBodies.push(body); return null; } const created = await gitea.createIssueComment(ctx, body); @@ -210,24 +190,25 @@ async function main() { }; /** * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立), - * 並把 `issueBuffer` 內暫存的情境留言依流程順序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 + * 並把 `pendingIssueCommentBodies` 內暫存的情境留言依流程順序寫入 issue; + * 設定閉包變數 `trackingIssue` 供後續留言直接發到 issue。 * 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。 * * @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選); * 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。 - * @returns {Promise} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。 + * @returns {Promise} 無回傳值;結果反映在閉包變數 `trackingIssue` 與 issue 留言。 */ const createIssueAndFlushBufferedComments = async (labelIds = []) => { - issue = await gitea.createIssue(ctx, { + trackingIssue = await gitea.createIssue(ctx, { title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), labels: labelIds, }); - log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`); - for (const body of issueBuffer) { - await gitea.createCommentOnIssue(ctx, issue.number, body); + log('建問題', 'INF', `已建立追蹤 issue #${trackingIssue.number},寫入 ${pendingIssueCommentBodies.length} 則情境留言。`); + for (const body of pendingIssueCommentBodies) { + await gitea.createCommentOnIssue(ctx, trackingIssue.number, body); } - issueBuffer.length = 0; + pendingIssueCommentBodies.length = 0; }; // ── 步驟 2:延後執行 ─────────────────────────────────────────────────── @@ -355,7 +336,7 @@ async function main() { // ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─ if (severe.length > 0) { if (ctx.createIssue) { - await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe }); + await review.postSevereToIssue({ ctx, gitea, issueNumber: trackingIssue.number, severe }); } else { await review.postSevereComments({ ctx, gitea, severe, cwd }); } @@ -365,7 +346,7 @@ async function main() { // 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ── if (others.length > 0) { if (ctx.createIssue) { - await review.postOthersToIssue({ ctx, gitea, issueNumber: issue.number, others }); + await review.postOthersToIssue({ ctx, gitea, issueNumber: trackingIssue.number, others }); } else { await queueOrPostComment(templates.othersComment(others)); log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); @@ -374,12 +355,12 @@ async function main() { // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─ // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。 - if (ctx.createIssue && issue) { + if (ctx.createIssue && trackingIssue) { await gitea.createIssueComment( ctx, - templates.issueLinkComment({ - issueNumber: issue.number, - issueUrl: issue.html_url, + templates.prIssueLinkComment({ + issueNumber: trackingIssue.number, + issueUrl: trackingIssue.html_url, severeCount: severe.length, otherCount: others.length, }), @@ -388,15 +369,15 @@ async function main() { // 僅有警告/建議時,issue 仍建立供追蹤,但不掛相依、不阻擋 PR 合併。 if (severe.length > 0) { try { - await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number); - log('建問題', 'INF', `有嚴重問題:已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number},issue 關閉前無法合併。`); + await gitea.addIssueDependency(ctx, ctx.prNumber, trackingIssue.number); + log('建問題', 'INF', `有嚴重問題:已將 PR #${ctx.prNumber} 設為相依於 issue #${trackingIssue.number},issue 關閉前無法合併。`); } catch (err) { log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`); } } else { - log('建問題', 'INF', `無嚴重問題(僅警告/建議):issue #${issue.number} 僅供追蹤,不阻擋 PR 合併。`); + log('建問題', 'INF', `無嚴重問題(僅警告/建議):issue #${trackingIssue.number} 僅供追蹤,不阻擋 PR 合併。`); } - log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`); + log('建問題', 'INF', `issue #${trackingIssue.number} 已寫入審查內容,並在 PR 回貼連結。`); } // ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)─────── diff --git a/src/lib/diagnostics.js b/src/lib/diagnostics.js index 011cd28..d39e9ee 100644 --- a/src/lib/diagnostics.js +++ b/src/lib/diagnostics.js @@ -37,13 +37,13 @@ function redactSecrets(text) { * 訊號與逾時狀態;只有 `ACTIONS_STEP_DEBUG=true` 時才附上經 {@link redactSecrets} * 遮罩且去除控制字元的 stderr/stdout 片段。 * - * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res + * @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} agentResult * `runAgent` 的回傳物件。 * @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。 */ -function agentFailureDetail(res) { +function agentFailureDetail(agentResult) { const parts = []; - const err = res && res.error; + const err = agentResult && agentResult.error; if (err) { if (err.killed) parts.push('已逾時終止'); if (typeof err.code === 'number') parts.push(`exit ${err.code}`); @@ -52,9 +52,9 @@ function agentFailureDetail(res) { } // 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。 if (process.env.ACTIONS_STEP_DEBUG === 'true') { - const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); + const stderr = redactSecrets(String((agentResult && agentResult.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); - const stdout = redactSecrets(String((res && res.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); + const stdout = redactSecrets(String((agentResult && agentResult.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); } if (parts.length === 0) { diff --git a/src/lib/gitea.js b/src/lib/gitea.js index d99e4ce..55a80ae 100644 --- a/src/lib/gitea.js +++ b/src/lib/gitea.js @@ -173,23 +173,23 @@ function createIssue(ctx, { title, body, labels }) { * 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/dependencies` * (body 為 IssueMeta:`{index, owner, repo}`)。 * - * 語義:URL 的 issue(`issueNumber`)相依於 body 的 issue(`dependency`)—— - * 在 `dependency` 關閉前,`issueNumber` 無法合併/關閉。本 endpoint 需 repo 啟用 + * 語義:URL 的 issue(`blockedIssueNumber`)相依於 body 的 issue(`blockingIssueNumber`)—— + * 在 `blockingIssueNumber` 關閉前,`blockedIssueNumber` 無法合併/關閉。本 endpoint 需 repo 啟用 * 「問題相依(issue dependencies)」功能,屬版本/設定相依;未啟用或不支援時 API 會回非 2xx。 * * @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、 * `owner`(repo 擁有者)、`repo`(repo 名稱)。 - * @param {number|string} issueNumber - 要被阻擋的 issue/PR 編號(相依方)。 - * @param {number} dependency - 作為阻擋來源的 issue 編號(同一 repo)。 + * @param {number|string} blockedIssueNumber - 要被阻擋的 issue/PR 編號(相依方)。 + * @param {number} blockingIssueNumber - 作為阻擋來源的 issue 編號(同一 repo)。 * @returns {Promise} 建立成功的相依關係物件(依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx,例如未啟用問題相依功能)由底層 `api` 丟出,錯誤附 `status`、`data`。 * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立追蹤 issue 後, * 以本函式把「PR(`ctx.prNumber`)相依於追蹤 issue」,讓 issue 完成/關閉前 PR 無法合併; * 呼叫端以 try/catch 降級(功能未啟用時記 WRN、不阻斷流程)。 */ -function addIssueDependency(ctx, issueNumber, dependency) { - return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, { - index: dependency, +function addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) { + return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, { + index: blockingIssueNumber, owner: ctx.owner, repo: ctx.repo, }); diff --git a/src/lib/review.js b/src/lib/review.js index db528f6..0095dd9 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -690,9 +690,9 @@ ${JSON.stringify(brief)} * 不使用 {@link templates.othersComment} 的單一表格——表格僅用於一般模式(PR)。 */ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { - await Promise.all( - severe.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))), - ); + for (const finding of severe) { + await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding)); + } log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`); } @@ -716,9 +716,9 @@ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { * 以本函式把警告+建議逐條留言到追蹤 issue,確保 issue 上每條問題都是可個別回覆的留言。 */ async function postOthersToIssue({ ctx, gitea, issueNumber, others }) { - await Promise.all( - others.map((finding) => gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding))), - ); + for (const finding of others) { + await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding)); + } log('步驟10', 'INF', `已將 ${others.length} 條警告+建議逐條留言到 issue #${issueNumber}。`); } diff --git a/src/lib/templates.js b/src/lib/templates.js index c970629..3393803 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -400,7 +400,7 @@ function nothingToReviewComment(ignoredCount) { * 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以 * {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。 */ -function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) { +function prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) { return `${MARK} ## 🔍 AI Code Review|已建立追蹤問題 @@ -421,5 +421,5 @@ module.exports = { issueBody, issueFindingComment, nothingToReviewComment, - issueLinkComment, + prIssueLinkComment, }; diff --git a/test/gitea.test.js b/test/gitea.test.js index f5f6469..a13bcdd 100644 --- a/test/gitea.test.js +++ b/test/gitea.test.js @@ -5,7 +5,7 @@ const test = require('node:test'); const gitea = require('../src/lib/gitea'); -function withFetchStub(handler, fn) { +function withFetchStub(handler, callback) { const originalFetch = global.fetch; const calls = []; global.fetch = async (url, options = {}) => { @@ -13,7 +13,7 @@ function withFetchStub(handler, fn) { return handler(url, options); }; return Promise.resolve() - .then(() => fn(calls)) + .then(() => callback(calls)) .finally(() => { global.fetch = originalFetch; }); diff --git a/test/review.test.js b/test/review.test.js index d49e210..e052818 100644 --- a/test/review.test.js +++ b/test/review.test.js @@ -44,7 +44,7 @@ test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => { } }); -test('postOthersToIssue 批次送出 issue 留言', async () => { +test('postOthersToIssue 依序送出 issue 留言以維持排序', async () => { const calls = []; let active = 0; let maxActive = 0; @@ -71,7 +71,7 @@ test('postOthersToIssue 批次送出 issue 留言', async () => { assert.equal(calls.length, 2); assert.equal(calls[0].issueNumber, 7); - assert.ok(maxActive > 1); + assert.equal(maxActive, 1); }); test('resultFilesToCommit 在建問題模式有嚴重問題時仍提交 findings', () => { -- 2.53.0 From d424447d1502f49538fcb3093d23b5ac7bfa16c5 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 14:39:42 +0800 Subject: [PATCH 67/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E5=9B=9E=E5=AF=AB=E6=9C=AC=E8=BC=AA=E5=B7=B2=E8=99=95=E7=90=86?= =?UTF-8?q?=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../findings/2026-07-20-15:57:46.json | 13 -- .../findings/2026-07-20-18:31:07.json | 98 ------------- .../findings/2026-07-21-14:27:42.json | 136 +----------------- 3 files changed, 1 insertion(+), 246 deletions(-) diff --git a/.gitea/ai-review/findings/2026-07-20-15:57:46.json b/.gitea/ai-review/findings/2026-07-20-15:57:46.json index 119763d..d0e003b 100644 --- a/.gitea/ai-review/findings/2026-07-20-15:57:46.json +++ b/.gitea/ai-review/findings/2026-07-20-15:57:46.json @@ -59,19 +59,6 @@ "problem": "推送流程新增 pushToken 分支,但沒有測試驗證兩套認證策略:有 PAT 時略過 origin、PAT 推送失敗不退回其他 token、無 PAT 時 origin 成功不重試、origin 失敗才用一般 token;也沒有案例保護含憑證資訊不出現在錯誤或測試輸出中。(本次已將認證改經 env 傳入並遮蔽 push 錯誤,測試仍待補。)", "suggestion": "mock git 執行器與 URL/env 組裝,補測 pushToken 有值/空、origin 成功/失敗、PAT 推送失敗及含特殊字元等案例;斷言 push 目標與呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。", "suggestedCode": "" - }, - { - "id": "F009", - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "action.yml", - "startLine": 47, - "endLine": 61, - "problem": "push-token 的用途與退回行為在區塊註解、欄位描述、required 與 default 註解中反覆說明,且單行 description 過長,資訊雖完整但重複,日後修改語意易只改到一處。", - "suggestion": "保留一段「為何需要 PAT」的必要背景,其餘讓欄位名稱、required、default 自行表意,將 description 收斂成呼叫端真正需要知道的契約。註:本專案採 doc-funcs 高密度註解慣例,是否精簡屬慣例取捨,需維護者確認。", - "suggestedCode": " # 專用推送 PAT;以 PAT 推送可重新觸發 CI。留空時沿用 token。\n push-token:\n description: '推送審查結果 commit 的 PAT(留空時沿用 token)'\n required: false\n default: ''" } ], "excluded": [] diff --git a/.gitea/ai-review/findings/2026-07-20-18:31:07.json b/.gitea/ai-review/findings/2026-07-20-18:31:07.json index 87139a2..8b6a863 100644 --- a/.gitea/ai-review/findings/2026-07-20-18:31:07.json +++ b/.gitea/ai-review/findings/2026-07-20-18:31:07.json @@ -120,20 +120,6 @@ "suggestedCode": "", "sourceIssue": 15 }, - { - "id": "F015", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/gitea.js", - "startLine": 190, - "endLine": 197, - "problem": "`dependency` 作為參數名太薄,與 `issueNumber` 並列時看不出它也是 issue 編號。讀到 `addIssueDependency(ctx, ctx.prNumber, issue.number)` 時,語意要靠上下文補拍子。", - "suggestion": "改用更完整的名稱,例如 `dependencyIssueNumber` 或 `blockingIssueNumber`,讓「誰被誰阻擋」在簽名裡就清楚成形。", - "suggestedCode": "```\nfunction addIssueDependency(ctx, issueNumber, dependencyIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {\n index: dependencyIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", - "sourceIssue": 15 - }, { "id": "F022", "reviewer": "Leo", @@ -148,34 +134,6 @@ "suggestedCode": "", "sourceIssue": 18 }, - { - "id": "F023", - "reviewer": "Mage", - "focus": "", - "badge": "🔮", - "severity": "警告", - "file": "src/index.js", - "startLine": 374, - "endLine": 387, - "problem": "建問題模式下只要有任何保留 finding 就會建立 issue,且後續一律把 PR 設為相依於該 issue;但收尾結果仍是 `severe.length === 0 ? 'success' : 'failure'`。最小情境:攻擊方只產生 1 條「建議」,`severe.length` 為 0,action commit `[success]`,但 PR 被 issue dependency 擋住無法合併。這讓「success=可通過」與「非嚴重問題也阻擋合併」兩個語義互相矛盾。", - "suggestion": "明確對齊語義:若只有嚴重問題才應阻擋合併,則只在 `severe.length > 0` 時建立 dependency;若所有保留問題都要阻擋合併,則 result/exit code 不應只看嚴重問題。", - "suggestedCode": "```\nif (severe.length > 0) {\n try {\n await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number);\n log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}。`);\n } catch (err) {\n log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`);\n }\n}\n```", - "sourceIssue": 18 - }, - { - "id": "F024", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/index.js", - "startLine": 179, - "endLine": 181, - "problem": "`issueBuffer` 的旋律太含糊:讀者會以為裡面放的是 issue,實際上暫存的是尚未送出的留言 body。命名沒有把資料形狀唱清楚。", - "suggestion": "改成能描述內容與用途的名稱,例如 `pendingIssueCommentBodies`,並同步調整註解與迴圈變數。", - "suggestedCode": "```\nconst pendingIssueCommentBodies = [];\nlet issue = null;\n```", - "sourceIssue": 18 - }, { "id": "F025", "reviewer": "Assassin", @@ -204,34 +162,6 @@ "suggestedCode": "", "sourceIssue": 19 }, - { - "id": "F027", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/index.js", - "startLine": 181, - "endLine": 185, - "problem": "`issue` 這個變數名太素,像樂譜上只寫「音符」卻不說是哪一聲部。此處承載的是建問題模式建立出的追蹤 issue,後面還會與 PR issue 編號、Gitea issue API 參數交錯出現,名稱過泛會讓閱讀節奏變濁。", - "suggestion": "改成能表明角色的名稱,例如 `trackingIssue`。對應的 `ensureIssueCreated` 也可改為 `ensureTrackingIssueCreated`,讓閉包狀態與用途一眼對上。", - "suggestedCode": "```\nconst issueBuffer = [];\nlet trackingIssue = null;\n\nconst postComment = async (body) => {\n if (ctx.createIssue) {\n if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n issueBuffer.push(body);\n return null;\n }\n const created = await gitea.createIssueComment(ctx, body);\n currentRunCommentIds.add(created.id);\n return created;\n};\n```", - "sourceIssue": 19 - }, - { - "id": "F029", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/templates.js", - "startLine": 388, - "endLine": 405, - "problem": "`issueLinkComment` 產生的是 PR 上唯一的建問題模式回貼留言,但名稱少了 PR 的聲部;同檔已有 `issueBody`、`issueFindingComment`,乍看會以為這也是 issue 內留言模板,命名層次不夠分明。", - "suggestion": "改名為 `prIssueLinkComment` 或 `trackingIssueLinkComment`,讓模板的投遞位置與用途直接寫在名稱裡,避免與 issue 內文、issue finding 留言混成一團。", - "suggestedCode": "```\nfunction prIssueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) {\n return `${MARK}\n## 🔍 AI Code Review|已建立追蹤問題\n\n本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`;\n}\n```", - "sourceIssue": 19 - }, { "id": "F031", "reviewer": "Leo", @@ -246,34 +176,6 @@ "suggestedCode": "", "sourceIssue": 21 }, - { - "id": "F032", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/index.js", - "startLine": 183, - "endLine": 224, - "problem": "`issue` 與 `issueBuffer` 的命名過於泛泛;在 Gitea 裡 PR 也是 issue,追蹤問題也是 issue,單靠 `issue` 這個名字無法唱出它究竟是哪一個聲部。", - "suggestion": "建議改成更具語義的名稱,例如 `trackingIssue`、`trackingIssueCommentBuffer`,讓讀者不用回讀 create-issue 模式的整段脈絡。", - "suggestedCode": "```\nlet trackingIssue = null;\nconst trackingIssueCommentBuffer = [];\n```", - "sourceIssue": 21 - }, - { - "id": "F033", - "reviewer": "Bard", - "focus": "", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/gitea.js", - "startLine": 190, - "endLine": 194, - "problem": "`addIssueDependency(ctx, issueNumber, dependency)` 的兩個參數名稱太相似,且 `dependency` 少了 issue 語義。這支 API 的方向性本來就容易讀錯,命名再模糊就像兩個音符共用同一個名字。", - "suggestion": "建議把參數改成能表達方向的名稱,例如 `blockedIssueNumber` 與 `blockingIssueNumber`,呼叫端也會更清楚是誰被誰擋住。", - "suggestedCode": "```\nfunction addIssueDependency(ctx, blockedIssueNumber, blockingIssueNumber) {\n return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${blockedIssueNumber}/dependencies`, {\n index: blockingIssueNumber,\n owner: ctx.owner,\n repo: ctx.repo,\n });\n}\n```", - "sourceIssue": 21 - }, { "id": "F040", "reviewer": "Assassin", diff --git a/.gitea/ai-review/findings/2026-07-21-14:27:42.json b/.gitea/ai-review/findings/2026-07-21-14:27:42.json index a6e11cb..21b40b2 100644 --- a/.gitea/ai-review/findings/2026-07-21-14:27:42.json +++ b/.gitea/ai-review/findings/2026-07-21-14:27:42.json @@ -7,141 +7,7 @@ "version": "codex-cli 0.144.6", "model": "gpt-5.5" }, - "findings": [ - { - "reviewer": "Mage", - "focus": "logic", - "badge": "🔮", - "severity": "嚴重", - "file": "src/index.js", - "startLine": 149, - "endLine": 153, - "problem": "這個變更把「本輪審查」改成即使有嚴重問題也回傳 0,並假設後續由 `[ai-review-bot][failure]` 結果 commit 觸發下一輪 CI 再失敗。最小重現:workflow 仍依 action.yml 常見用法傳入自動 token(或任一不會觸發 synchronize CI 的 token)→ AI 找到 1 條嚴重問題 → 本輪成功建立留言與 push 結果 commit,但該 push 不觸發下一輪 → 沒有任何檢查讀到 `[failure]` commit,PR 最終呈現通過。這是未驗證的外部時序假設;嚴重 finding 會被靜默放行。", - "suggestion": "不要讓失敗判定完全依賴下一輪 CI。若 `severe.length > 0`,本輪在完成留言與結果落地後仍應回傳 1;或至少提供明確 input 控制 direct-fail,並在無法驗證 token 會觸發 CI 時預設直接 fail。", - "suggestedCode": "", - "id": "F009", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。此條指控嚴重 finding 需仰賴下一輪 CI 才失敗,且自動 token 可能不觸發 CI,與既有 pushToken 測試缺口或 PAT 風險 finding 不屬同一處同一失效模式;未能排除。" - } - } - }, - { - "reviewer": "Mage", - "focus": "logic", - "badge": "🔮", - "severity": "警告", - "file": "src/lib/review.js", - "startLine": 707, - "endLine": 718, - "problem": "`postOthersToIssue` 以並行方式送出多則 issue 留言時,issue 上的實際留言順序取決於 API 回應與資料庫寫入完成順序,不保證等於已排序的 findings 順序。最小重現:兩條建議分別位於 `a.js:10` 與 `a.js:20`,第二個 API 較快完成,issue 會先出現第 20 行問題,再出現第 10 行問題;使用者逐檔逐行處理時順序會錯亂。", - "suggestion": "若 issue 留言順序是介面契約,逐則 `await gitea.createCommentOnIssue(...)` 發送;若要保留並行,需在每則留言標題加入穩定序號,例如 `2/5`,讓非同步完成不破壞閱讀順序。", - "suggestedCode": "", - "id": "F011", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。歷史 findings 未涵蓋 postOthersToIssue 並行送出導致 issue 留言順序不穩定;目前也無排除事項可套用,故保留。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "action.yml", - "startLine": 22, - "endLine": 24, - "problem": "manifest 的註解忽然奏起一整段實作細節:PAT、CI 觸發、主程式步驟 1 全擠在 input 說明旁,和同檔其他「介面用途」型註解的節奏不一致。讀者只是想知道 token 該填什麼,卻被迫聽完流程旁白。", - "suggestion": "把 action.yml 留給介面契約;細節移到 README 或主流程文件。此處可濃縮成「建議使用可觸發 CI 的 PAT」即可。", - "suggestedCode": "# 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。", - "id": "F002", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。已知排除事項與歷史 findings 主要涵蓋 push-token 說明重複或 PAT 安全風險;本條指向 action.yml 中 token input 註解混入主流程步驟細節,並非同一處同一問題,未能排除。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "readme.md", - "startLine": 40, - "endLine": 47, - "problem": "mermaid 圖的節點 ID 旋律走岔了:畫面標示是 3→4→5→...→8→2→9,但節點名稱卻用 S3、S4、S2 來回跳。即使流程語意想表達「步驟 2 延後」,節點 ID 與視覺順序不一致,會讓維護者在對照圖與文字時多繞一圈。", - "suggestion": "讓節點 ID 維持閱讀順序,將真正的流程步驟放在節點文字裡。例如用 N2、N3 或 A、B 這類中性 ID,避免 S2 看起來像應該排在 S1 後面。", - "suggestedCode": "S1 -->|未命中| N3[3 偵測 AI 工具並留言]\n N3 --> N4[4 讀 .reviewignore 整理 diff 並留言]\n N4 --> N5[5 攻擊方登場留言]\n N5 --> N6[6 攻擊方 sub agent 並行找問題]\n N6 --> N7[7 防守方登場留言]\n N7 --> N8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json]\n N8 --> N2[2 延後將舊留言標記解決(成功產生結果後才執行)]\n N2 --> S9[9 嚴重問題逐條掛行留言]", - "id": "F003", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。歷史 findings 雖有流程步驟硬編碼與文件同步成本問題,但本條聚焦 mermaid 節點 ID 與視覺流程順序不一致,屬不同可讀性指控,未命中既有排除。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "src/index.js", - "startLine": 122, - "endLine": 139, - "problem": "`main()` 的 JSDoc 從函式說明變成流程章回。步驟、例外模式、issue 建立時機、相依關係、commit 規則全塞在同一段,和程式下方已存在的分段註解重複,讀起來像同一旋律被兩把琴同時彈奏。", - "suggestion": "JSDoc 保留函式職責、回傳值與關鍵模式差異即可;完整 10 步驟流程交給 README 或下方區塊註解。這會讓 `main()` 開頭更快進入正題。", - "suggestedCode": "", - "id": "F004", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。歷史 findings 有步驟編號散落與 main() 內閉包 JSDoc 過重等問題,但本條指向 main() 函式 JSDoc 與下方流程註解重複,範圍與主張不完全相同,證據不足以排除。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/diagnostics.js", - "startLine": 45, - "endLine": 47, - "problem": "`res` 這個參數名太短促,和檔內其他 `text`、`parts`、`stderr`、`stdout` 這些直白命名相比顯得含糊。診斷工具本該讓人少猜一點,這裡卻讓讀者先猜它是哪一種 result。", - "suggestion": "改用 `result` 或 `agentResult`,讓函式簽名本身就說清楚資料來源。", - "suggestedCode": "function agentFailureDetail(agentResult) {\n const parts = [];\n const err = agentResult && agentResult.error;", - "id": "F005", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。未見已知排除事項或歷史 finding 涵蓋 diagnostics.js 中 res 參數命名過短的問題;屬新的命名可讀性指控。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "test/gitea.test.js", - "startLine": 8, - "endLine": 8, - "problem": "`fn` 是一個太倉促的縮寫,放在測試輔助函式裡尤其刺耳;同一行已有 `handler` 這種完整命名,`fn` 顯得像漏拍的音符。", - "suggestion": "改成 `callback` 或 `run`,讓呼叫意圖更清楚,也和此專案偏完整語意的命名風格一致。", - "suggestedCode": "function withFetchStub(handler, callback) {", - "id": "F006", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。未見已知排除事項或歷史 finding 涵蓋 test/gitea.test.js 中 fn 測試輔助參數命名問題;屬新的命名可讀性指控。" - } - } - } - ], + "findings": [], "excluded": [ { "reviewer": "Assassin", -- 2.53.0 From 55da29a86f5034bfc8c4acf06cbe4ee873b13608 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Tue, 21 Jul 2026 06:42:40 +0000 Subject: [PATCH 68/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 99 +++++++ .../findings/2026-07-21-14:42:39.json | 280 ++++++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-21-14:42:39.json diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index df97c7c..84b5ce1 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1791,5 +1791,104 @@ "endLine": 32, "problem": "`redactSecrets` 新增多個遮罩規則與控制字元單行化,但測試只透過 `agentFailureDetail` 間接覆蓋 Authorization 與 token 兩種格式。URL 內嵌帳密、長 token、GitHub token 樣式、控制字元注入與 null/undefined 邊界都還沒被直接驗證。", "reason": "Paladin:可排除(重複)。歷史 findings 已記錄失敗診斷與 redactSecrets 對 Authorization、URL 帳密、token 格式、控制字元、截斷與空值邊界缺少測試;本條為同一測試缺口。" + }, + { + "addedAt": "2026/07/21 14:42:39", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 179, + "endLine": 212, + "problem": "`queueOrPostComment` 與 `createIssueAndFlushBufferedComments` 這兩個閉包名稱像兩段不同旋律:一個強調「排隊或發布」,另一個卻把「建立 issue、flush 暫存留言、設定狀態」全塞進名稱與實作。讀者要來回追 `trackingIssue`、`pendingIssueCommentBodies`,節奏偏長且語意負擔重。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 main() 內新增閉包與 issue/comment 路由細節讓主流程閱讀負擔加重;本條只是改以目前函式名稱描述同一維護性問題。" + }, + { + "addedAt": "2026/07/21 14:42:39", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 165, + "endLine": 213, + "problem": "`main()` 這次把「留言目的地切換、issue 建立、暫存留言 flush、標籤挑選前置狀態」都塞進閉包與可變狀態(`pendingIssueCommentBodies`、`trackingIssue`、`currentRunCommentIds`)。半年後要改建問題模式時,維護者得同時追蹤主流程步驟、閉包副作用與留言落點,任何新增留言點都可能忘記處理 queue/flush 或 PR 排除清單,長期會讓流程編排變得很脆弱。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。已知與歷史 findings 已涵蓋 main() 內留言目的地、issue 狀態、緩衝佇列與可變閉包耦合,及應抽出發布器/sink 的方向。" + }, + { + "addedAt": "2026/07/21 14:42:39", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 302, + "endLine": 353, + "problem": "建問題模式的收束邏輯把「挑標籤、建立 issue、發 finding、回貼 PR、設定 issue dependency、決定是否阻擋合併」全部展開在 `main()`。這段和前面的 `createIssueAndFlushBufferedComments` 共同組成一條隱性的 issue workflow,但邊界沒有被封裝;未來要新增 issue 模板、改排序、改阻擋條件或重試策略時,會在主流程裡到處補條件,維護成本會快速上升。", + "reason": "Paladin:可排除(重複)。此條與 F006 及歷史 findings 同樣指向建問題模式在 main() 中承擔 issue workflow、發布分流與狀態管理,屬同一維護性問題的延伸描述。" + }, + { + "addedAt": "2026/07/21 14:42:39", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "readme.md", + "startLine": 56, + "endLine": 132, + "problem": "README 內大量維護到原始碼行號的連結,這次 diff 只是分支與行號位移就需要同步改一整片表格。這種文件和程式碼行號的硬耦合很容易在後續修改時過期,讀者點到錯誤位置,維護者也要花時間做機械式同步。", + "reason": "Paladin:可排除(重複)。歷史 findings 已多次指出 README 功能表硬編分支與行號連結,導致文件與原始碼高度耦合且容易失準。" + }, + { + "addedAt": "2026/07/21 14:42:39", + "prNumber": 6, + "reviewer": "Mage", + "severity": "警告", + "file": "src/index.js", + "startLine": 251, + "endLine": 253, + "problem": "無可審查變更的分支在一般模式下會先把舊留言標為過時,之後才保存 findings 與 commit/push 結果。最小重現:`files.length === 0`、`nothingToReviewComment` 成功、`resolveOldComments` 成功,但接著 `saveFindings` 因檔案系統錯誤或 `commitFindings` 因 push 失敗拋例外。此時舊審查結果已被標過時,但沒有成功落地本回合的 success 結果 commit,狀態會停在半更新。", + "reason": "Paladin:可排除(命中已知排除事項)。維護者已裁示舊留言應刻意在本回合結果發布前標為過時;本條要求延後清理,落在同一已排除範圍。" + }, + { + "addedAt": "2026/07/21 14:42:39", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 160, + "endLine": 353, + "problem": "建問題模式的主流程被大幅改寫,但目前測試只驗證了少數 helper,沒有驗證「留言先暫存、確定有 kept finding 才建 issue、無保留問題時靜默通過、嚴重問題才加 issue dependency、最後回貼 PR 連結」這些新增流程。這些行為一旦順序或條件寫錯,測試不會擋下來。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式 main() 層級流程缺測試,包括暫存留言、有 finding 才建 issue、無 finding 靜默、PR 回貼、嚴重與非嚴重分流及 dependency 失敗降級。" + }, + { + "addedAt": "2026/07/21 14:42:39", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 307, + "endLine": 361, + "problem": "`pushWithCredential` 新增了關鍵認證行為,但沒有測試驗證它真的用 `GIT_CONFIG_*` 清掉 checkout 的 extraheader、再注入 PAT Authorization,也沒有測試遠端 URL 不符與 push 失敗時不洩漏 URL/token 的失敗路徑。這段是結果 commit 能否觸發下一輪 CI 的核心,沒有測試等於這個新契約還沒通過試煉。", + "reason": "Paladin:可排除(重複)。歷史 findings 已記錄 pushWithCredential/pushToken 認證推送路徑缺少測試,包含 token 不進 argv、認證遮蔽、推送目標與失敗訊息不洩漏敏感資訊。" + }, + { + "addedAt": "2026/07/21 14:42:39", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 130, + "endLine": 192, + "problem": "`resolveMergeBase` 新增多段 fetch fallback:先抓 base、再 deepen base、deepen PR HEAD、最後 shallow repo 才 unshallow,但現有測試只驗證不安全 baseRef 會被拒絕,沒有驗證淺層歷史補抓順序、每次 fetch 後會重試 merge-base、成功後會短路,也沒有覆蓋所有策略失敗時的診斷錯誤。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、成功短路、降級順序與最終失敗診斷缺測試提出相同問題。" + }, + { + "addedAt": "2026/07/21 14:42:39", + "prNumber": 6, + "reviewer": "Maya", + "severity": "建議", + "file": "src/lib/diagnostics.js", + "startLine": 18, + "endLine": 69, + "problem": "`redactSecrets` 與 `agentFailureDetail` 是新加入的安全診斷防線,但測試只覆蓋 Authorization/token 的基本遮罩。控制字元單行化、URL 內嵌帳密、長 token/hex、輸入與輸出長度截斷、以及沒有 error code 時的 fallback 訊息都還沒被驗證。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecrets/agentFailureDetail 對控制字元、URL 帳密、token 格式、截斷與 fallback 診斷等邊界缺少測試。" } ] diff --git a/.gitea/ai-review/findings/2026-07-21-14:42:39.json b/.gitea/ai-review/findings/2026-07-21-14:42:39.json new file mode 100644 index 0000000..cf27a27 --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-21-14:42:39.json @@ -0,0 +1,280 @@ +{ + "generatedAt": "2026/07/21 14:42:39", + "commitSha": "d424447d1502f49538fcb3093d23b5ac7bfa16c5", + "prNumber": 6, + "tool": { + "name": "codex", + "version": "codex-cli 0.144.6", + "model": "gpt-5.5" + }, + "findings": [ + { + "reviewer": "Assassin", + "focus": "security", + "badge": "🗡️", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 344, + "endLine": 348, + "problem": "建問題模式在 `addIssueDependency` 失敗時只記錄警告,接著本輪仍會回傳 0;若 `commitFindings` 後續因沒有實際 diff 可提交而沒有產生 `[failure]` 結果 commit,攻擊者只要讓嚴重 finding 被搬到追蹤 issue,且目標 Gitea 未啟用 issue dependencies 或 token 權限不足,就會 fail-open:PR 既沒有相依阻擋,也沒有失敗檢查阻擋合併。", + "suggestion": "有嚴重問題時,相依關係設定失敗應視為阻擋條件:要嘛直接回傳 1,要嘛確認 failure 結果 commit 已成功產生後才允許本輪回傳 0。不要把阻擋機制失效降級成純警告。", + "suggestedCode": "", + "id": "F001", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。此條不是單純重複既有「相依 API 失敗降級缺測試」,而是指控嚴重 finding 搬到 issue 後,dependency 失敗可能使阻擋機制失效;已知排除事項未涵蓋此安全語義。" + } + } + }, + { + "reviewer": "Mage", + "focus": "logic", + "badge": "🔮", + "severity": "警告", + "file": "src/index.js", + "startLine": 330, + "endLine": 338, + "problem": "在建問題模式下,這段新增的 PR 回貼 issue 連結會在每次審查有保留問題時都新增一則 PR 留言,但同一流程前面明確跳過 `resolveOldComments`(建問題模式不清理 PR 舊留言)。最小重現:PR 第一次審查建立 issue #10 並在 PR 留連結;後續推新 commit 再跑一次,建立 issue #11 並再留一則連結。PR 上會同時存在 #10 與 #11,舊 issue 可能已過時,讀者無法判斷哪個才是目前審查結果。", + "suggestion": "建問題模式也應對本 action 先前的 PR 連結留言做過時標記,或在新增連結前查找並更新既有連結留言。若要避免碰觸 issue 內的審查內容,清理範圍可限制在 PR 上含 `MARK` 且標題為「已建立追蹤問題」的留言。", + "suggestedCode": "", + "id": "F009", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。已知排除事項只裁示舊審查留言在本輪結果前標過時的時機;本條指控建問題模式跳過 PR 舊連結留言清理,導致多個追蹤 issue 連結並存,未被既有排除涵蓋。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/diagnostics.js", + "startLine": 55, + "endLine": 63, + "problem": "`agentFailureDetail` 裡的 `stderr` 與 `stdout` 區塊幾乎同譜重奏:取值、slice、redact、判斷、push 只差欄位名。這種重複雖小,卻讓後續若要調整遮罩或長度時容易改一半走調。", + "suggestion": "建議抽出小 helper,例如 `appendRedactedOutput(parts, label, value)`,讓 stderr/stdout 共用同一段處理節奏。", + "suggestedCode": "", + "id": "F005", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。此條針對 diagnostics.js 中 stderr/stdout 處理重複的維護性問題,歷史 findings 主要是安全診斷缺測試與外洩風險,並非同一指控。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/gitrepo.js", + "startLine": 139, + "endLine": 160, + "problem": "`runFetch`、`tryMergeBase`、`firstError`、`strategies` 夾在 `resolveMergeBase` 主旋律中,使這個函式同時負責驗證、fetch 策略編排、診斷文字組裝與錯誤包裝。即使邏輯可行,閱讀節奏已偏密,維護者很難一眼分辨「主要流程」與「補救策略」。", + "suggestion": "建議將 fetch 策略與診斷收集抽成小函式,例如 `fetchAndRecord`、`resolveMergeBaseWithStrategies`,讓 `resolveMergeBase` 保留高階流程:驗證 baseRef → fetch base → 嘗試 merge-base → 補抓歷史。", + "suggestedCode": "", + "id": "F003", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。歷史 findings 主要涵蓋 resolveMergeBase 缺測試與診斷不足,本條指向函式內策略編排、診斷組裝與錯誤包裝混雜的可維護性問題,未被既有排除事項完整涵蓋。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/gitrepo.js", + "startLine": 317, + "endLine": 346, + "problem": "`pushWithCredential` 的 JSDoc 已經很完整,但正文註解再次長篇解釋 checkout token、PAT、extraheader 清空等細節;文件與程式內註解重複奏同一段旋律,反而稀釋真正需要看的程式碼。", + "suggestion": "保留 JSDoc 的背景說明,函式內註解縮成操作提示即可,例如只說明「先清空 checkout extraheader,再注入本次 PAT header」。", + "suggestedCode": "", + "id": "F004", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。既有排除事項雖有 push-token manifest 說明重複,但未涵蓋 pushWithCredential 函式內 JSDoc 與正文註解重複;證據不足以判定為重複或誤報。" + } + } + } + ], + "excluded": [ + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 179, + "endLine": 212, + "problem": "`queueOrPostComment` 與 `createIssueAndFlushBufferedComments` 這兩個閉包名稱像兩段不同旋律:一個強調「排隊或發布」,另一個卻把「建立 issue、flush 暫存留言、設定狀態」全塞進名稱與實作。讀者要來回追 `trackingIssue`、`pendingIssueCommentBodies`,節奏偏長且語意負擔重。", + "suggestion": "建議統一命名語彙,例如把「暫存/發布」集中成 `postReviewComment`、`flushPendingIssueComments`,讓函式名稱只描述一件事;建立 issue 與 flush 留言也可拆成兩段,讀起來會更像樂句而不是長句。", + "suggestedCode": "", + "id": "F002", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 finding 已指出 main() 內新增閉包與 issue/comment 路由細節讓主流程閱讀負擔加重;本條只是改以目前函式名稱描述同一維護性問題。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 165, + "endLine": 213, + "problem": "`main()` 這次把「留言目的地切換、issue 建立、暫存留言 flush、標籤挑選前置狀態」都塞進閉包與可變狀態(`pendingIssueCommentBodies`、`trackingIssue`、`currentRunCommentIds`)。半年後要改建問題模式時,維護者得同時追蹤主流程步驟、閉包副作用與留言落點,任何新增留言點都可能忘記處理 queue/flush 或 PR 排除清單,長期會讓流程編排變得很脆弱。", + "suggestion": "把留言目的地抽成明確的小型物件或模組,例如 `ReviewCommentSink` / `IssueCommentSink`,由它負責 `post()`、`flush()`、`currentRunCommentIds`。`main()` 只保留流程順序,不直接管理留言暫存與 issue 狀態。", + "suggestedCode": "", + "id": "F006", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(命中已知排除事項且重複)。已知與歷史 findings 已涵蓋 main() 內留言目的地、issue 狀態、緩衝佇列與可變閉包耦合,及應抽出發布器/sink 的方向。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 302, + "endLine": 353, + "problem": "建問題模式的收束邏輯把「挑標籤、建立 issue、發 finding、回貼 PR、設定 issue dependency、決定是否阻擋合併」全部展開在 `main()`。這段和前面的 `createIssueAndFlushBufferedComments` 共同組成一條隱性的 issue workflow,但邊界沒有被封裝;未來要新增 issue 模板、改排序、改阻擋條件或重試策略時,會在主流程裡到處補條件,維護成本會快速上升。", + "suggestion": "把建問題模式整理成單一高階函式,例如 `review.publishIssueModeResults({ ctx, gitea, tool, model, cwd, kept, severe, others, bufferedComments })`,主流程只接收 `trackingIssue` / `filesToCommit` 等結果。這樣 PR 模式與 issue 模式的責任邊界會清楚很多。", + "suggestedCode": "", + "id": "F007", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。此條與 F006 及歷史 findings 同樣指向建問題模式在 main() 中承擔 issue workflow、發布分流與狀態管理,屬同一維護性問題的延伸描述。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "建議", + "file": "readme.md", + "startLine": 56, + "endLine": 132, + "problem": "README 內大量維護到原始碼行號的連結,這次 diff 只是分支與行號位移就需要同步改一整片表格。這種文件和程式碼行號的硬耦合很容易在後續修改時過期,讀者點到錯誤位置,維護者也要花時間做機械式同步。", + "suggestion": "若這份表格是 API 索引,建議改成自動產生,或至少移除 `#Lxx` 行號錨點,改連到檔案或穩定章節錨點。讓文件描述 API,而不是跟著每次程式碼行號漂移。", + "suggestedCode": "", + "id": "F008", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已多次指出 README 功能表硬編分支與行號連結,導致文件與原始碼高度耦合且容易失準。" + } + } + }, + { + "reviewer": "Mage", + "focus": "logic", + "badge": "🔮", + "severity": "警告", + "file": "src/index.js", + "startLine": 251, + "endLine": 253, + "problem": "無可審查變更的分支在一般模式下會先把舊留言標為過時,之後才保存 findings 與 commit/push 結果。最小重現:`files.length === 0`、`nothingToReviewComment` 成功、`resolveOldComments` 成功,但接著 `saveFindings` 因檔案系統錯誤或 `commitFindings` 因 push 失敗拋例外。此時舊審查結果已被標過時,但沒有成功落地本回合的 success 結果 commit,狀態會停在半更新。", + "suggestion": "把 `review.resolveOldComments` 延後到 `saveFindings` 與必要的 `commitFindings` 成功之後;或至少在空變更路徑也遵守同一個交易順序:先產生並提交本回合結果,再清理舊留言。", + "suggestedCode": "", + "id": "F010", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(命中已知排除事項)。維護者已裁示舊留言應刻意在本回合結果發布前標為過時;本條要求延後清理,落在同一已排除範圍。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/index.js", + "startLine": 160, + "endLine": 353, + "problem": "建問題模式的主流程被大幅改寫,但目前測試只驗證了少數 helper,沒有驗證「留言先暫存、確定有 kept finding 才建 issue、無保留問題時靜默通過、嚴重問題才加 issue dependency、最後回貼 PR 連結」這些新增流程。這些行為一旦順序或條件寫錯,測試不會擋下來。", + "suggestion": "請補 `main()` 層級的流程測試,stub `agents`、`review`、`gitea`、`gitrepo`,至少覆蓋:`createIssue=true && kept=[]` 不建立 issue/不貼 PR 留言;`kept` 只有警告/建議時建立 issue、flush 暫存留言、回貼 PR 連結但不呼叫 `addIssueDependency`;`kept` 含嚴重時呼叫 `addIssueDependency`,且 dependency 失敗時流程仍完成並 commit failure findings。", + "suggestedCode": "", + "id": "F011", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式 main() 層級流程缺測試,包括暫存留言、有 finding 才建 issue、無 finding 靜默、PR 回貼、嚴重與非嚴重分流及 dependency 失敗降級。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 307, + "endLine": 361, + "problem": "`pushWithCredential` 新增了關鍵認證行為,但沒有測試驗證它真的用 `GIT_CONFIG_*` 清掉 checkout 的 extraheader、再注入 PAT Authorization,也沒有測試遠端 URL 不符與 push 失敗時不洩漏 URL/token 的失敗路徑。這段是結果 commit 能否觸發下一輪 CI 的核心,沒有測試等於這個新契約還沒通過試煉。", + "suggestion": "請補推送行為測試。可用可注入的 exec wrapper 或子程序測試方式驗證:`git push` argv 不含 token、env 包含兩筆同 scope extraheader、`GIT_CONFIG_VALUE_0` 為空值、`GIT_CONFIG_VALUE_1` 為 Basic header;再補 `remote.origin !== server.origin` 與 exec 失敗時錯誤訊息不含 token/remoteUrl 的案例。", + "suggestedCode": "", + "id": "F012", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已記錄 pushWithCredential/pushToken 認證推送路徑缺少測試,包含 token 不進 argv、認證遮蔽、推送目標與失敗訊息不洩漏敏感資訊。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 130, + "endLine": 192, + "problem": "`resolveMergeBase` 新增多段 fetch fallback:先抓 base、再 deepen base、deepen PR HEAD、最後 shallow repo 才 unshallow,但現有測試只驗證不安全 baseRef 會被拒絕,沒有驗證淺層歷史補抓順序、每次 fetch 後會重試 merge-base、成功後會短路,也沒有覆蓋所有策略失敗時的診斷錯誤。", + "suggestion": "請補針對 `resolveMergeBase` 的失敗與邊界測試。建議把 git 執行函式抽成可替換依賴,或用臨時 git repo 模擬 shallow 狀態,斷言:首次 merge-base 成功不走 fallback;deepen base 成功後立即回傳;deepen PR HEAD 使用目前 HEAD SHA 而不是遠端 `HEAD`;非 shallow repo 不呼叫 `--unshallow`;全部失敗時錯誤包含策略診斷。", + "suggestedCode": "", + "id": "F013", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、成功短路、降級順序與最終失敗診斷缺測試提出相同問題。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "建議", + "file": "src/lib/diagnostics.js", + "startLine": 18, + "endLine": 69, + "problem": "`redactSecrets` 與 `agentFailureDetail` 是新加入的安全診斷防線,但測試只覆蓋 Authorization/token 的基本遮罩。控制字元單行化、URL 內嵌帳密、長 token/hex、輸入與輸出長度截斷、以及沒有 error code 時的 fallback 訊息都還沒被驗證。", + "suggestion": "請補診斷邊界測試,至少包含:含換行與 tab 的輸出不會產生多行 log;`https://user:pass@host` 會被遮罩;40 字元以上 token 會被遮罩;debug 模式下 stderr/stdout 只輸出到限制長度;沒有 `error.code`、`signal`、`stderr/stdout` 時仍回傳安全的 fallback 訊息。", + "suggestedCode": "", + "id": "F014", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已涵蓋 redactSecrets/agentFailureDetail 對控制字元、URL 帳密、token 格式、截斷與 fallback 診斷等邊界缺少測試。" + } + } + } + ] +} -- 2.53.0 From aaf2e484d5717ca20eecad861f16386e06597850 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 15:57:53 +0800 Subject: [PATCH 69/79] =?UTF-8?q?fix(ai-review):=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=BB=BA=E5=95=8F=E9=A1=8C=E6=A8=A1=E5=BC=8F=E9=98=BB=E6=93=8B?= =?UTF-8?q?=E5=A4=B1=E6=95=88=E8=88=87=E9=80=A3=E7=B5=90=E7=B4=AF=E7=A9=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 59 +++++++++++++++++++++++++++++++++------------ src/lib/review.js | 35 +++++++++++++++++++++++++++ test/review.test.js | 37 ++++++++++++++++++++++++++++ 3 files changed, 116 insertions(+), 15 deletions(-) diff --git a/src/index.js b/src/index.js index 1b63e3b..b750e16 100644 --- a/src/index.js +++ b/src/index.js @@ -72,7 +72,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { * 供下一回合 `main()` 步驟 1 比對辨識、直接回報結果而不重複審查。 * 依 `commitAndPushFindings` 的回傳值記錄不同日誌:true=已 commit/push; * false=檔案無實際變更(空 commit 防護),記「略過 commit/push」。 - * commit/push 失敗(例如與開發者新 commit 競態)時僅記 WRN log,不拋出例外、不改變審查結果。 + * commit/push 失敗(例如與開發者新 commit 競態)時僅記 WRN log,不拋出例外;呼叫端可依回傳值決定是否阻擋。 * * @param {Object} params - 解構參數。 * @param {string} params.cwd - repo 根目錄(workspace)絕對路徑,git 操作在此目錄執行。 @@ -84,7 +84,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { * @param {string} params.ctx.repository - `owner/repo` 形式的 repo 名稱。 * @param {string[]} params.files - 要 commit 的檔案 repo 相對路徑陣列(如 findings 檔、`.gitea/ai-review/exclusions.json`);全數無變更時只記 INF 略過。 * @param {'success'|'failure'} params.result - 本回合審查結果:success=無嚴重問題、failure=有嚴重問題;會拼進 commit 訊息尾端。 - * @returns {void} 無回傳值;成敗僅反映在 log 上。 + * @returns {boolean} true=已 commit/push;false=無變更或 commit/push 失敗。 * @remarks * 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、 * 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json; @@ -107,12 +107,15 @@ function commitFindings({ cwd, ctx, files, result }) { }); if (committed) { log('收尾', 'INF', `審查結果檔已 commit 並 push 回 ${ctx.headRef}(結果:${result})。`); + return true; } else { log('收尾', 'INF', '審查結果檔無實際變更,略過 commit/push。'); + return false; } } catch (err) { - // push 失敗(例如與開發者新 commit 競態)時只記錄,不改變審查結果。 + // push 失敗(例如與開發者新 commit 競態)時只記錄,交由呼叫端依嚴重度決定是否阻擋。 log('收尾', 'WRN', `commit/push 審查結果檔失敗:${err.message}。`); + return false; } } @@ -165,6 +168,7 @@ async function main() { // 建問題模式:追蹤 issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色) // 先暫存於 pendingIssueCommentBodies,建立 issue 後一次寫入。 const pendingIssueCommentBodies = []; + let issueModeActive = ctx.createIssue; let trackingIssue = null; /** * 發布一則審查留言。依模式決定去向: @@ -179,7 +183,7 @@ async function main() { * 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。 */ const queueOrPostComment = async (body) => { - if (ctx.createIssue) { + if (issueModeActive) { if (trackingIssue) return gitea.createCommentOnIssue(ctx, trackingIssue.number, body); pendingIssueCommentBodies.push(body); return null; @@ -210,13 +214,27 @@ async function main() { } pendingIssueCommentBodies.length = 0; }; + /** + * 建問題模式降級:追蹤 issue 無法建立或寫入時,改把已暫存的情境留言發回 PR,後續沿用一般模式。 + * + * @returns {Promise} 無回傳值;會關閉建問題模式並把 PR 留言 id 登錄到 `currentRunCommentIds`。 + */ + const fallbackToPrComments = async () => { + issueModeActive = false; + trackingIssue = null; + for (const body of pendingIssueCommentBodies) { + const created = await gitea.createIssueComment(ctx, body); + currentRunCommentIds.add(created.id); + } + pendingIssueCommentBodies.length = 0; + }; // ── 步驟 2:延後執行 ─────────────────────────────────────────────────── // 「將 PR 既有留言標記為解決」原本在此執行,但若工具偵測/diff/攻防裁決任一失敗, // 舊結果會先被清掉卻沒有新結果。故延後到「本回合審查已成功產生結果、發布問題留言前」 // 才呼叫 review.resolveOldComments(見下方步驟 4 空變更路徑與步驟 9 前); // 屆時本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時。 - // 建問題模式全程不觸碰 PR 既有留言(審查內容改發到 issue)。 + // 建問題模式不清理 PR 既有審查內容,只在收束時標記舊追蹤 issue 連結。 // ── 步驟 3:偵測 AI agent 工具並留言 ────────────────────────────────── const tool = agents.detectTool(); @@ -248,7 +266,7 @@ async function main() { if (files.length === 0) { // 沒有可審查的變更:保存空 findings、以 success 收場。 // 一般模式在 PR 留言告知;建問題模式靜默通過(不建 issue、PR 也不留言,暫存的情境留言捨棄)。 - if (ctx.createIssue) { + if (issueModeActive) { log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。'); } else { await queueOrPostComment(templates.nothingToReviewComment(ignoredCount)); @@ -256,7 +274,7 @@ async function main() { await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); } const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] }); - if (ctx.createIssue) { + if (issueModeActive) { // 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。 log('收尾', 'INF', '建問題模式且無可審查變更,略過 commit/push。'); } else { @@ -299,7 +317,7 @@ async function main() { // ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入; // 無保留問題則不建 issue、PR 也完全不留言(靜默通過,暫存的情境留言捨棄)。 ────── - if (ctx.createIssue) { + if (issueModeActive) { if (kept.length > 0) { // 先依保留問題挑好標籤,於建立 issue 時一次帶入(省去「先建空標籤 issue 再補掛」的多餘 API 往返); // 標籤挑選失敗一律降級為不掛標籤,不阻斷建 issue 流程。 @@ -318,7 +336,12 @@ async function main() { } catch (err) { log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`); } - await createIssueAndFlushBufferedComments(labelIds); + try { + await createIssueAndFlushBufferedComments(labelIds); + } catch (err) { + log('建問題', 'WRN', `建立或寫入追蹤 issue 失敗(${err.message}),改用 PR 留言與 findings 檔流程。`); + await fallbackToPrComments(); + } } else { // 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。 log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。'); @@ -329,13 +352,13 @@ async function main() { // 延後到此可避免工具偵測/diff/攻防裁決任一失敗時舊結果先被清掉卻無新結果; // 本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時; // 嚴重/其他問題留言於本步驟之後才發布,同樣不受影響。 - if (!ctx.createIssue) { + if (!issueModeActive) { await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); } // ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─ if (severe.length > 0) { - if (ctx.createIssue) { + if (issueModeActive && trackingIssue) { await review.postSevereToIssue({ ctx, gitea, issueNumber: trackingIssue.number, severe }); } else { await review.postSevereComments({ ctx, gitea, severe, cwd }); @@ -345,7 +368,7 @@ async function main() { // ── 步驟 10:警告+建議——一般模式彙整為單一表格留言到 PR; // 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ── if (others.length > 0) { - if (ctx.createIssue) { + if (issueModeActive && trackingIssue) { await review.postOthersToIssue({ ctx, gitea, issueNumber: trackingIssue.number, others }); } else { await queueOrPostComment(templates.othersComment(others)); @@ -355,7 +378,8 @@ async function main() { // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─ // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。 - if (ctx.createIssue && trackingIssue) { + if (issueModeActive && trackingIssue) { + await review.resolveOldIssueLinkComments({ ctx, gitea }); await gitea.createIssueComment( ctx, templates.prIssueLinkComment({ @@ -385,13 +409,14 @@ async function main() { // 若有嚴重問題,仍 commit findings 檔產生 [failure] 結果 commit,避免相依 API 不支援時 fail-open。 const result = severe.length === 0 ? 'success' : 'failure'; const filesToCommit = review.resultFilesToCommit({ - createIssue: ctx.createIssue, + createIssue: issueModeActive, severeCount: severe.length, relativePath, exclusionsChanged, }); + let resultCommitted = false; if (filesToCommit.length > 0) { - commitFindings({ cwd, ctx, files: filesToCommit, result }); + resultCommitted = commitFindings({ cwd, ctx, files: filesToCommit, result }); } else { log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。'); } @@ -399,6 +424,10 @@ async function main() { // 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果 // 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。) if (result === 'failure') { + if (!resultCommitted) { + log('收尾', 'ERR', '本輪有嚴重問題,但未成功產生 [failure] 結果 commit;直接回報失敗避免 fail-open。'); + return 1; + } log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。'); } return 0; diff --git a/src/lib/review.js b/src/lib/review.js index 0095dd9..2c0af8f 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -832,6 +832,40 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) { } } +/** + * 建問題模式:只將 PR 上舊的「追蹤問題連結」留言標註為過時,不觸碰 issue 內審查內容。 + * + * @param {Object} params - 解構參數。 + * @param {Object} params.ctx - 執行環境 context(`loadContext()` 產出)。 + * @param {Object} params.gitea - Gitea API 模組,需提供 `whoAmI`、`listIssueComments`、`editIssueComment`。 + * @returns {Promise} 無回傳值;失敗時只記 WRN,不阻斷主流程。 + */ +async function resolveOldIssueLinkComments({ ctx, gitea }) { + let botLogin = ''; + try { + botLogin = (await gitea.whoAmI(ctx)).login || ''; + } catch (err) { + log('建問題', 'WRN', `無法取得 bot 身分(${err.message}),略過舊追蹤連結標註。`); + return; + } + + try { + const comments = await gitea.listIssueComments(ctx); + let outdatedCount = 0; + for (const comment of comments) { + const isBot = comment.user && comment.user.login === botLogin; + const body = typeof comment.body === 'string' ? comment.body : ''; + const isIssueLink = body.includes(templates.MARK) && body.includes('## 🔍 AI Code Review|已建立追蹤問題'); + if (!isBot || !isIssueLink || body.startsWith(templates.OUTDATED_PREFIX)) continue; + await gitea.editIssueComment(ctx, comment.id, `${templates.OUTDATED_PREFIX}${body}`); + outdatedCount += 1; + } + log('建問題', 'INF', `舊追蹤 issue 連結已標註〔已過時〕:${outdatedCount} 則。`); + } catch (err) { + log('建問題', 'WRN', `標註舊追蹤 issue 連結失敗:${err.message}。`); + } +} + /** * 步驟 9:嚴重問題逐條掛在 PR 程式碼行上留言(建立 code review); * 建立 review 失敗時降級為一般留言逐條發布(留言內補上檔案與行號位置)。 @@ -884,5 +918,6 @@ module.exports = { postSevereToIssue, postOthersToIssue, resolveOldComments, + resolveOldIssueLinkComments, postSevereComments, }; diff --git a/test/review.test.js b/test/review.test.js index e052818..8c97f9e 100644 --- a/test/review.test.js +++ b/test/review.test.js @@ -97,3 +97,40 @@ test('resultFilesToCommit 在建問題模式無嚴重問題時只提交 exclusio ['.gitea/ai-review/exclusions.json'], ); }); + +test('resolveOldIssueLinkComments 只標註舊追蹤 issue 連結', async () => { + const edited = []; + const fakeGitea = { + async whoAmI() { + return { login: 'bot' }; + }, + async listIssueComments() { + return [ + { + id: 1, + user: { login: 'bot' }, + body: '\n## 🔍 AI Code Review|已建立追蹤問題\nold', + }, + { + id: 2, + user: { login: 'bot' }, + body: '\n## 📋 變更摘要(送審 git diff)\nkeep', + }, + { + id: 3, + user: { login: 'someone' }, + body: '\n## 🔍 AI Code Review|已建立追蹤問題\nkeep', + }, + ]; + }, + async editIssueComment(ctx, commentId, body) { + edited.push({ ctx, commentId, body }); + }, + }; + + await review.resolveOldIssueLinkComments({ ctx: { token: 'hidden' }, gitea: fakeGitea }); + + assert.equal(edited.length, 1); + assert.equal(edited[0].commentId, 1); + assert.match(edited[0].body, /^> 〔已過時〕/); +}); -- 2.53.0 From 75ca8cdb834ab588d7867ce6ea05d9c8dba35340 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 15:57:53 +0800 Subject: [PATCH 70/79] =?UTF-8?q?refactor(ai-review=20=E8=A8=BA=E6=96=B7):?= =?UTF-8?q?=20=E6=94=B6=E6=96=82=E5=A4=B1=E6=95=97=E8=BC=B8=E5=87=BA?= =?UTF-8?q?=E8=99=95=E7=90=86=E8=88=87=E6=8E=A8=E9=80=81=E8=A8=BB=E8=A7=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/diagnostics.js | 19 +++++++++++++++---- src/lib/gitrepo.js | 5 ++--- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/src/lib/diagnostics.js b/src/lib/diagnostics.js index d39e9ee..99c7862 100644 --- a/src/lib/diagnostics.js +++ b/src/lib/diagnostics.js @@ -29,6 +29,19 @@ function redactSecrets(text) { .trim(); } +/** + * 將 AI CLI 失敗輸出整理成單行、遮罩且限長的診斷片段。 + * + * @param {string[]} parts - 要附加診斷片段的陣列。 + * @param {string} label - 診斷欄位名稱(如 stderr/stdout)。 + * @param {*} value - 原始診斷輸出。 + * @returns {void} + */ +function appendRedactedOutput(parts, label, value) { + const redacted = redactSecrets(String(value || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); + if (redacted) parts.push(`${label}:${redacted.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); +} + /** * 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號為主,原始輸出預設隱藏。 * @@ -52,10 +65,8 @@ function agentFailureDetail(agentResult) { } // 失敗輸出可能含 token 或 PII,預設不寫入長期 CI log;debug 模式才輸出遮罩後片段。 if (process.env.ACTIONS_STEP_DEBUG === 'true') { - const stderr = redactSecrets(String((agentResult && agentResult.stderr) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); - if (stderr) parts.push(`stderr:${stderr.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); - const stdout = redactSecrets(String((agentResult && agentResult.output) || '').slice(0, AGENT_DIAGNOSTIC_INPUT_LIMIT)); - if (stdout) parts.push(`stdout:${stdout.slice(0, AGENT_DIAGNOSTIC_OUTPUT_LIMIT)}`); + appendRedactedOutput(parts, 'stderr', agentResult && agentResult.stderr); + appendRedactedOutput(parts, 'stdout', agentResult && agentResult.output); } if (parts.length === 0) { parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index a9278a3..cd67754 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -300,8 +300,7 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s ); const refspec = `HEAD:refs/heads/${headRef}`; const remoteUrl = `${serverUrl}/${repository}.git`; - // 一律以 token 的身分明確認證推送(不走 origin 的自動 token)——只要 token 是能觸發 CI 的 PAT, - // 結果 commit 就會讓 PR 的 synchronize 事件再觸發 CI,由步驟 1 快速回報把結果蓋到新 head。 + // 一律以 token 的身分明確認證推送,不沿用 origin 的自動 token。 pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl); return true; } @@ -348,7 +347,7 @@ function pushWithCredential(cwd, remoteUrl, token, refspec, serverUrl) { env: { ...process.env, GIT_TERMINAL_PROMPT: '0', - // 兩筆同 scope 設定:先空值清掉 checkout 的自動 token,再注入 PAT 的 Authorization。 + // 先清空 checkout extraheader,再注入本次 PAT header。 GIT_CONFIG_COUNT: '2', GIT_CONFIG_KEY_0: headerScope, GIT_CONFIG_VALUE_0: '', -- 2.53.0 From 216bc39255ec176a0f66f2b257beea5929ef4eb8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 15:57:53 +0800 Subject: [PATCH 71/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E5=9B=9E=E5=AF=AB=E6=9C=AC=E8=BC=AA=E5=B7=B2=E8=99=95=E7=90=86?= =?UTF-8?q?=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../findings/2026-07-21-14:42:39.json | 76 ------------------- 1 file changed, 76 deletions(-) diff --git a/.gitea/ai-review/findings/2026-07-21-14:42:39.json b/.gitea/ai-review/findings/2026-07-21-14:42:39.json index cf27a27..24fa402 100644 --- a/.gitea/ai-review/findings/2026-07-21-14:42:39.json +++ b/.gitea/ai-review/findings/2026-07-21-14:42:39.json @@ -8,63 +8,6 @@ "model": "gpt-5.5" }, "findings": [ - { - "reviewer": "Assassin", - "focus": "security", - "badge": "🗡️", - "severity": "嚴重", - "file": "src/index.js", - "startLine": 344, - "endLine": 348, - "problem": "建問題模式在 `addIssueDependency` 失敗時只記錄警告,接著本輪仍會回傳 0;若 `commitFindings` 後續因沒有實際 diff 可提交而沒有產生 `[failure]` 結果 commit,攻擊者只要讓嚴重 finding 被搬到追蹤 issue,且目標 Gitea 未啟用 issue dependencies 或 token 權限不足,就會 fail-open:PR 既沒有相依阻擋,也沒有失敗檢查阻擋合併。", - "suggestion": "有嚴重問題時,相依關係設定失敗應視為阻擋條件:要嘛直接回傳 1,要嘛確認 failure 結果 commit 已成功產生後才允許本輪回傳 0。不要把阻擋機制失效降級成純警告。", - "suggestedCode": "", - "id": "F001", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。此條不是單純重複既有「相依 API 失敗降級缺測試」,而是指控嚴重 finding 搬到 issue 後,dependency 失敗可能使阻擋機制失效;已知排除事項未涵蓋此安全語義。" - } - } - }, - { - "reviewer": "Mage", - "focus": "logic", - "badge": "🔮", - "severity": "警告", - "file": "src/index.js", - "startLine": 330, - "endLine": 338, - "problem": "在建問題模式下,這段新增的 PR 回貼 issue 連結會在每次審查有保留問題時都新增一則 PR 留言,但同一流程前面明確跳過 `resolveOldComments`(建問題模式不清理 PR 舊留言)。最小重現:PR 第一次審查建立 issue #10 並在 PR 留連結;後續推新 commit 再跑一次,建立 issue #11 並再留一則連結。PR 上會同時存在 #10 與 #11,舊 issue 可能已過時,讀者無法判斷哪個才是目前審查結果。", - "suggestion": "建問題模式也應對本 action 先前的 PR 連結留言做過時標記,或在新增連結前查找並更新既有連結留言。若要避免碰觸 issue 內的審查內容,清理範圍可限制在 PR 上含 `MARK` 且標題為「已建立追蹤問題」的留言。", - "suggestedCode": "", - "id": "F009", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。已知排除事項只裁示舊審查留言在本輪結果前標過時的時機;本條指控建問題模式跳過 PR 舊連結留言清理,導致多個追蹤 issue 連結並存,未被既有排除涵蓋。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/diagnostics.js", - "startLine": 55, - "endLine": 63, - "problem": "`agentFailureDetail` 裡的 `stderr` 與 `stdout` 區塊幾乎同譜重奏:取值、slice、redact、判斷、push 只差欄位名。這種重複雖小,卻讓後續若要調整遮罩或長度時容易改一半走調。", - "suggestion": "建議抽出小 helper,例如 `appendRedactedOutput(parts, label, value)`,讓 stderr/stdout 共用同一段處理節奏。", - "suggestedCode": "", - "id": "F005", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。此條針對 diagnostics.js 中 stderr/stdout 處理重複的維護性問題,歷史 findings 主要是安全診斷缺測試與外洩風險,並非同一指控。" - } - } - }, { "reviewer": "Bard", "focus": "style", @@ -83,25 +26,6 @@ "reason": "保留。歷史 findings 主要涵蓋 resolveMergeBase 缺測試與診斷不足,本條指向函式內策略編排、診斷組裝與錯誤包裝混雜的可維護性問題,未被既有排除事項完整涵蓋。" } } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/gitrepo.js", - "startLine": 317, - "endLine": 346, - "problem": "`pushWithCredential` 的 JSDoc 已經很完整,但正文註解再次長篇解釋 checkout token、PAT、extraheader 清空等細節;文件與程式內註解重複奏同一段旋律,反而稀釋真正需要看的程式碼。", - "suggestion": "保留 JSDoc 的背景說明,函式內註解縮成操作提示即可,例如只說明「先清空 checkout extraheader,再注入本次 PAT header」。", - "suggestedCode": "", - "id": "F004", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。既有排除事項雖有 push-token manifest 說明重複,但未涵蓋 pushWithCredential 函式內 JSDoc 與正文註解重複;證據不足以判定為重複或誤報。" - } - } } ], "excluded": [ -- 2.53.0 From 76617dd9dc5c9c90da22fde07158bbaa1babc74a Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Tue, 21 Jul 2026 08:00:35 +0000 Subject: [PATCH 72/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 99 +++++++ .../findings/2026-07-21-16:00:33.json | 261 ++++++++++++++++++ 2 files changed, 360 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-21-16:00:33.json diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 84b5ce1..e5463a1 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1890,5 +1890,104 @@ "endLine": 69, "problem": "`redactSecrets` 與 `agentFailureDetail` 是新加入的安全診斷防線,但測試只覆蓋 Authorization/token 的基本遮罩。控制字元單行化、URL 內嵌帳密、長 token/hex、輸入與輸出長度截斷、以及沒有 error code 時的 fallback 訊息都還沒被驗證。", "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 redactSecrets/agentFailureDetail 對控制字元、URL 帳密、token 格式、截斷與 fallback 診斷等邊界缺少測試。" + }, + { + "addedAt": "2026/07/21 16:00:33", + "prNumber": 6, + "reviewer": "Bard", + "severity": "警告", + "file": "src/index.js", + "startLine": 166, + "endLine": 223, + "problem": "`main()` 這段新添的建問題模式狀態、暫存佇列、三個閉包輔助函式,全塞在主流程開頭,讓主旋律還沒開始就先進入一大段插曲。`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 彼此共享可變狀態,讀者必須在腦中追蹤閉包副作用,主流程的步驟節奏因此變得沉重。", + "reason": "Paladin:可排除(重複)。此條指涉 main() 內 issue 模式狀態、暫存佇列與閉包職責混雜,已由既有排除事項與歷史 findings 多次涵蓋。" + }, + { + "addedAt": "2026/07/21 16:00:33", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/index.js", + "startLine": 230, + "endLine": 239, + "problem": "註解標成「步驟 2:延後執行」,但實際位置夾在快速檢查與步驟 3 之前,後面又在步驟 8 後再次出現「步驟 2(延後執行)」。同一個步驟號在不同位置反覆變奏,雖然註解有解釋,閱讀節拍仍容易打結。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出流程步驟編號散落且實際順序變成 1、3~8、2、9~10,與本條「步驟 2 延後但仍用線性編號」相同。" + }, + { + "addedAt": "2026/07/21 16:00:33", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 166, + "endLine": 235, + "problem": "`main()` 現在同時負責流程編排、PR 留言、issue 模式暫存、issue 建立、fallback 狀態切換與留言 flush。這段靠 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 多個閉包變數互相配合,半年後要改「留言要發去哪裡」或「建 issue 失敗怎麼降級」時,很容易漏掉某個狀態轉換,尤其後面收尾 commit、resolve 舊留言、嚴重/其他問題發布都還會讀這些狀態。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。main() 同時承擔留言路由、issue 狀態、fallback 與 flush 的發布狀態管理,已由既有排除事項與歷史 findings 涵蓋。" + }, + { + "addedAt": "2026/07/21 16:00:33", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "src/index.js", + "startLine": 222, + "endLine": 230, + "problem": "註解與 log 名稱把「標記舊留言過時」稱為「步驟 2」,但實際執行點被延後到流程後段。這種非時間順序的步驟編號會讓維護者追 log 或對照 README 流程圖時產生認知落差:看到「步驟 2」不再代表第二個發生的動作,而是某個被延後的歷史步驟。", + "reason": "Paladin:可排除(重複)。與 F003 及歷史 findings 指涉同一個延後執行的「步驟 2」仍以線性步驟編號呈現所造成的認知落差。" + }, + { + "addedAt": "2026/07/21 16:00:33", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "readme.md", + "startLine": 76, + "endLine": 131, + "problem": "README 內大量手動維護到 `src/branch/develop/...#Lxx` 的精確行號連結。這次變更已經因程式碼位移而更新一整片連結,未來每次插入函式或註解都會造成文件 churn;更糟的是漏改時文件會指到錯誤行,讀者以為文件可信,實際上卻被帶到過期位置。", + "reason": "Paladin:可排除(重複)。README 大量手動維護 develop 分支與 #Lxx 行號連結,歷史 findings 已多次記錄相同維護成本與連結漂移風險。" + }, + { + "addedAt": "2026/07/21 16:00:33", + "prNumber": 6, + "reviewer": "Mage", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 117, + "endLine": 120, + "problem": "當 PR 審查出嚴重問題,但結果 commit/push 失敗時,`commitFindings` 只回傳 `false`。新版主流程又改成「本輪審查不因嚴重問題直接 exit 1,而是依賴下一輪讀到 `[failure]` commit 才失敗」。最小情境:`severe.length > 0`、token 權限不足或 push 競態導致 `commitAndPushFindings` 丟錯;此時沒有 `[failure]` commit、也不會有下一輪快速回報,但本輪仍可能以 0 結束,PR 檢查會在存在嚴重問題時通過。", + "reason": "Paladin:可排除(重複)。此條與 F001 指涉同一風險:嚴重 finding 存在時 failure 結果 commit/push 失敗,可能使本輪檢查仍以成功結束。" + }, + { + "addedAt": "2026/07/21 16:00:33", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 163, + "endLine": 437, + "problem": "建問題模式被大幅改寫:情境留言先暫存、只有 kept finding 才建 issue、建立失敗會 fallback 回 PR、嚴重/非嚴重 finding 會改發到 issue、最後還要回貼追蹤 issue 連結與設定相依關係。但目前新增測試只覆蓋了少數 helper,沒有驗證 `main()` 在這些分支下的實際副作用。這些行為沒經過試煉,等於還不知道留言會不會發錯地方、fallback 後會不會漏清舊留言、或無 finding 時是否真的靜默通過。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、只在有 finding 時建 issue、fallback、嚴重/非嚴重分流、PR 回貼與靜默通過等核心分支缺少流程測試。" + }, + { + "addedAt": "2026/07/21 16:00:33", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 298, + "endLine": 355, + "problem": "`commitAndPushFindings` 的推送行為改成一律走 `pushWithCredential`,並宣稱 token 不進 argv、會清掉 checkout 的 extraheader、只接受同一 Gitea origin 的 `.git` URL。這是安全與 CI 觸發都很關鍵的失敗路徑,但目前 `test/gitrepo.test.js` 只測了分支名稱檢查,沒有驗證推送時的 argv/env,也沒有驗證遠端 URL 不符時會停止。這段若改壞,測試不會提醒我們 token 可能出現在命令列或推送根本沒用 PAT 身分。", + "reason": "Paladin:可排除(重複)。歷史 findings 已記錄 commitAndPushFindings/pushWithCredential 的認證推送路徑、argv/env、遠端目標檢查與失敗訊息遮蔽缺少測試。" + }, + { + "addedAt": "2026/07/21 16:00:33", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 130, + "endLine": 188, + "problem": "`resolveMergeBase` 新增了多階段 fetch 補歷史流程:先 fetch base、首次 merge-base、再 deepen base、deepen PR HEAD、必要時 unshallow,且每個策略成功後要立即重試並短路返回。現有測試只驗證不安全 `baseRef` 會在 fetch 前被拒絕,沒有測到淺層 checkout、首次失敗後補抓成功、策略失敗繼續下一個、全部失敗時診斷訊息等邊界。這正是容易 off-by-one 或順序錯的流程,現在還沒有測試保護。", + "reason": "Paladin:可排除(重複)。resolveMergeBase 多階段 fetch、淺層與非淺層路徑、策略停止條件、全部失敗診斷與 cause 缺少測試,已由歷史 findings 涵蓋。" } ] diff --git a/.gitea/ai-review/findings/2026-07-21-16:00:33.json b/.gitea/ai-review/findings/2026-07-21-16:00:33.json new file mode 100644 index 0000000..523eb8d --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-21-16:00:33.json @@ -0,0 +1,261 @@ +{ + "generatedAt": "2026/07/21 16:00:33", + "commitSha": "216bc39255ec176a0f66f2b257beea5929ef4eb8", + "prNumber": 6, + "tool": { + "name": "codex", + "version": "codex-cli 0.144.6", + "model": "gpt-5.5" + }, + "findings": [ + { + "reviewer": "Assassin", + "focus": "security", + "badge": "🗡️", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 105, + "endLine": 116, + "problem": "這裡把審查結果 commit/push 失敗吞掉並回傳 `false`,而本次變更又把主流程改成「本輪審查不因嚴重問題直接 exit 1,靠下一輪讀到 `[failure]` commit 才失敗」。攻擊者只要讓結果 commit 推不上去,例如在 PR head 競態推送、讓 token 沒有 push 權限、或讓來源分支拒絕 bot push,就能讓嚴重安全 finding 已產生但沒有 failure commit、也沒有下一輪失敗檢查,等同把必要檢查繞過。", + "suggestion": "嚴重問題存在時,結果 commit/push 失敗必須直接讓本輪 workflow 失敗;只有 success 結果或無變更時才可降級不阻擋。呼叫端應檢查 `commitFindings` 回傳值,或讓 `commitFindings` 在 `result === 'failure'` 時重拋錯誤。", + "suggestedCode": "const committed = commitFindings({ cwd, ctx, files: filesToCommit, result });\nif (result === 'failure' && !committed) {\n log('收尾', 'ERR', '存在嚴重問題,但無法推送 failure 結果 commit;本輪直接失敗以避免繞過檢查。');\n return 1;\n}\nreturn 0;", + "id": "F001", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留(成立)。已知排除事項與歷史 findings 未涵蓋「嚴重 finding 依賴 failure commit,但 failure commit 推送失敗時本輪仍可能通過」這個繞過風險;證據不足以排除。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "action.yml", + "startLine": 18, + "endLine": 19, + "problem": "中文敘述裡混入 `PR/issue`、`findings/exclusions` 這種半形斜線寫法,但同一份變更其他地方大量使用 `PR/issue`、`警告+建議` 這類全形符號。標點像節拍器,這裡忽然換拍,讓 manifest 的文字風格不夠一致。", + "suggestion": "統一中文文件與註解中的分隔符號,建議在中文語境使用全形斜線:`PR/issue`、`findings/exclusions`;若是程式路徑或指令片段才保留半形 `/`。", + "suggestedCode": "", + "id": "F004", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留(成立)。未命中已知排除事項,也未見歷史 finding 涵蓋 action.yml 中文標點半形/全形風格不一致;依現有資料不能判為重複或誤報。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/gitrepo.js", + "startLine": 299, + "endLine": 344, + "problem": "`pushWithCredential` 的 JSDoc 幾乎把安全設計、CI 觸發語意、checkout extraheader 行為全部寫成一篇短文。資訊本身有價值,但集中在函式註解裡會壓過函式簽名,讀者想找參數與責任邊界時,得先穿過一大段敘事。", + "suggestion": "保留函式層級的摘要與關鍵安全不變式,其餘背景可移到較短的段落或專門文件。JSDoc 建議聚焦在「做什麼、為何不能改、參數怎麼用」,避免把完整決策紀錄塞進 API 註解。", + "suggestedCode": "", + "id": "F005", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留(成立)。歷史 finding 雖有 resolveMergeBase 閱讀密度問題,但未涵蓋 pushWithCredential JSDoc 過長且混入決策紀錄;目前不能排除。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "建議", + "file": "src/lib/gitrepo.js", + "startLine": 367, + "endLine": 369, + "problem": "為了測試把內部函式掛在 `module.exports.__test`,會讓 production module 的公開形狀混入測試專用 API。未來其他程式碼可能誤用 `__test.assertSafeBranchRef`,而維護者也得在重構時顧慮這個非正式出口,模組邊界會慢慢變模糊。", + "suggestion": "把分支名稱驗證抽到獨立小模組並正常匯出,例如 `src/lib/gitref.js`,讓 production code 與測試都依賴同一個正式 API;或若它只屬於 gitrepo 內部,就改由測試 `resolveMergeBase`/`commitAndPushFindings` 的外部行為覆蓋,不暴露 `__test`。", + "suggestedCode": "", + "id": "F009", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留(成立)。目前提供的排除事項與歷史 findings 未涵蓋 module.exports.__test 暴露測試專用 API 的模組邊界問題;證據不足以排除。" + } + } + } + ], + "excluded": [ + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "警告", + "file": "src/index.js", + "startLine": 166, + "endLine": 223, + "problem": "`main()` 這段新添的建問題模式狀態、暫存佇列、三個閉包輔助函式,全塞在主流程開頭,讓主旋律還沒開始就先進入一大段插曲。`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 彼此共享可變狀態,讀者必須在腦中追蹤閉包副作用,主流程的步驟節奏因此變得沉重。", + "suggestion": "把留言去向抽成小型 helper(例如 `createCommentSink`),讓 `main()` 只看見 `commentSink.post()`、`commentSink.flushToIssue()`、`commentSink.fallbackToPr()` 這類語意清楚的介面。主流程保留編排,狀態管理移到專責函式,樂句會乾淨許多。", + "suggestedCode": "", + "id": "F002", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。此條指涉 main() 內 issue 模式狀態、暫存佇列與閉包職責混雜,已由既有排除事項與歷史 findings 多次涵蓋。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 230, + "endLine": 239, + "problem": "註解標成「步驟 2:延後執行」,但實際位置夾在快速檢查與步驟 3 之前,後面又在步驟 8 後再次出現「步驟 2(延後執行)」。同一個步驟號在不同位置反覆變奏,雖然註解有解釋,閱讀節拍仍容易打結。", + "suggestion": "將流程步驟編號與執行順序拆開命名,例如稱為「舊留言清理(延後)」或「收斂前清理舊留言」,避免用 `步驟 2` 這種線性編號描述一個實際延後執行的階段。", + "suggestedCode": "", + "id": "F003", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已指出流程步驟編號散落且實際順序變成 1、3~8、2、9~10,與本條「步驟 2 延後但仍用線性編號」相同。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 166, + "endLine": 235, + "problem": "`main()` 現在同時負責流程編排、PR 留言、issue 模式暫存、issue 建立、fallback 狀態切換與留言 flush。這段靠 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 多個閉包變數互相配合,半年後要改「留言要發去哪裡」或「建 issue 失敗怎麼降級」時,很容易漏掉某個狀態轉換,尤其後面收尾 commit、resolve 舊留言、嚴重/其他問題發布都還會讀這些狀態。", + "suggestion": "把發布目的地抽成一個小型 publisher 模組或類別,讓 `main()` 只呼叫 `publisher.comment()`、`publisher.ensureIssue()`、`publisher.fallbackToPr()`、`publisher.resolveOldComments()` 這類語意方法。狀態留在 publisher 內部,並補單元測試覆蓋「一般模式、issue 模式有 finding、issue 模式無 finding、建立 issue 失敗降級」四條路徑。", + "suggestedCode": "", + "id": "F006", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(命中已知排除事項且重複)。main() 同時承擔留言路由、issue 狀態、fallback 與 flush 的發布狀態管理,已由既有排除事項與歷史 findings 涵蓋。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "建議", + "file": "src/index.js", + "startLine": 222, + "endLine": 230, + "problem": "註解與 log 名稱把「標記舊留言過時」稱為「步驟 2」,但實際執行點被延後到流程後段。這種非時間順序的步驟編號會讓維護者追 log 或對照 README 流程圖時產生認知落差:看到「步驟 2」不再代表第二個發生的動作,而是某個被延後的歷史步驟。", + "suggestion": "改用穩定的階段名稱取代數字,例如 `清理舊留言`、`工具偵測`、`diff 摘要`,或重新編號成實際執行順序。若一定要保留舊編號,建議只放在 README 遷移說明,不要散落在程式碼註解與 log label 中。", + "suggestedCode": "", + "id": "F007", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。與 F003 及歷史 findings 指涉同一個延後執行的「步驟 2」仍以線性步驟編號呈現所造成的認知落差。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "readme.md", + "startLine": 76, + "endLine": 131, + "problem": "README 內大量手動維護到 `src/branch/develop/...#Lxx` 的精確行號連結。這次變更已經因程式碼位移而更新一整片連結,未來每次插入函式或註解都會造成文件 churn;更糟的是漏改時文件會指到錯誤行,讀者以為文件可信,實際上卻被帶到過期位置。", + "suggestion": "避免在手寫 README 維護精確行號。可改連到檔案或章節錨點,或把 API 列表改由文件產生器輸出。若需要固定定位,優先使用語意穩定的錨點或函式章節,而不是會隨程式碼行數漂移的 `#Lxx`。", + "suggestedCode": "", + "id": "F008", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。README 大量手動維護 develop 分支與 #Lxx 行號連結,歷史 findings 已多次記錄相同維護成本與連結漂移風險。" + } + } + }, + { + "reviewer": "Mage", + "focus": "logic", + "badge": "🔮", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 117, + "endLine": 120, + "problem": "當 PR 審查出嚴重問題,但結果 commit/push 失敗時,`commitFindings` 只回傳 `false`。新版主流程又改成「本輪審查不因嚴重問題直接 exit 1,而是依賴下一輪讀到 `[failure]` commit 才失敗」。最小情境:`severe.length > 0`、token 權限不足或 push 競態導致 `commitAndPushFindings` 丟錯;此時沒有 `[failure]` commit、也不會有下一輪快速回報,但本輪仍可能以 0 結束,PR 檢查會在存在嚴重問題時通過。", + "suggestion": "嚴重問題存在時,若 failure 結果 commit 沒有成功產生,必須讓本輪直接回傳 1。可保留「成功 push failure commit 時本輪回傳 0、下一輪失敗」的設計,但 push 失敗不可靜默通過。", + "suggestedCode": "const result = severe.length === 0 ? 'success' : 'failure';\nconst committed = commitFindings({ cwd, ctx, files: filesToCommit, result });\nif (result === 'failure' && !committed) {\n log('收尾', 'ERR', '有嚴重問題但無法推送 failure 結果 commit,本輪直接失敗。');\n return 1;\n}\nreturn 0;", + "id": "F010", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。此條與 F001 指涉同一風險:嚴重 finding 存在時 failure 結果 commit/push 失敗,可能使本輪檢查仍以成功結束。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/index.js", + "startLine": 163, + "endLine": 437, + "problem": "建問題模式被大幅改寫:情境留言先暫存、只有 kept finding 才建 issue、建立失敗會 fallback 回 PR、嚴重/非嚴重 finding 會改發到 issue、最後還要回貼追蹤 issue 連結與設定相依關係。但目前新增測試只覆蓋了少數 helper,沒有驗證 `main()` 在這些分支下的實際副作用。這些行為沒經過試煉,等於還不知道留言會不會發錯地方、fallback 後會不會漏清舊留言、或無 finding 時是否真的靜默通過。", + "suggestion": "請補 `src/index.js` 的流程層級測試,mock `gitea`、`review`、`agents`、`gitrepo` 等依賴,至少覆蓋:\n\n| 情境 | 應驗證 |\n| --- | --- |\n| `createIssue=true` 且 `kept.length===0` | 不建 issue、不發 PR 留言、暫存情境留言不外洩 |\n| `createIssue=true` 且有 finding | 先建 issue,再依序 flush 工具/diff/角色留言,finding 發到 issue,PR 只留下追蹤 issue 連結 |\n| 建 issue 或寫入 issue 失敗 | fallback 後情境留言改發 PR,後續使用一般模式留言與舊留言清理 |\n| 嚴重 finding | 結果 commit 標記為 failure,但本輪 `main()` 回傳 0,下一輪才由 bot commit 回報 1 |", + "suggestedCode": "", + "id": "F011", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式的暫存留言、只在有 finding 時建 issue、fallback、嚴重/非嚴重分流、PR 回貼與靜默通過等核心分支缺少流程測試。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 298, + "endLine": 355, + "problem": "`commitAndPushFindings` 的推送行為改成一律走 `pushWithCredential`,並宣稱 token 不進 argv、會清掉 checkout 的 extraheader、只接受同一 Gitea origin 的 `.git` URL。這是安全與 CI 觸發都很關鍵的失敗路徑,但目前 `test/gitrepo.test.js` 只測了分支名稱檢查,沒有驗證推送時的 argv/env,也沒有驗證遠端 URL 不符時會停止。這段若改壞,測試不會提醒我們 token 可能出現在命令列或推送根本沒用 PAT 身分。", + "suggestion": "請補針對 `commitAndPushFindings`/`pushWithCredential` 的測試。建議用可替換的 `execFileSync` seam 或測試專用注入方式,斷言:\n\n| 情境 | 斷言方向 |\n| --- | --- |\n| 正常推送 | `git push` argv 只含不帶帳密的 `remoteUrl` 與 refspec |\n| 正常推送 | env 含 `GIT_CONFIG_COUNT=2`,先清空 `http./.extraheader`,再注入 `Authorization: Basic ...` |\n| `remoteUrl` origin 不符或非 `.git` | 拋出固定錯誤且不執行 push |\n| push 失敗 | 對外錯誤訊息不含 token、remote URL 或 argv |", + "suggestedCode": "", + "id": "F012", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已記錄 commitAndPushFindings/pushWithCredential 的認證推送路徑、argv/env、遠端目標檢查與失敗訊息遮蔽缺少測試。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 130, + "endLine": 188, + "problem": "`resolveMergeBase` 新增了多階段 fetch 補歷史流程:先 fetch base、首次 merge-base、再 deepen base、deepen PR HEAD、必要時 unshallow,且每個策略成功後要立即重試並短路返回。現有測試只驗證不安全 `baseRef` 會在 fetch 前被拒絕,沒有測到淺層 checkout、首次失敗後補抓成功、策略失敗繼續下一個、全部失敗時診斷訊息等邊界。這正是容易 off-by-one 或順序錯的流程,現在還沒有測試保護。", + "suggestion": "請補資料驅動的 `resolveMergeBase` 測試,mock git 執行結果來驗證:首次 merge-base 成功時不跑 deepen;deepen base 後成功會立刻回傳;deepen PR HEAD 使用目前 `HEAD` SHA 而不是遠端符號 `HEAD`;非 shallow repo 不呼叫 `--unshallow`;全部策略失敗時錯誤包含各策略診斷且保留 cause。", + "suggestedCode": "", + "id": "F013", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。resolveMergeBase 多階段 fetch、淺層與非淺層路徑、策略停止條件、全部失敗診斷與 cause 缺少測試,已由歷史 findings 涵蓋。" + } + } + } + ] +} -- 2.53.0 From 1f012c6cfb46d110cb4aead06e70b04ea5b2ad80 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 16:26:30 +0800 Subject: [PATCH 73/79] =?UTF-8?q?refactor(gitref):=20=E5=B0=87=E5=88=86?= =?UTF-8?q?=E6=94=AF=E5=90=8D=E7=A8=B1=E9=A9=97=E8=AD=89=E6=94=B9=E7=82=BA?= =?UTF-8?q?=E6=AD=A3=E5=BC=8F=E6=A8=A1=E7=B5=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/gitref.js | 32 ++++++++++++++++++++++++++++++ src/lib/gitrepo.js | 47 ++++---------------------------------------- test/gitrepo.test.js | 5 +++-- 3 files changed, 39 insertions(+), 45 deletions(-) create mode 100644 src/lib/gitref.js diff --git a/src/lib/gitref.js b/src/lib/gitref.js new file mode 100644 index 0000000..ab77ff5 --- /dev/null +++ b/src/lib/gitref.js @@ -0,0 +1,32 @@ +'use strict'; + +const { execFileSync } = require('child_process'); + +/** + * 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。 + * + * @param {string} refName - 使用者或事件 payload 提供的分支名稱。 + * @param {string} fieldName - 錯誤訊息中的欄位名稱。 + * @returns {string} 原樣回傳通過驗證的分支名稱。 + * @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。 + */ +function assertSafeBranchRef(refName, fieldName) { + const value = String(refName || '').trim(); + if (!value) throw new Error(`${fieldName} 不可為空。`); + if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) { + throw new Error(`${fieldName} 不是安全的分支名稱:${value}`); + } + try { + execFileSync('git', ['check-ref-format', '--branch', value], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'pipe'], + }); + } catch { + throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`); + } + return value; +} + +module.exports = { + assertSafeBranchRef, +}; diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index cd67754..9c49c50 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -1,37 +1,10 @@ 'use strict'; const { execFileSync } = require('child_process'); +const { assertSafeBranchRef } = require('./gitref'); // git 操作工具:一律以 execFileSync 呼叫 git(不經 shell,避免注入),輸出以 UTF-8 回傳。 -/** - * 驗證遠端分支名稱可安全用於 refspec 與 refs/remotes/origin/*。 - * - * @param {string} refName - 使用者或事件 payload 提供的分支名稱。 - * @param {string} fieldName - 錯誤訊息中的欄位名稱。 - * @returns {string} 原樣回傳通過驗證的分支名稱。 - * @throws {Error} 分支名稱空白、含路徑穿越,或不符合 git 分支 ref 規則時拋出。 - * @remarks - * 使用情境:`resolveMergeBase` 的 `baseRef` 與 `commitAndPushFindings` 的 - * `headRef` 會被組進 refspec;先驗證可避免惡意 payload 影響本地 refs 路徑。 - */ -function assertSafeBranchRef(refName, fieldName) { - const value = String(refName || '').trim(); - if (!value) throw new Error(`${fieldName} 不可為空。`); - if (value.includes('..') || value.startsWith('/') || value.endsWith('/') || value.includes('\\')) { - throw new Error(`${fieldName} 不是安全的分支名稱:${value}`); - } - try { - execFileSync('git', ['check-ref-format', '--branch', value], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'pipe'], - }); - } catch { - throw new Error(`${fieldName} 不是合法的 git 分支名稱:${value}`); - } - return value; -} - /** * 同步執行 git 指令並回傳原始 stdout 輸出。 * @@ -308,18 +281,9 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s /** * 以帶認證的方式推送到指定遠端,認證資訊只經環境變數傳入、不進命令列 argv。 * - * 認證方式:等同 `https://ai-review-bot:@host/...` 的 HTTP Basic(git 會把 - * URL 帳密轉成相同的 `Authorization: Basic` 標頭送出),但改以 git 的 - * `GIT_CONFIG_*` 環境變數注入 `http./.extraheader`,使 base64 憑證**不出現在 argv** - * (避免程序清單/例外回顯洩漏);推送目標 URL 亦不含帳密。 - * - * 觸發 CI 關鍵:`actions/checkout` 會把「自動 Actions token」持久化在同一個 - * `http./.extraheader` scope;若沿用它推送,Gitea 會視為「自動 token 觸發」而 - * **不再觸發 workflow**(防遞迴)。故本函式對這次 push 於該 scope**先以空值重置**(清掉自動 - * token——git 對 extraHeader 給空值即清空既有清單),**再注入 PAT 的 Authorization**,讓推送以 - * PAT 身分進行、觸發 PR 的 synchronize;作用範圍僅限本次 push 的環境變數,不影響 action 其他 - * 仰賴 checkout 持久化憑證的 fetch(如 {@link resolveMergeBase})。 - * 推送失敗時**不重拋原始例外**(其 message 會含命令列與遠端 URL),改拋固定訊息。 + * 以 `GIT_CONFIG_*` 注入本次 HTTP Basic extraheader,避免憑證出現在 argv; + * 同時先清空 checkout 持久化的自動 token extraheader,確保本次 push 使用呼叫端 token。 + * 推送失敗時改拋固定訊息,避免原始例外帶出遠端 URL 或認證資訊。 * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} remoteUrl - 不含帳密的遠端 URL(形如 `https://host/owner/repo.git`)。 @@ -367,7 +331,4 @@ module.exports = { fileDiff, fileLastUpdatedIso, commitAndPushFindings, - __test: { - assertSafeBranchRef, - }, }; diff --git a/test/gitrepo.test.js b/test/gitrepo.test.js index baa5895..dcc9fc1 100644 --- a/test/gitrepo.test.js +++ b/test/gitrepo.test.js @@ -4,14 +4,15 @@ const assert = require('node:assert/strict'); const test = require('node:test'); const gitrepo = require('../src/lib/gitrepo'); +const gitref = require('../src/lib/gitref'); test('assertSafeBranchRef 接受一般分支名稱', () => { - assert.equal(gitrepo.__test.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123'); + assert.equal(gitref.assertSafeBranchRef('feature/review-123', 'baseRef'), 'feature/review-123'); }); test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => { assert.throws( - () => gitrepo.__test.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'), + () => gitref.assertSafeBranchRef('../../hooks/pre-push', 'baseRef'), /不是安全的分支名稱/, ); }); -- 2.53.0 From 8e18bbacc24a24d4cb0df7fcad36b9e202b96d53 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 16:26:30 +0800 Subject: [PATCH 74/79] =?UTF-8?q?style(action=20manifest):=20=E7=B5=B1?= =?UTF-8?q?=E4=B8=80=E4=B8=AD=E6=96=87=E6=96=9C=E7=B7=9A=E6=A8=99=E9=BB=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index 859e653..e1ef5de 100644 --- a/action.yml +++ b/action.yml @@ -16,11 +16,11 @@ author: 'Jeffery' # 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入, # runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取。 inputs: - # Gitea API token:用於對 PR/issue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。 + # Gitea API token:用於對 PR/issue 留言審查結果,以及 push 審查結果檔(findings/exclusions)回 repo。 token: # 參數用途說明:secrets/vars context 在 action 內不可用,故由呼叫端 workflow 以 secrets 傳入。 # 建議傳入能觸發 CI 的 PAT;自動 token 推送結果 commit 時可能不會再觸發 workflow。 - description: 'Gitea API token(PR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)' + description: 'Gitea API token(PR/issue 留言與 push findings 用;建議以能觸發 CI 的 PAT 由 secrets 傳入)' # 必填:缺少 token 無法呼叫 Gitea API,action 無法運作。 required: true # 指定 AI 工具使用的模型名稱。 -- 2.53.0 From 43ad2b56d27429aa3c812522dc1d4fedce4a9b76 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 16:26:30 +0800 Subject: [PATCH 75/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E5=9B=9E=E5=AF=AB=E6=9C=AC=E8=BC=AA=20findings=20=E8=88=87?= =?UTF-8?q?=E6=8E=92=E9=99=A4=E4=BA=8B=E9=A0=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/ai-review/exclusions.json | 11 +++ .../findings/2026-07-21-16:00:33.json | 79 +------------------ 2 files changed, 12 insertions(+), 78 deletions(-) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index e5463a1..4fe8d32 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -1989,5 +1989,16 @@ "endLine": 188, "problem": "`resolveMergeBase` 新增了多階段 fetch 補歷史流程:先 fetch base、首次 merge-base、再 deepen base、deepen PR HEAD、必要時 unshallow,且每個策略成功後要立即重試並短路返回。現有測試只驗證不安全 `baseRef` 會在 fetch 前被拒絕,沒有測到淺層 checkout、首次失敗後補抓成功、策略失敗繼續下一個、全部失敗時診斷訊息等邊界。這正是容易 off-by-one 或順序錯的流程,現在還沒有測試保護。", "reason": "Paladin:可排除(重複)。resolveMergeBase 多階段 fetch、淺層與非淺層路徑、策略停止條件、全部失敗診斷與 cause 缺少測試,已由歷史 findings 涵蓋。" + }, + { + "addedAt": "2026/07/21 16:26:12", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 105, + "endLine": 116, + "problem": "這裡把審查結果 commit/push 失敗吞掉並回傳 `false`,而本次變更又把主流程改成「本輪審查不因嚴重問題直接 exit 1,靠下一輪讀到 `[failure]` commit 才失敗」。攻擊者只要讓結果 commit 推不上去,例如在 PR head 競態推送、讓 token 沒有 push 權限、或讓來源分支拒絕 bot push,就能讓嚴重安全 finding 已產生但沒有 failure commit、也沒有下一輪失敗檢查,等同把必要檢查繞過。", + "reason": "人工裁示排除:主流程已在 result === failure 且 commitFindings 回傳 false 時直接回傳 1;本 finding 針對舊位置的描述已由現有收尾防線涵蓋。" } ] diff --git a/.gitea/ai-review/findings/2026-07-21-16:00:33.json b/.gitea/ai-review/findings/2026-07-21-16:00:33.json index 523eb8d..ac0c2d5 100644 --- a/.gitea/ai-review/findings/2026-07-21-16:00:33.json +++ b/.gitea/ai-review/findings/2026-07-21-16:00:33.json @@ -7,84 +7,7 @@ "version": "codex-cli 0.144.6", "model": "gpt-5.5" }, - "findings": [ - { - "reviewer": "Assassin", - "focus": "security", - "badge": "🗡️", - "severity": "嚴重", - "file": "src/index.js", - "startLine": 105, - "endLine": 116, - "problem": "這裡把審查結果 commit/push 失敗吞掉並回傳 `false`,而本次變更又把主流程改成「本輪審查不因嚴重問題直接 exit 1,靠下一輪讀到 `[failure]` commit 才失敗」。攻擊者只要讓結果 commit 推不上去,例如在 PR head 競態推送、讓 token 沒有 push 權限、或讓來源分支拒絕 bot push,就能讓嚴重安全 finding 已產生但沒有 failure commit、也沒有下一輪失敗檢查,等同把必要檢查繞過。", - "suggestion": "嚴重問題存在時,結果 commit/push 失敗必須直接讓本輪 workflow 失敗;只有 success 結果或無變更時才可降級不阻擋。呼叫端應檢查 `commitFindings` 回傳值,或讓 `commitFindings` 在 `result === 'failure'` 時重拋錯誤。", - "suggestedCode": "const committed = commitFindings({ cwd, ctx, files: filesToCommit, result });\nif (result === 'failure' && !committed) {\n log('收尾', 'ERR', '存在嚴重問題,但無法推送 failure 結果 commit;本輪直接失敗以避免繞過檢查。');\n return 1;\n}\nreturn 0;", - "id": "F001", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留(成立)。已知排除事項與歷史 findings 未涵蓋「嚴重 finding 依賴 failure commit,但 failure commit 推送失敗時本輪仍可能通過」這個繞過風險;證據不足以排除。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "action.yml", - "startLine": 18, - "endLine": 19, - "problem": "中文敘述裡混入 `PR/issue`、`findings/exclusions` 這種半形斜線寫法,但同一份變更其他地方大量使用 `PR/issue`、`警告+建議` 這類全形符號。標點像節拍器,這裡忽然換拍,讓 manifest 的文字風格不夠一致。", - "suggestion": "統一中文文件與註解中的分隔符號,建議在中文語境使用全形斜線:`PR/issue`、`findings/exclusions`;若是程式路徑或指令片段才保留半形 `/`。", - "suggestedCode": "", - "id": "F004", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留(成立)。未命中已知排除事項,也未見歷史 finding 涵蓋 action.yml 中文標點半形/全形風格不一致;依現有資料不能判為重複或誤報。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "src/lib/gitrepo.js", - "startLine": 299, - "endLine": 344, - "problem": "`pushWithCredential` 的 JSDoc 幾乎把安全設計、CI 觸發語意、checkout extraheader 行為全部寫成一篇短文。資訊本身有價值,但集中在函式註解裡會壓過函式簽名,讀者想找參數與責任邊界時,得先穿過一大段敘事。", - "suggestion": "保留函式層級的摘要與關鍵安全不變式,其餘背景可移到較短的段落或專門文件。JSDoc 建議聚焦在「做什麼、為何不能改、參數怎麼用」,避免把完整決策紀錄塞進 API 註解。", - "suggestedCode": "", - "id": "F005", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留(成立)。歷史 finding 雖有 resolveMergeBase 閱讀密度問題,但未涵蓋 pushWithCredential JSDoc 過長且混入決策紀錄;目前不能排除。" - } - } - }, - { - "reviewer": "Leo", - "focus": "maintainability", - "badge": "🧰", - "severity": "建議", - "file": "src/lib/gitrepo.js", - "startLine": 367, - "endLine": 369, - "problem": "為了測試把內部函式掛在 `module.exports.__test`,會讓 production module 的公開形狀混入測試專用 API。未來其他程式碼可能誤用 `__test.assertSafeBranchRef`,而維護者也得在重構時顧慮這個非正式出口,模組邊界會慢慢變模糊。", - "suggestion": "把分支名稱驗證抽到獨立小模組並正常匯出,例如 `src/lib/gitref.js`,讓 production code 與測試都依賴同一個正式 API;或若它只屬於 gitrepo 內部,就改由測試 `resolveMergeBase`/`commitAndPushFindings` 的外部行為覆蓋,不暴露 `__test`。", - "suggestedCode": "", - "id": "F009", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留(成立)。目前提供的排除事項與歷史 findings 未涵蓋 module.exports.__test 暴露測試專用 API 的模組邊界問題;證據不足以排除。" - } - } - } - ], + "findings": [], "excluded": [ { "reviewer": "Bard", -- 2.53.0 From b60f50f2b4d77da0f01651ea2d0cf0ee44bf4110 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Tue, 21 Jul 2026 08:30:13 +0000 Subject: [PATCH 76/79] chore: update ai-review findings [ai-review-bot][failure] --- .gitea/ai-review/exclusions.json | 99 +++++++ .../findings/2026-07-21-16:30:12.json | 280 ++++++++++++++++++ 2 files changed, 379 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-21-16:30:12.json diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 4fe8d32..40256e4 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -2000,5 +2000,104 @@ "endLine": 116, "problem": "這裡把審查結果 commit/push 失敗吞掉並回傳 `false`,而本次變更又把主流程改成「本輪審查不因嚴重問題直接 exit 1,靠下一輪讀到 `[failure]` commit 才失敗」。攻擊者只要讓結果 commit 推不上去,例如在 PR head 競態推送、讓 token 沒有 push 權限、或讓來源分支拒絕 bot push,就能讓嚴重安全 finding 已產生但沒有 failure commit、也沒有下一輪失敗檢查,等同把必要檢查繞過。", "reason": "人工裁示排除:主流程已在 result === failure 且 commitFindings 回傳 false 時直接回傳 1;本 finding 針對舊位置的描述已由現有收尾防線涵蓋。" + }, + { + "addedAt": "2026/07/21 16:30:12", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "readme.md", + "startLine": 48, + "endLine": 48, + "problem": "Mermaid 節點代號從 `N8` 跳回 `N2`,雖然流程語意是「步驟 2 延後」,但原始碼讀起來像旋律突然倒帶;維護者在對照圖與步驟時容易被代號順序絆住。", + "reason": "Paladin:可排除(重複)。歷史 findings 已指出流程步驟編號與實際順序、README 流程圖及文件對照會產生認知落差;本條 Mermaid 節點代號問題屬同一類指控。" + }, + { + "addedAt": "2026/07/21 16:30:12", + "prNumber": 6, + "reviewer": "Bard", + "severity": "警告", + "file": "src/index.js", + "startLine": 167, + "endLine": 235, + "problem": "`main()` 內新增了建 issue 模式狀態、暫存佇列與三個帶長篇 JSDoc 的閉包,讓主流程的旋律被大量伴奏蓋過。這段同時管理 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 與 PR 留言 id,可讀性負擔明顯升高。", + "reason": "Paladin:可排除(重複)。已知排除事項與歷史 findings 已涵蓋 main() 內留言路由、issue 狀態、暫存佇列與發布職責耦合造成可讀性與維護負擔。" + }, + { + "addedAt": "2026/07/21 16:30:12", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "src/lib/gitrepo.js", + "startLine": 121, + "endLine": 128, + "problem": "`strategies` 以陣列位置承載語意,`[label, ...args]` 雖簡短,卻讓每個元素的第二欄之後全靠讀者猜是 git argv;這裡的可讀性像沒有小節線的樂譜。", + "reason": "Paladin:可排除(重複)。歷史 finding 已針對 resolveMergeBase 內 strategies 與補救策略編排使閱讀節奏偏密提出問題;本條 tuple 可讀性屬同一段策略結構的細項。" + }, + { + "addedAt": "2026/07/21 16:30:12", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 163, + "endLine": 231, + "problem": "`main()` 這次把一般模式/建問題模式/降級模式的留言去向都塞進閉包狀態:`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 彼此耦合。半年後要新增一種落地方式或調整「何時清舊留言」時,維護者必須同時追多個可變狀態與後續多個 `if (issueModeActive)` 分支,很容易漏掉 flush、清空、登錄留言 id 或 fallback 後的收尾行為。", + "reason": "Paladin:可排除(重複)。與 F003 及既有排除事項相同,均指向 main() 以可變閉包狀態承擔留言目的地、issue 生命週期與 fallback 狀態機。" + }, + { + "addedAt": "2026/07/21 16:30:12", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 232, + "endLine": 241, + "problem": "流程註解把「將舊留言標記為解決」稱為「步驟 2(延後執行)」,但實際執行點在審查結果產生後、接近原本步驟 9 前。這種「編號是 2、時間點是 8 後」的設計已經擴散到多個檔案的 JSDoc 與 log 描述,未來查 CI log 或維護流程圖時會產生認知落差:看到 `步驟2` 不代表它真的發生在第二步。", + "reason": "Paladin:可排除(重複)。歷史 findings 已明確記錄步驟編號散落於 log、JSDoc、README 且實際順序變成 1、3 到 8、2、9 到 10 的維護風險。" + }, + { + "addedAt": "2026/07/21 16:30:12", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "src/lib/gitrepo.js", + "startLine": 103, + "endLine": 160, + "problem": "`resolveMergeBase()` 內同時負責驗證 base ref、fetch base、重試 merge-base、補抓淺層歷史、累積診斷與組錯誤訊息。這些都和「取得 merge-base」有關,但現在全部攤在同一個函式裡,策略陣列、診斷格式與 git 操作細節混在一起;未來要新增 fetch 策略或改診斷文字時,容易不小心改壞主流程判斷。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出 resolveMergeBase 同時負責驗證、fetch 策略編排、診斷與錯誤包裝,難以分辨主流程與補救策略。" + }, + { + "addedAt": "2026/07/21 16:30:12", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 164, + "endLine": 232, + "problem": "建問題模式新增了「先暫存情境留言、確定有保留 finding 才建 issue、建 issue 失敗時降級回 PR 留言」這一整段流程,但目前測試只覆蓋了部分 helper,沒有驗證主流程在 create-issue=true 下的分流行為。這代表像「無保留問題時不應建 issue/不應留言」、「建立 issue 失敗時應把 pending 留言補回 PR」、「trackingIssue 建立後後續留言應改送 issue」這些行為都還沒通過試煉。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式核心流程、暫存留言、issue 建立條件、發布分流、外部 API 狀態切換與降級路徑缺少測試。" + }, + { + "addedAt": "2026/07/21 16:30:12", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 104, + "endLine": 150, + "problem": "resolveMergeBase 新增了多階段 fetch/deepen/unshallow fallback 與診斷錯誤,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證任何成功或失敗 fallback 路徑。這段是 diff 基準的核心邏輯,若 shallow checkout 下 deepen PR HEAD 用錯 ref、策略順序跑錯,或全部失敗時診斷不正確,現有測試都抓不到。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、策略停止條件、降級與最終診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/21 16:30:12", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 272, + "endLine": 322, + "problem": "commitAndPushFindings 改成一律透過 pushWithCredential 用 PAT extraheader 推送,且新增 headRef 安全檢查與固定錯誤遮蔽;但目前 gitrepo 測試沒有覆蓋 commitAndPushFindings/pushWithCredential 的推送行為。這讓「token 不進 argv」、「清掉 checkout 既有 extraheader」、「遠端 URL 不符時拒絕」、「push 失敗不洩漏 URL/token」這些失敗與安全邊界都沒有被驗證。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 commitAndPushFindings 與 pushWithCredential 的推送策略、認證遮蔽、失敗路徑與 token 不外洩等測試缺口。" } ] diff --git a/.gitea/ai-review/findings/2026-07-21-16:30:12.json b/.gitea/ai-review/findings/2026-07-21-16:30:12.json new file mode 100644 index 0000000..a7d5f61 --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-21-16:30:12.json @@ -0,0 +1,280 @@ +{ + "generatedAt": "2026/07/21 16:30:12", + "commitSha": "43ad2b56d27429aa3c812522dc1d4fedce4a9b76", + "prNumber": 6, + "tool": { + "name": "codex", + "version": "codex-cli 0.144.6", + "model": "gpt-5.5" + }, + "findings": [ + { + "reviewer": "Mage", + "focus": "logic", + "badge": "🔮", + "severity": "嚴重", + "file": "src/index.js", + "startLine": 134, + "endLine": 139, + "problem": "這裡把「本輪審查」改成即使找到嚴重 finding 也回傳 0,並依賴後續 `[ai-review-bot][failure]` 結果 commit 觸發下一輪才讓檢查失敗。最小重現情境:PR 內有 1 條嚴重問題,但 `commitFindings()` 因分支保護、token 無 push 權限、遠端競態或 PAT 無法觸發 CI 而回傳 false;本輪仍成功結束,且沒有下一輪 failure commit 可被步驟 1 讀到,嚴重問題就被靜默放行。未驗證「結果 commit 一定成功且一定觸發下一輪」這個假設時,它就是流程成敗判定的單點失效。", + "suggestion": "嚴重 finding 已確認後,若 failure 結果 commit/push 沒有成功產生,就應在本輪直接回傳 1;只有在確認 failure commit 已成功推送時,才可把失敗狀態交給下一輪快速回報。也就是:`severe.length > 0 && !resultCommitPushed` 必須阻擋。", + "suggestedCode": "const result = severe.length === 0 ? 'success' : 'failure';\nlet pushed = false;\nif (filesToCommit.length > 0) {\n pushed = commitFindings({ cwd, ctx, files: filesToCommit, result });\n}\n\nif (severe.length > 0 && !pushed) {\n log('收尾', 'ERR', '已有嚴重問題,但 failure 結果 commit 未成功產生;本輪直接以失敗收場。');\n return 1;\n}\n\nreturn 0;", + "id": "F009", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。此條指控嚴重 finding 依賴結果 commit 觸發下一輪才失敗,且 commit/push 失敗時本輪可能靜默通過;既有排除事項未涵蓋此成敗判定風險。" + } + } + }, + { + "reviewer": "Mage", + "focus": "logic", + "badge": "🔮", + "severity": "警告", + "file": "src/index.js", + "startLine": 201, + "endLine": 209, + "problem": "`createIssueAndFlushBufferedComments()` 先建立 `trackingIssue`,再逐則寫入暫存留言;但只要其中一則留言失敗,外層 catch 會呼叫 `fallbackToPrComments()`,把整批 `pendingIssueCommentBodies` 全部改貼回 PR。最小重現情境:追蹤 issue 建立成功,第一則工具留言也成功寫入 issue,第二則 diff 留言 API 回 500;流程降級後 PR 會收到全部暫存留言,而已建立的 issue 仍殘留第一則留言、沒有後續 finding、也可能沒有 PR 連結或相依關係。這會留下部分成功、部分降級的不一致狀態。", + "suggestion": "flush 時應在每則留言成功後立刻從 pending 佇列移除,並明確處理「issue 已建立但 flush 失敗」的狀態:要嘛繼續沿用已建立 issue 並讓後續失敗冒泡,要嘛在降級前補一則 PR 診斷/連結並避免重貼已成功寫入 issue 的留言。", + "suggestedCode": "while (pendingIssueCommentBodies.length > 0) {\n const body = pendingIssueCommentBodies[0];\n await gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n pendingIssueCommentBodies.shift();\n}", + "id": "F010", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。既有排除事項涵蓋 issue 建立後留言失敗導致重跑重複 issue,但本條另指出 flush 部分成功後 fallback 會把整批 pending 留言改貼 PR,造成 issue 與 PR 狀態不一致;不是單純重複。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "action.yml", + "startLine": 3, + "endLine": 3, + "problem": "手動維護的「更新時間」已與本次檔案標示的最後更新時間不同步;樂譜開頭的拍號一錯,讀者後面每段註解都會多一分懷疑。同樣的不協調也出現在 `readme.md` 與 `src/index.js` 的 banner/文件時間。", + "suggestion": "移除這類容易走調的手動時間戳,或改由發布流程自動產生。若一定要保留,請讓所有檔案的時間標示與本次變更一致。", + "suggestedCode": "", + "id": "F001", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。已知排除事項與歷史 findings 未涵蓋手動更新時間不同步;目前也無足夠原始碼證據可判定為誤報。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/index.js", + "startLine": 199, + "endLine": 199, + "problem": "`createIssueAndFlushBufferedComments` 這個名稱把「建立 issue」與「flush 暫存留言」兩個實作細節硬串在一起,像一句過長的歌詞;呼叫點讀起來偏機械,沒有清楚表達業務意圖。", + "suggestion": "改成較語意化的命名,例如 `openTrackingIssue` 或 `createTrackingIssueWithContext`,讓讀者先理解目的,再從函式內容看見 flush 的細節。", + "suggestedCode": "", + "id": "F004", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。此條針對 createIssueAndFlushBufferedComments 的命名語意與可讀性,未明確命中既有排除事項;與先前 ensureIssueCreated 暗示冪等的問題不完全相同。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "建議", + "file": "src/lib/gitref.js", + "startLine": 12, + "endLine": 24, + "problem": "assertSafeBranchRef 目前只測了一個正常分支與一個 ../../ 路徑穿越案例;但這個函式承擔 refspec 安全邊界,新增的空值、前後斜線、反斜線、以及 git check-ref-format 拒絕的格式都沒有案例。邊界沒被驗證時,未來調整條件很容易放過不合法 ref。", + "suggestion": "補上表格測試,至少涵蓋空字串、純空白、/feature、feature/、feature\\\\x、feature..x、feature.lock、feature@{x},並斷言錯誤訊息能區分「不安全」與「不合法 git 分支名稱」。", + "suggestedCode": "", + "id": "F014", + "verdicts": { + "Paladin": { + "exclude": false, + "reason": "保留。此條針對 assertSafeBranchRef 的 refspec 安全邊界測試不足,未見已知排除事項或歷史 findings 明確涵蓋;證據不足以排除。" + } + } + } + ], + "excluded": [ + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "readme.md", + "startLine": 48, + "endLine": 48, + "problem": "Mermaid 節點代號從 `N8` 跳回 `N2`,雖然流程語意是「步驟 2 延後」,但原始碼讀起來像旋律突然倒帶;維護者在對照圖與步驟時容易被代號順序絆住。", + "suggestion": "節點 id 建議改用語意名稱,而把顯示文字保留步驟編號。例如 `ResolveOldComments[2 延後將舊留言標記解決...]`,讓圖的原始碼與顯示內容各自清楚。", + "suggestedCode": "", + "id": "F002", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已指出流程步驟編號與實際順序、README 流程圖及文件對照會產生認知落差;本條 Mermaid 節點代號問題屬同一類指控。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "警告", + "file": "src/index.js", + "startLine": 167, + "endLine": 235, + "problem": "`main()` 內新增了建 issue 模式狀態、暫存佇列與三個帶長篇 JSDoc 的閉包,讓主流程的旋律被大量伴奏蓋過。這段同時管理 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies` 與 PR 留言 id,可讀性負擔明顯升高。", + "suggestion": "把留言路由與建 issue 暫存邏輯抽成小型 helper(例如 `createCommentRouter`),`main()` 只保留流程編排;JSDoc 也移到 helper 旁,讓主流程步驟重新一眼可讀。", + "suggestedCode": "", + "id": "F003", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。已知排除事項與歷史 findings 已涵蓋 main() 內留言路由、issue 狀態、暫存佇列與發布職責耦合造成可讀性與維護負擔。" + } + } + }, + { + "reviewer": "Bard", + "focus": "style", + "badge": "🎼", + "severity": "建議", + "file": "src/lib/gitrepo.js", + "startLine": 121, + "endLine": 128, + "problem": "`strategies` 以陣列位置承載語意,`[label, ...args]` 雖簡短,卻讓每個元素的第二欄之後全靠讀者猜是 git argv;這裡的可讀性像沒有小節線的樂譜。", + "suggestion": "改成物件陣列,明確標出 `label` 與 `args`,可降低後續新增 fetch 策略時的閱讀成本。", + "suggestedCode": "const strategies = [\n { label: 'deepen base', args: ['fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`] },\n { label: 'deepen PR HEAD', args: ['fetch', '--no-tags', '--deepen=1000', 'origin', headSha] },\n];", + "id": "F005", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 finding 已針對 resolveMergeBase 內 strategies 與補救策略編排使閱讀節奏偏密提出問題;本條 tuple 可讀性屬同一段策略結構的細項。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 163, + "endLine": 231, + "problem": "`main()` 這次把一般模式/建問題模式/降級模式的留言去向都塞進閉包狀態:`issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 彼此耦合。半年後要新增一種落地方式或調整「何時清舊留言」時,維護者必須同時追多個可變狀態與後續多個 `if (issueModeActive)` 分支,很容易漏掉 flush、清空、登錄留言 id 或 fallback 後的收尾行為。", + "suggestion": "把留言目的地抽成小型物件,讓主流程只呼叫 `commentSink.postContext()`、`commentSink.finalizeAfterFindings()`、`commentSink.postFinding()` 這類語意方法。一般 PR、issue buffering、fallback 的狀態轉換集中在同一個模組或工廠函式內,`main()` 保持審查流程編排,不承擔留言狀態機。", + "suggestedCode": "", + "id": "F006", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。與 F003 及既有排除事項相同,均指向 main() 以可變閉包狀態承擔留言目的地、issue 生命週期與 fallback 狀態機。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "警告", + "file": "src/index.js", + "startLine": 232, + "endLine": 241, + "problem": "流程註解把「將舊留言標記為解決」稱為「步驟 2(延後執行)」,但實際執行點在審查結果產生後、接近原本步驟 9 前。這種「編號是 2、時間點是 8 後」的設計已經擴散到多個檔案的 JSDoc 與 log 描述,未來查 CI log 或維護流程圖時會產生認知落差:看到 `步驟2` 不代表它真的發生在第二步。", + "suggestion": "不要用固定序號承載流程語意。建議改成穩定階段名稱,例如 `清理舊留言`、`工具偵測`、`diff整理`,或重新編號為實際執行順序並在文件說明「清理舊留言延後到結果產生後」。這樣 log、JSDoc、README 流程圖才不會互相背離。", + "suggestedCode": "", + "id": "F007", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已明確記錄步驟編號散落於 log、JSDoc、README 且實際順序變成 1、3 到 8、2、9 到 10 的維護風險。" + } + } + }, + { + "reviewer": "Leo", + "focus": "maintainability", + "badge": "🧰", + "severity": "建議", + "file": "src/lib/gitrepo.js", + "startLine": 103, + "endLine": 160, + "problem": "`resolveMergeBase()` 內同時負責驗證 base ref、fetch base、重試 merge-base、補抓淺層歷史、累積診斷與組錯誤訊息。這些都和「取得 merge-base」有關,但現在全部攤在同一個函式裡,策略陣列、診斷格式與 git 操作細節混在一起;未來要新增 fetch 策略或改診斷文字時,容易不小心改壞主流程判斷。", + "suggestion": "把補抓策略與重試邏輯拆成私有 helper,例如 `fetchBaseRef()`、`mergeBaseOrNull()`、`shallowFetchStrategies()`。主函式保留高階流程:驗證 ref → 更新 base → 嘗試 merge-base → 逐一套用策略 → 丟出診斷錯誤。", + "suggestedCode": "", + "id": "F008", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 finding 已指出 resolveMergeBase 同時負責驗證、fetch 策略編排、診斷與錯誤包裝,難以分辨主流程與補救策略。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/index.js", + "startLine": 164, + "endLine": 232, + "problem": "建問題模式新增了「先暫存情境留言、確定有保留 finding 才建 issue、建 issue 失敗時降級回 PR 留言」這一整段流程,但目前測試只覆蓋了部分 helper,沒有驗證主流程在 create-issue=true 下的分流行為。這代表像「無保留問題時不應建 issue/不應留言」、「建立 issue 失敗時應把 pending 留言補回 PR」、「trackingIssue 建立後後續留言應改送 issue」這些行為都還沒通過試煉。", + "suggestion": "建議把 main 流程拆出可注入 gitea/review/agents/gitrepo 的 orchestrator,或至少匯出可測的流程函式,補上 create-issue=true 的案例:1. kept=[] 時不呼叫 createIssue/createIssueComment;2. kept>0 時先 flush pending issue comments 再發 finding;3. createIssue 拋錯時 fallbackToPrComments 會發回 PR 並記錄 currentRunCommentIds;4. issueModeActive 降級後會走一般 PR 留言與 resolveOldComments。", + "suggestedCode": "", + "id": "F011", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已涵蓋建問題模式核心流程、暫存留言、issue 建立條件、發布分流、外部 API 狀態切換與降級路徑缺少測試。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 104, + "endLine": 150, + "problem": "resolveMergeBase 新增了多階段 fetch/deepen/unshallow fallback 與診斷錯誤,但測試只驗證不安全 baseRef 會被拒絕,沒有驗證任何成功或失敗 fallback 路徑。這段是 diff 基準的核心邏輯,若 shallow checkout 下 deepen PR HEAD 用錯 ref、策略順序跑錯,或全部失敗時診斷不正確,現有測試都抓不到。", + "suggestion": "建議用假的 git 執行層或臨時 git repo 補測:首次 merge-base 成功時不跑後續策略;首次失敗但 deepen base 後成功;deepen base 失敗會繼續 deepen PR HEAD;shallow repo 才嘗試 --unshallow;全部失敗時錯誤訊息包含各策略診斷且保留 cause。", + "suggestedCode": "", + "id": "F012", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已針對 resolveMergeBase 多階段 fetch、淺層與非淺層分支、策略停止條件、降級與最終診斷缺少測試提出相同問題。" + } + } + }, + { + "reviewer": "Maya", + "focus": "testing", + "badge": "🧪", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 272, + "endLine": 322, + "problem": "commitAndPushFindings 改成一律透過 pushWithCredential 用 PAT extraheader 推送,且新增 headRef 安全檢查與固定錯誤遮蔽;但目前 gitrepo 測試沒有覆蓋 commitAndPushFindings/pushWithCredential 的推送行為。這讓「token 不進 argv」、「清掉 checkout 既有 extraheader」、「遠端 URL 不符時拒絕」、「push 失敗不洩漏 URL/token」這些失敗與安全邊界都沒有被驗證。", + "suggestion": "建議以 stub child_process.execFileSync 或可注入 git runner 的方式補測:commit 後呼叫 git push 的 argv 不含 token;env 內含 GIT_CONFIG_COUNT 與 Authorization extraheader;headRef 不安全時在 git add/commit/push 前拋錯;remote origin 不符與 push 失敗時錯誤訊息不包含 token、repo URL 或 Basic header。", + "suggestedCode": "", + "id": "F013", + "verdicts": { + "Paladin": { + "exclude": true, + "reason": "可排除(重複)。歷史 findings 已涵蓋 commitAndPushFindings 與 pushWithCredential 的推送策略、認證遮蔽、失敗路徑與 token 不外洩等測試缺口。" + } + } + } + ] +} -- 2.53.0 From 449b17480b1e6841b980f2dc924b1dca9b6d0852 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 17:21:57 +0800 Subject: [PATCH 77/79] =?UTF-8?q?fix(ai-review):=20=E7=B5=90=E6=9E=9C?= =?UTF-8?q?=E6=8E=A8=E9=80=81=E5=A4=B1=E6=95=97=E6=99=82=E5=9B=9E=E5=A0=B1?= =?UTF-8?q?=E5=B7=A5=E4=BD=9C=E6=B5=81=E5=A4=B1=E6=95=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- action.yml | 2 +- readme.md | 2 +- src/index.js | 37 ++++++++++++++++++------------------- src/lib/review.js | 13 +++++++++++++ test/gitrepo.test.js | 27 +++++++++++++++++++++++++++ test/review.test.js | 24 ++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 21 deletions(-) diff --git a/action.yml b/action.yml index e1ef5de..b3a3fd1 100644 --- a/action.yml +++ b/action.yml @@ -1,6 +1,6 @@ # ============================================================================ # 用途:定義 AI Code Review Node action 的名稱、輸入參數與 Node.js 24 進入點,供 Gitea / GitHub workflow 以 uses 引用。 -# 更新時間:2026/07/17 18:49:58 +# 更新時間:2026/07/21 17:19:15 # ============================================================================ # Gitea / GitHub node action 的 manifest(action.yml): # 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs), diff --git a/readme.md b/readme.md index daf0b4f..facea15 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # AI Code Review -> 更新時間:2026/07/17 18:49:58 +> 更新時間:2026/07/21 17:19:15 AI 多角色 code review 的 Gitea **node action**(`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]`/`[failure]`)供下次觸發快速回報。 diff --git a/src/index.js b/src/index.js index b750e16..c6957da 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ console.log('================================================'); console.log('Action : AI Code Review'); console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings'); -console.log('更新時間: 2026/07/17 18:49:58'); +console.log('更新時間: 2026/07/21 17:19:15'); console.log('================================================'); const fs = require('fs'); @@ -126,15 +126,14 @@ function commitFindings({ cwd, ctx, files, result }) { * 建問題模式會把審查情境與每條 finding 發到追蹤 issue;沒有保留 finding 時不建立 issue、PR 也不留言。 * 嚴重 finding 會寫入 failure 結果 commit,警告與建議只建立追蹤資訊,不直接阻擋合併。 * - * @returns {Promise} process exit code:本輪「審查」一律回傳 0(不因嚴重問題直接讓檢查失敗—— - * 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報); - * 回傳 1 僅發生於:步驟 1 偵測到 `[ai-review-bot][failure]` 結果 commit,或前置條件不足 - * (缺 PR 編號/token、找不到 AI 工具)等無法進行審查的情況。 + * @returns {Promise} process exit code:沒有嚴重問題且結果 commit 已成功推送時回傳 0; + * 若步驟 1 偵測到 `[ai-review-bot][failure]`、前置條件不足,或需要推送結果檔卻未成功推送, + * 則回傳 1,避免 token 權限不足或遠端競態讓 workflow 靜默通過。 * @remarks * 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))`; * 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記, * 讓下一次 workflow 觸發時重新完整審查。警告+建議等級不影響結果標記,只有「嚴重」會使結果 commit - * 標記為 failure;而「失敗檢查(exit 1)」只由步驟 1 讀到該 failure 結果 commit 時產生,審查本輪不直接 exit 1。 + * 標記為 failure;若結果檔需要 push 卻失敗,本輪直接 exit 1,避免缺權限時無聲放行。 * 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變上述結果標記判定。 */ async function main() { @@ -193,7 +192,7 @@ async function main() { return created; }; /** - * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立), + * 建問題模式:開啟追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立), * 並把 `pendingIssueCommentBodies` 內暫存的情境留言依流程順序寫入 issue; * 設定閉包變數 `trackingIssue` 供後續留言直接發到 issue。 * 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。 @@ -202,17 +201,18 @@ async function main() { * 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。 * @returns {Promise} 無回傳值;結果反映在閉包變數 `trackingIssue` 與 issue 留言。 */ - const createIssueAndFlushBufferedComments = async (labelIds = []) => { + const openTrackingIssue = async (labelIds = []) => { trackingIssue = await gitea.createIssue(ctx, { title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), labels: labelIds, }); log('建問題', 'INF', `已建立追蹤 issue #${trackingIssue.number},寫入 ${pendingIssueCommentBodies.length} 則情境留言。`); - for (const body of pendingIssueCommentBodies) { + while (pendingIssueCommentBodies.length > 0) { + const body = pendingIssueCommentBodies[0]; await gitea.createCommentOnIssue(ctx, trackingIssue.number, body); + pendingIssueCommentBodies.shift(); } - pendingIssueCommentBodies.length = 0; }; /** * 建問題模式降級:追蹤 issue 無法建立或寫入時,改把已暫存的情境留言發回 PR,後續沿用一般模式。 @@ -337,7 +337,7 @@ async function main() { log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`); } try { - await createIssueAndFlushBufferedComments(labelIds); + await openTrackingIssue(labelIds); } catch (err) { log('建問題', 'WRN', `建立或寫入追蹤 issue 失敗(${err.message}),改用 PR 留言與 findings 檔流程。`); await fallbackToPrComments(); @@ -377,7 +377,7 @@ async function main() { } // ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─ - // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。 + // 標籤已於建立 issue 時一次帶入(見上方 selectLabels → openTrackingIssue),此處不再補掛。 if (issueModeActive && trackingIssue) { await review.resolveOldIssueLinkComments({ ctx, gitea }); await gitea.createIssueComment( @@ -420,14 +420,13 @@ async function main() { } else { log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。'); } - // 本輪「審查」一律以成功收場、不直接讓檢查失敗;有嚴重問題時已推出 [failure] 結果 commit, - // 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果 - // 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。) + // 需要推送結果檔卻沒成功時直接失敗;這通常代表 token 權限不足、非快轉或分支保護阻擋。 + if (review.shouldFailMissingResultCommit({ filesToCommit, resultCommitted })) { + log('收尾', 'ERR', '審查結果檔需要 push 但未成功產生結果 commit;直接回報失敗避免缺權限時靜默通過。'); + return 1; + } + // 嚴重問題已推出 [failure] 結果 commit 時,由它再觸發的下一輪在步驟 1 讀 commit 訊息回報失敗。 if (result === 'failure') { - if (!resultCommitted) { - log('收尾', 'ERR', '本輪有嚴重問題,但未成功產生 [failure] 結果 commit;直接回報失敗避免 fail-open。'); - return 1; - } log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。'); } return 0; diff --git a/src/lib/review.js b/src/lib/review.js index 2c0af8f..51ef319 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -579,6 +579,18 @@ function resultFilesToCommit({ createIssue, severeCount, relativePath, exclusion return files; } +/** + * 判斷本輪是否因結果 commit 未成功推送而必須失敗。 + * + * @param {Object} params - 解構參數。 + * @param {string[]} params.filesToCommit - 本輪預期寫回遠端分支的結果檔清單。 + * @param {boolean} params.resultCommitted - 結果檔是否已成功 commit 並 push。 + * @returns {boolean} 需要寫回結果檔卻沒有成功推送時回傳 true。 + */ +function shouldFailMissingResultCommit({ filesToCommit, resultCommitted }) { + return filesToCommit.length > 0 && !resultCommitted; +} + /** * 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。 * @@ -914,6 +926,7 @@ module.exports = { sortFindings, appendExclusions, resultFilesToCommit, + shouldFailMissingResultCommit, selectLabels, postSevereToIssue, postOthersToIssue, diff --git a/test/gitrepo.test.js b/test/gitrepo.test.js index dcc9fc1..831f9dc 100644 --- a/test/gitrepo.test.js +++ b/test/gitrepo.test.js @@ -17,6 +17,33 @@ test('assertSafeBranchRef 拒絕路徑穿越分支名稱', () => { ); }); +test('assertSafeBranchRef 拒絕空白分支名稱', () => { + for (const value of ['', ' ', null, undefined]) { + assert.throws( + () => gitref.assertSafeBranchRef(value, 'baseRef'), + /baseRef 不可為空/, + ); + } +}); + +test('assertSafeBranchRef 拒絕不安全的 refspec 分支名稱', () => { + for (const value of ['/feature', 'feature/', 'feature\\x', 'feature..x']) { + assert.throws( + () => gitref.assertSafeBranchRef(value, 'baseRef'), + /不是安全的分支名稱/, + ); + } +}); + +test('assertSafeBranchRef 拒絕 git 不合法的分支名稱', () => { + for (const value of ['feature.lock', 'feature@{x}', 'feature~x', 'feature^x']) { + assert.throws( + () => gitref.assertSafeBranchRef(value, 'baseRef'), + /不是合法的 git 分支名稱/, + ); + } +}); + test('resolveMergeBase 會在 git fetch 前拒絕不安全 baseRef', () => { assert.throws( () => gitrepo.resolveMergeBase(process.cwd(), '../../hooks/pre-push'), diff --git a/test/review.test.js b/test/review.test.js index 8c97f9e..1364502 100644 --- a/test/review.test.js +++ b/test/review.test.js @@ -98,6 +98,30 @@ test('resultFilesToCommit 在建問題模式無嚴重問題時只提交 exclusio ); }); +test('shouldFailMissingResultCommit 在結果檔需要 push 但未成功時回報失敗', () => { + assert.equal( + review.shouldFailMissingResultCommit({ + filesToCommit: ['.gitea/ai-review/findings/run.json'], + resultCommitted: false, + }), + true, + ); + assert.equal( + review.shouldFailMissingResultCommit({ + filesToCommit: ['.gitea/ai-review/findings/run.json'], + resultCommitted: true, + }), + false, + ); + assert.equal( + review.shouldFailMissingResultCommit({ + filesToCommit: [], + resultCommitted: false, + }), + false, + ); +}); + test('resolveOldIssueLinkComments 只標註舊追蹤 issue 連結', async () => { const edited = []; const fakeGitea = { -- 2.53.0 From 44c33b6c1ecb66ad56423a49142d54397d6cf5fd Mon Sep 17 00:00:00 2001 From: Jeffery Date: Tue, 21 Jul 2026 17:21:57 +0800 Subject: [PATCH 78/79] =?UTF-8?q?chore(ai-review=20=E7=8B=80=E6=85=8B):=20?= =?UTF-8?q?=E5=9B=9E=E5=AF=AB=E6=9C=AC=E8=BC=AA=E5=B7=B2=E8=99=95=E7=90=86?= =?UTF-8?q?=20findings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../findings/2026-07-20-17:37:54.json | 39 -------- .../findings/2026-07-21-16:30:12.json | 98 +------------------ 2 files changed, 1 insertion(+), 136 deletions(-) diff --git a/.gitea/ai-review/findings/2026-07-20-17:37:54.json b/.gitea/ai-review/findings/2026-07-20-17:37:54.json index d413d8d..2818581 100644 --- a/.gitea/ai-review/findings/2026-07-20-17:37:54.json +++ b/.gitea/ai-review/findings/2026-07-20-17:37:54.json @@ -47,45 +47,6 @@ "suggestion": "補單元測試攔截 git 參數/env:斷言以 token 認證推送、推送目標 refspec 正確、呼叫次數,並確保任何拋出的錯誤、log 或快照都不含原始 token。屬測試架構決策。", "suggestedCode": "" }, - { - "id": "F005", - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "action.yml", - "startLine": 3, - "endLine": 3, - "problem": "檔頭「更新時間」為手動維護的固定字串,與實際檔案更新時間不一致;散落各檔的手動時間戳容易走調,讀者無法判斷可信度。", - "suggestion": "屬 jsc spec-time-log 慣例(各檔頭「更新時間」由 doc-funcs 流程統一產生/同步)。是否移除改用版控紀錄、或如何統一更新,宜由 doc-funcs 流程處理,不在 resolve 逐條硬改。", - "suggestedCode": "" - }, - { - "id": "F006", - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "readme.md", - "startLine": 3, - "endLine": 3, - "problem": "README 檔頭手動「更新時間」與實際更新時間不符,並與 action.yml、src/index.js 重複保存同類易過期資訊,形成多個不一致的真相來源。", - "suggestion": "同 F005:屬 jsc spec-time-log 慣例,交 doc-funcs 流程統一維護(移除或自動注入)。", - "suggestedCode": "" - }, - { - "id": "F007", - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "src/index.js", - "startLine": 7, - "endLine": 7, - "problem": "啟動橫幅硬編碼的「更新時間」與程式實際更新時間不一致,每次改程式都要人工校準,製造噪音並讓執行日誌呈現失真版本資訊。", - "suggestion": "同 F005:屬 jsc spec-time-log 慣例,交 doc-funcs 流程統一維護;若日誌需辨識版本,可改顯示 CI 注入的 commit SHA/版本號(屬慣例調整)。", - "suggestedCode": "" - }, { "id": "F008", "reviewer": "Bard", diff --git a/.gitea/ai-review/findings/2026-07-21-16:30:12.json b/.gitea/ai-review/findings/2026-07-21-16:30:12.json index a7d5f61..526f0df 100644 --- a/.gitea/ai-review/findings/2026-07-21-16:30:12.json +++ b/.gitea/ai-review/findings/2026-07-21-16:30:12.json @@ -7,103 +7,7 @@ "version": "codex-cli 0.144.6", "model": "gpt-5.5" }, - "findings": [ - { - "reviewer": "Mage", - "focus": "logic", - "badge": "🔮", - "severity": "嚴重", - "file": "src/index.js", - "startLine": 134, - "endLine": 139, - "problem": "這裡把「本輪審查」改成即使找到嚴重 finding 也回傳 0,並依賴後續 `[ai-review-bot][failure]` 結果 commit 觸發下一輪才讓檢查失敗。最小重現情境:PR 內有 1 條嚴重問題,但 `commitFindings()` 因分支保護、token 無 push 權限、遠端競態或 PAT 無法觸發 CI 而回傳 false;本輪仍成功結束,且沒有下一輪 failure commit 可被步驟 1 讀到,嚴重問題就被靜默放行。未驗證「結果 commit 一定成功且一定觸發下一輪」這個假設時,它就是流程成敗判定的單點失效。", - "suggestion": "嚴重 finding 已確認後,若 failure 結果 commit/push 沒有成功產生,就應在本輪直接回傳 1;只有在確認 failure commit 已成功推送時,才可把失敗狀態交給下一輪快速回報。也就是:`severe.length > 0 && !resultCommitPushed` 必須阻擋。", - "suggestedCode": "const result = severe.length === 0 ? 'success' : 'failure';\nlet pushed = false;\nif (filesToCommit.length > 0) {\n pushed = commitFindings({ cwd, ctx, files: filesToCommit, result });\n}\n\nif (severe.length > 0 && !pushed) {\n log('收尾', 'ERR', '已有嚴重問題,但 failure 結果 commit 未成功產生;本輪直接以失敗收場。');\n return 1;\n}\n\nreturn 0;", - "id": "F009", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。此條指控嚴重 finding 依賴結果 commit 觸發下一輪才失敗,且 commit/push 失敗時本輪可能靜默通過;既有排除事項未涵蓋此成敗判定風險。" - } - } - }, - { - "reviewer": "Mage", - "focus": "logic", - "badge": "🔮", - "severity": "警告", - "file": "src/index.js", - "startLine": 201, - "endLine": 209, - "problem": "`createIssueAndFlushBufferedComments()` 先建立 `trackingIssue`,再逐則寫入暫存留言;但只要其中一則留言失敗,外層 catch 會呼叫 `fallbackToPrComments()`,把整批 `pendingIssueCommentBodies` 全部改貼回 PR。最小重現情境:追蹤 issue 建立成功,第一則工具留言也成功寫入 issue,第二則 diff 留言 API 回 500;流程降級後 PR 會收到全部暫存留言,而已建立的 issue 仍殘留第一則留言、沒有後續 finding、也可能沒有 PR 連結或相依關係。這會留下部分成功、部分降級的不一致狀態。", - "suggestion": "flush 時應在每則留言成功後立刻從 pending 佇列移除,並明確處理「issue 已建立但 flush 失敗」的狀態:要嘛繼續沿用已建立 issue 並讓後續失敗冒泡,要嘛在降級前補一則 PR 診斷/連結並避免重貼已成功寫入 issue 的留言。", - "suggestedCode": "while (pendingIssueCommentBodies.length > 0) {\n const body = pendingIssueCommentBodies[0];\n await gitea.createCommentOnIssue(ctx, trackingIssue.number, body);\n pendingIssueCommentBodies.shift();\n}", - "id": "F010", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。既有排除事項涵蓋 issue 建立後留言失敗導致重跑重複 issue,但本條另指出 flush 部分成功後 fallback 會把整批 pending 留言改貼 PR,造成 issue 與 PR 狀態不一致;不是單純重複。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "action.yml", - "startLine": 3, - "endLine": 3, - "problem": "手動維護的「更新時間」已與本次檔案標示的最後更新時間不同步;樂譜開頭的拍號一錯,讀者後面每段註解都會多一分懷疑。同樣的不協調也出現在 `readme.md` 與 `src/index.js` 的 banner/文件時間。", - "suggestion": "移除這類容易走調的手動時間戳,或改由發布流程自動產生。若一定要保留,請讓所有檔案的時間標示與本次變更一致。", - "suggestedCode": "", - "id": "F001", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。已知排除事項與歷史 findings 未涵蓋手動更新時間不同步;目前也無足夠原始碼證據可判定為誤報。" - } - } - }, - { - "reviewer": "Bard", - "focus": "style", - "badge": "🎼", - "severity": "建議", - "file": "src/index.js", - "startLine": 199, - "endLine": 199, - "problem": "`createIssueAndFlushBufferedComments` 這個名稱把「建立 issue」與「flush 暫存留言」兩個實作細節硬串在一起,像一句過長的歌詞;呼叫點讀起來偏機械,沒有清楚表達業務意圖。", - "suggestion": "改成較語意化的命名,例如 `openTrackingIssue` 或 `createTrackingIssueWithContext`,讓讀者先理解目的,再從函式內容看見 flush 的細節。", - "suggestedCode": "", - "id": "F004", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。此條針對 createIssueAndFlushBufferedComments 的命名語意與可讀性,未明確命中既有排除事項;與先前 ensureIssueCreated 暗示冪等的問題不完全相同。" - } - } - }, - { - "reviewer": "Maya", - "focus": "testing", - "badge": "🧪", - "severity": "建議", - "file": "src/lib/gitref.js", - "startLine": 12, - "endLine": 24, - "problem": "assertSafeBranchRef 目前只測了一個正常分支與一個 ../../ 路徑穿越案例;但這個函式承擔 refspec 安全邊界,新增的空值、前後斜線、反斜線、以及 git check-ref-format 拒絕的格式都沒有案例。邊界沒被驗證時,未來調整條件很容易放過不合法 ref。", - "suggestion": "補上表格測試,至少涵蓋空字串、純空白、/feature、feature/、feature\\\\x、feature..x、feature.lock、feature@{x},並斷言錯誤訊息能區分「不安全」與「不合法 git 分支名稱」。", - "suggestedCode": "", - "id": "F014", - "verdicts": { - "Paladin": { - "exclude": false, - "reason": "保留。此條針對 assertSafeBranchRef 的 refspec 安全邊界測試不足,未見已知排除事項或歷史 findings 明確涵蓋;證據不足以排除。" - } - } - } - ], + "findings": [], "excluded": [ { "reviewer": "Bard", -- 2.53.0 From 25f76e6e6edf14e429a3fb022c1bfbbcc5c747c4 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Tue, 21 Jul 2026 09:25:39 +0000 Subject: [PATCH 79/79] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/exclusions.json | 88 ++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/.gitea/ai-review/exclusions.json b/.gitea/ai-review/exclusions.json index 40256e4..3d0256f 100644 --- a/.gitea/ai-review/exclusions.json +++ b/.gitea/ai-review/exclusions.json @@ -2099,5 +2099,93 @@ "endLine": 322, "problem": "commitAndPushFindings 改成一律透過 pushWithCredential 用 PAT extraheader 推送,且新增 headRef 安全檢查與固定錯誤遮蔽;但目前 gitrepo 測試沒有覆蓋 commitAndPushFindings/pushWithCredential 的推送行為。這讓「token 不進 argv」、「清掉 checkout 既有 extraheader」、「遠端 URL 不符時拒絕」、「push 失敗不洩漏 URL/token」這些失敗與安全邊界都沒有被驗證。", "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 commitAndPushFindings 與 pushWithCredential 的推送策略、認證遮蔽、失敗路徑與 token 不外洩等測試缺口。" + }, + { + "addedAt": "2026/07/21 17:25:38", + "prNumber": 6, + "reviewer": "Assassin", + "severity": "警告", + "file": "src/lib/diagnostics.js", + "startLine": 68, + "endLine": 71, + "problem": "攻擊者只要讓 AI CLI 失敗,並碰上 runner 開了 `ACTIONS_STEP_DEBUG=true`,就能把 CLI 的 stderr/stdout 片段推進長期保存的 CI log。這裡的遮罩是黑名單式,會漏掉不少常見秘密格式或個資,例如短 token、AWS access key、JWT 片段、email、電話、內部路徑與提示中夾帶的 diff 內容。安全邊界不能寄望「debug log 只有自己看」;repo 協作者或 CI log 讀者都可能取得這些輸出。", + "reason": "Paladin:可排除(重複)。既有紀錄已多次涵蓋 debug 模式輸出 AI CLI stderr/stdout、黑名單遮罩可繞過,以及機密或個資外洩風險;本條只是改到 diagnostics.js 後的同型指控。" + }, + { + "addedAt": "2026/07/21 17:25:38", + "prNumber": 6, + "reviewer": "Bard", + "severity": "建議", + "file": "readme.md", + "startLine": 41, + "endLine": 51, + "problem": "Mermaid 流程圖的節點 ID 從 `N3`、`N4` 一路走到 `N8`,中途又接回 `N2`。顯示文字雖然表達「步驟 2 延後」,但原始碼層面的節點命名逆行,讓文件維護者讀圖時節奏斷裂。", + "reason": "Paladin:可排除(重複)。歷史 finding 已指出流程步驟編號被當成跨模組識別值、README 流程圖出現 1、3~8、2、9~10 的逆序維護問題;本條屬同一文件編號耦合問題。" + }, + { + "addedAt": "2026/07/21 17:25:38", + "prNumber": 6, + "reviewer": "Leo", + "severity": "警告", + "file": "src/index.js", + "startLine": 162, + "endLine": 392, + "problem": "`main()` 這次把建問題模式的狀態機、留言緩衝、issue 建立、fallback、舊留言清理、結果 commit 判定都塞進同一個流程函式與多個閉包裡。半年後要改其中一條路徑時,很難確認 `issueModeActive`、`trackingIssue`、`pendingIssueCommentBodies`、`currentRunCommentIds` 在各分支是否仍一致;尤其建 issue 失敗後降級回 PR 留言,後面又要決定清舊留言、發嚴重問題、提交 findings,維護者需要整段流程一起讀才敢動。", + "reason": "Paladin:可排除(命中已知排除事項且重複)。已知排除與歷史 findings 已涵蓋 main() 內建問題模式、留言路由、issue 狀態、緩衝佇列與發布職責耦合的問題。" + }, + { + "addedAt": "2026/07/21 17:25:38", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "src/lib/gitea.js", + "startLine": 156, + "endLine": 160, + "problem": "文件註解提到 `main()` 的 `createIssueAndFlushBufferedComments`,但這次新增的實際閉包名稱是 `openTrackingIssue`。這類失準的內部函式名引用會讓未來維護者循線找不到程式碼,久了文件會變成負債。", + "reason": "Paladin:可排除(列表內重複)。與 F002 指涉 src/lib/gitea.js 同一段 JSDoc 引用不存在的 createIssueAndFlushBufferedComments 問題。" + }, + { + "addedAt": "2026/07/21 17:25:38", + "prNumber": 6, + "reviewer": "Leo", + "severity": "建議", + "file": "src/lib/templates.js", + "startLine": 302, + "endLine": 305, + "problem": "`issueBody()` 的 JSDoc 同樣引用不存在的 `createIssueAndFlushBufferedComments`。模板模組本來應該只說明模板用途;引用外層流程的私有閉包名稱,會讓文件跟主流程重構強耦合。", + "reason": "Paladin:可排除(列表內重複)。與 F003 指涉 src/lib/templates.js 的 issueBody JSDoc 引用不存在內部閉包名稱,屬同一問題。" + }, + { + "addedAt": "2026/07/21 17:25:38", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/index.js", + "startLine": 162, + "endLine": 239, + "problem": "建問題模式新增了 `queueOrPostComment`、`openTrackingIssue`、`fallbackToPrComments` 這組分流行為,但目前測試只覆蓋了部分 `review` helper,沒有驗證主流程在 issue 尚未建立時會暫存留言、建立成功後會依序 flush、建立或寫入失敗時會回退到 PR 留言。這些都是本次新增的失敗路徑,沒測到時很容易出現審查內容被靜默丟掉或留言落錯地方。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋建問題模式暫存留言、建立後依序 flush、建立或 API 失敗時降級,以及無 finding 靜默通過等核心流程缺少測試。" + }, + { + "addedAt": "2026/07/21 17:25:38", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 103, + "endLine": 158, + "problem": "`resolveMergeBase` 新增了多段 fetch 補抓策略、淺層 repo 判斷、每次 fetch 後重試 merge-base,以及全部失敗時附診斷的錯誤;目前測試只驗證不安全 `baseRef` 會在 fetch 前被拒絕,沒有測到這些新增分支。這些邊界正是 shallow checkout 最容易壞的地方。", + "reason": "Paladin:可排除(重複)。歷史 findings 已針對 resolveMergeBase 的多階段 fetch、淺層與非淺層分支、停止條件、降級與最終診斷缺少測試提出相同問題。" + }, + { + "addedAt": "2026/07/21 17:25:38", + "prNumber": 6, + "reviewer": "Maya", + "severity": "警告", + "file": "src/lib/gitrepo.js", + "startLine": 287, + "endLine": 326, + "problem": "`pushWithCredential` 是新增的認證推送核心路徑,包含清掉 checkout 既有 extraheader、用 PAT extraheader 推送、遠端 URL origin 檢查,以及失敗時隱藏 URL/token;但目前沒有任何測試驗證這些行為。這裡的失敗路徑沒測到,容易在 runner 上才發現結果 commit 推不上去或錯誤訊息洩漏敏感資訊。", + "reason": "Paladin:可排除(重複)。歷史 findings 已涵蓋 pushToken/pushWithCredential、origin 與一般 token 推送策略、認證遮蔽及失敗路徑缺少測試。" } ] -- 2.53.0