feat(ai-code-review): 建問題模式導向 issue,並整併流程調整、文件與 CI #16

Closed
opened 2026-07-20 09:19:08 +00:00 by admin · 7 comments
Owner

變更摘要

本 PR 將 ai-code-review action 分支的完整成果併入 develop,主要含四塊:

  1. 建問題模式重構(feat)create-issue: 'true' 時,審查留言(工具/diff/角色/嚴重問題/警告+建議)全部改發到追蹤 issue、不再張貼到 PR;不執行「舊留言標過時/resolve」;issue 延後到「確定有保留問題」才建立;無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過);有問題時最後在 PR 回貼一則 issue 連結並補掛標籤,形成雙向關聯。
  2. 審查流程步驟調整(refactor):將「標記舊留言過時/resolve」提前到偵測 AI 工具之前,並依新順序重編所有步驟編號與說明。
  3. 修正(gitrepo):補抓淺層 checkout 的 base 歷史,避免 merge-base 計算失敗。
  4. 文件與 CI:補齊 action.ymlsrc 各模組文件、重建 readme.md,新增 .gitea/workflows/ci.yaml(多工具 code review 驗證)並啟用建問題模式。

影響範圍

範圍 說明
action 執行流程 src/index.js main() 依模式分流:留言去向、跳過 resolveOldComments、issue 延後建立、severe/others 導向 issue、有問題才在 PR 回貼連結
Gitea API src/lib/gitea.js 新增 addLabelsToIssue
留言模板 src/lib/templates.js 新增 issueLinkComment
審查核心 src/lib/review.js 新增 postSevereToIssue,移除 createIssueWithFindingssortFindingsForIssue
git base 解析 src/lib/gitrepo.js 補抓淺層 checkout 的 base 歷史
文件與 CI readme.mdaction.yml.gitea/workflows/ci.yaml.gitea/workflows/readme.md

風險與注意事項

  • 建問題模式行為明顯改變:啟用後 PR 上不再有審查留言(有問題時改為 issue + 一則連結留言;無問題時完全不留言),請確認團隊流程可接受。
  • 端對端行為需靠 CI 實測:本地僅完成語法與模組載入驗證,實際 issue/PR 互動需一次 CI 觸發確認。
  • readme.md 尚未反映「建問題模式導向 issue」的最新行為與新/移除的函式,建議後續以 doc-funcs 重建文件。
  • 各檔頭部「更新時間」時間戳仍為 2026/07/17 18:49:58,未刷新。

本問題由 AI Code Review 依 PR #6 的審查結果自動建立,問題明細見下方留言。

<!-- ai-code-review --> ## 變更摘要 本 PR 將 ai-code-review action 分支的完整成果併入 `develop`,主要含四塊: 1. **建問題模式重構(feat)**:`create-issue: 'true'` 時,審查留言(工具/diff/角色/嚴重問題/警告+建議)全部改發到追蹤 issue、不再張貼到 PR;不執行「舊留言標過時/resolve」;issue 延後到「確定有保留問題」才建立;**無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過)**;有問題時最後在 PR 回貼一則 issue 連結並補掛標籤,形成雙向關聯。 2. **審查流程步驟調整(refactor)**:將「標記舊留言過時/resolve」提前到偵測 AI 工具之前,並依新順序重編所有步驟編號與說明。 3. **修正(gitrepo)**:補抓淺層 checkout 的 base 歷史,避免 merge-base 計算失敗。 4. **文件與 CI**:補齊 `action.yml`/`src` 各模組文件、重建 `readme.md`,新增 `.gitea/workflows/ci.yaml`(多工具 code review 驗證)並啟用建問題模式。 ## 影響範圍 | 範圍 | 說明 | | --- | --- | | action 執行流程 | `src/index.js` `main()` 依模式分流:留言去向、跳過 resolveOldComments、issue 延後建立、severe/others 導向 issue、有問題才在 PR 回貼連結 | | Gitea API | `src/lib/gitea.js` 新增 `addLabelsToIssue` | | 留言模板 | `src/lib/templates.js` 新增 `issueLinkComment` | | 審查核心 | `src/lib/review.js` 新增 `postSevereToIssue`,移除 `createIssueWithFindings`、`sortFindingsForIssue` | | git base 解析 | `src/lib/gitrepo.js` 補抓淺層 checkout 的 base 歷史 | | 文件與 CI | `readme.md`、`action.yml`、`.gitea/workflows/ci.yaml`、`.gitea/workflows/readme.md` | ## 風險與注意事項 - **建問題模式行為明顯改變**:啟用後 PR 上不再有審查留言(有問題時改為 issue + 一則連結留言;無問題時完全不留言),請確認團隊流程可接受。 - **端對端行為需靠 CI 實測**:本地僅完成語法與模組載入驗證,實際 issue/PR 互動需一次 CI 觸發確認。 - `readme.md` 尚未反映「建問題模式導向 issue」的最新行為與新/移除的函式,建議後續以 doc-funcs 重建文件。 - 各檔頭部「更新時間」時間戳仍為 `2026/07/17 18:49:58`,未刷新。 --- > 本問題由 AI Code Review 依 PR #6 的審查結果自動建立,問題明細見下方留言。
Author
Owner

🤖 AI Code Review|審查工具

項目 內容
工具 antigravity
版本 1.1.4
模型 gemini-3.5-flash
審查 commit 321a717f91303bd4b865eacb0d11546ef9e79e11
Run Job #33
flowchart LR
    A[整理 git diff] --> B[⚔️ 攻擊方找問題]
    B --> C[🛡️ 防守方裁決]
    C --> D[保存 findings]
    D --> E[留言到 PR]
<!-- ai-code-review --> ## 🤖 AI Code Review|審查工具 | 項目 | 內容 | | --- | --- | | 工具 | `antigravity` | | 版本 | `1.1.4` | | 模型 | `gemini-3.5-flash` | | 審查 commit | `321a717f91303bd4b865eacb0d11546ef9e79e11` | | Run Job | [#33](https://gitea.jsc.idv.tw/node-actions/ai-code-review/actions/runs/1602) | ```mermaid flowchart LR A[整理 git diff] --> B[⚔️ 攻擊方找問題] B --> C[🛡️ 防守方裁決] C --> D[保存 findings] D --> E[留言到 PR] ```
Author
Owner

📋 變更摘要(送審 git diff)

檔案 用途 git diff 長度 最後更新時間
action.yml 定義 Action 的輸入參數與執行進入點 95 行/4205 字元 2026/07/20 15:00:23
readme.md 介紹專案用途、執行流程與使用方式的說明文件 276 行/23829 字元(過長截斷送審) 2026/07/20 16:01:05
src/index.js Action 的主程式進入點,控制整個審查流程 320 行/14204 字元 2026/07/20 15:00:15
src/lib/agents.js 偵測與執行 AI 審查工具的相關模組 14 行/503 字元 2026/07/20 09:46:12
src/lib/context.js 解析與管理 Action 執行時的上下文與輸入參數 31 行/1398 字元 2026/07/20 15:00:23
src/lib/gitea.js 呼叫 Gitea REST API 進行 PR 留言與操作的模組 121 行/5278 字元 2026/07/20 16:01:05
src/lib/gitrepo.js 封裝 Git 操作以取得比較基準與提交審查結果 222 行/9110 字元 2026/07/20 16:01:05
src/lib/review.js 處理程式碼審查的核心邏輯與代理人失敗診斷 580 行/24901 字元(過長截斷送審) 2026/07/20 16:37:15
src/lib/roles.js 載入與管理審查流程中攻擊方與防守方的角色設定 14 行/586 字元 2026/07/20 09:46:12
src/lib/templates.js 定義 PR 留言與審查報告所使用的 Markdown 模板 165 行/5949 字元 2026/07/20 15:00:32

共 10 個檔案納入審查;另有 7 個檔案依 .reviewignore 排除。

<!-- ai-code-review --> ## 📋 變更摘要(送審 git diff) | 檔案 | 用途 | git diff 長度 | 最後更新時間 | | --- | --- | --- | --- | | `action.yml` | 定義 Action 的輸入參數與執行進入點 | 95 行/4205 字元 | 2026/07/20 15:00:23 | | `readme.md` | 介紹專案用途、執行流程與使用方式的說明文件 | 276 行/23829 字元(過長截斷送審) | 2026/07/20 16:01:05 | | `src/index.js` | Action 的主程式進入點,控制整個審查流程 | 320 行/14204 字元 | 2026/07/20 15:00:15 | | `src/lib/agents.js` | 偵測與執行 AI 審查工具的相關模組 | 14 行/503 字元 | 2026/07/20 09:46:12 | | `src/lib/context.js` | 解析與管理 Action 執行時的上下文與輸入參數 | 31 行/1398 字元 | 2026/07/20 15:00:23 | | `src/lib/gitea.js` | 呼叫 Gitea REST API 進行 PR 留言與操作的模組 | 121 行/5278 字元 | 2026/07/20 16:01:05 | | `src/lib/gitrepo.js` | 封裝 Git 操作以取得比較基準與提交審查結果 | 222 行/9110 字元 | 2026/07/20 16:01:05 | | `src/lib/review.js` | 處理程式碼審查的核心邏輯與代理人失敗診斷 | 580 行/24901 字元(過長截斷送審) | 2026/07/20 16:37:15 | | `src/lib/roles.js` | 載入與管理審查流程中攻擊方與防守方的角色設定 | 14 行/586 字元 | 2026/07/20 09:46:12 | | `src/lib/templates.js` | 定義 PR 留言與審查報告所使用的 Markdown 模板 | 165 行/5949 字元 | 2026/07/20 15:00:32 | > 共 10 個檔案納入審查;另有 7 個檔案依 `.reviewignore` 排除。
Author
Owner

⚔️ 攻擊方登場

角色 面向 個性
🗡️ Assassin 安全性(security) 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用
🎼 Bard 風格(style) 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版
🧰 Leo 可維護性(maintainability) 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來
🔮 Mage 邏輯(logic) 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」
🧪 Maya 測試(testing) 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑
Rogue 效率(efficiency) 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」
<!-- ai-code-review --> ## ⚔️ 攻擊方登場 | 角色 | 面向 | 個性 | | --- | --- | --- | | 🗡️ **Assassin** | 安全性(security) | 多疑偏執、以攻擊者視角看世界,假設每筆輸入都是惡意的,每個信任都會被濫用 | | 🎼 **Bard** | 風格(style) | 唯美龜毛、追求優雅,把可讀性與一致性當作旋律,最受不了走調的命名與排版 | | 🧰 **Leo** | 可維護性(maintainability) | 有遠見、重視長期維護成本,凡事先問「六個月後的自己還看得懂嗎?」,討厭把債留給未來 | | 🔮 **Mage** | 邏輯(logic) | 嚴謹冷靜、滴水不漏,凡事推演到最壞情況,深信「沒驗證過的假設都是 bug」 | | 🧪 **Maya** | 測試(testing) | 對測試覆蓋率有執念,深信「沒有測試的程式碼等於沒寫完」,溫和但堅持,最在意邊界與失敗路徑 | | ⚡ **Rogue** | 效率(efficiency) | 急性子、講求速度,最痛恨被浪費的 CPU 週期與記憶體,凡事先問「這能不能更快、更省」 |
Author
Owner

🛡️ 防守方登場

角色 面向 個性
🛡️ Paladin 裁決(verdict) 沉穩公正、就事論事,不護短也不冤枉,只依排除事項與原始碼脈絡裁定問題成立與否
<!-- ai-code-review --> ## 🛡️ 防守方登場 | 角色 | 面向 | 個性 | | --- | --- | --- | | 🛡️ **Paladin** | 裁決(verdict) | 沉穩公正、就事論事,不護短也不冤枉,只依排除事項與原始碼脈絡裁定問題成立與否 |
Author
Owner

🔴 嚴重|🗡️ Assassin

位置src/lib/gitrepo.js 第 102–149 行

問題描述

攻擊者可以透過提交惡意 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 也存在類似的拼接風險。

修改建議

在將 baseRefheadRef 傳入 git 指令之前,應進行嚴格的合法性檢查。建議使用正則表達式限制分支名稱僅能包含安全的字元(如英數字、斜線、底線、連字號、句點),且絕對不得含有 .. 或以 - 開頭,必要時亦可使用 git check-ref-format 命令先行驗證該分支名稱是否安全。

建議寫法

function resolveMergeBase(cwd, baseRef) {
  // 嚴格的分支名稱白名單檢查,防止路徑穿越與參數注入
  const safeBranchRegex = /^(?!-)(?!.*?\.\.)[a-zA-Z0-9/_.-]+$/;
  if (!safeBranchRegex.test(baseRef)) {
    throw new Error(`偵測到不合法的分支名稱: ${baseRef}`);
  }
  const remoteBase = `origin/${baseRef}`;
<!-- ai-code-review --> ### 🔴 嚴重|🗡️ Assassin **位置**:`src/lib/gitrepo.js` 第 102–149 行 **問題描述** 攻擊者可以透過提交惡意 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` 也存在類似的拼接風險。 **修改建議** 在將 `baseRef` 與 `headRef` 傳入 git 指令之前,應進行嚴格的合法性檢查。建議使用正則表達式限制分支名稱僅能包含安全的字元(如英數字、斜線、底線、連字號、句點),且絕對不得含有 `..` 或以 `-` 開頭,必要時亦可使用 `git check-ref-format` 命令先行驗證該分支名稱是否安全。 **建議寫法** ``` function resolveMergeBase(cwd, baseRef) { // 嚴格的分支名稱白名單檢查,防止路徑穿越與參數注入 const safeBranchRegex = /^(?!-)(?!.*?\.\.)[a-zA-Z0-9/_.-]+$/; if (!safeBranchRegex.test(baseRef)) { throw new Error(`偵測到不合法的分支名稱: ${baseRef}`); } const remoteBase = `origin/${baseRef}`; ```
Author
Owner

🟠 警告|🗡️ Assassin

位置src/lib/review.js 第 16–82 行

問題描述

當 AI 代理工具執行失敗時,agentFailureDetail 預設會將 stderr 和 stdout 的前 500 字元經由 redactSecrets 遮蔽後輸出到 CI 日誌中。然而,redactSecrets 採用的是黑名單式的正則表達式,若 agent 輸出中包含非標準格式的 API 金鑰(如短於 40 字元的 secret、自定義 Token 標頭)、資料庫連線字串或敏感個資(PII),此種黑名單防線極易被繞過,進而將敏感機密永久暴露於公開的 CI 日誌中。

修改建議

建議在預設情況下,CLI 失敗時只輸出 Exit Code 與錯誤類型的診斷資訊,而不主動傾印 stdout/stderr 的內容。只有當明確啟用 CI 偵錯模式(例如 process.env.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}`);
  }
  
  // 僅在 Debug 模式下才輸出 stderr 與 stdout 片段,以降低機密外洩風險
  if (process.env.ACTIONS_STEP_DEBUG === 'true') {
    const INPUT_LIMIT = 2_000;
    const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT));
    if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`);
    const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT));
    if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`);
  } else {
    parts.push('詳細錯誤已隱藏,請開啟 ACTIONS_STEP_DEBUG 重跑以檢視。');
  }
  
  if (parts.length === 0) {
    parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)');
  }
  return parts.join('|');
}
<!-- ai-code-review --> ### 🟠 警告|🗡️ Assassin **位置**:`src/lib/review.js` 第 16–82 行 **問題描述** 當 AI 代理工具執行失敗時,`agentFailureDetail` 預設會將 stderr 和 stdout 的前 500 字元經由 `redactSecrets` 遮蔽後輸出到 CI 日誌中。然而,`redactSecrets` 採用的是黑名單式的正則表達式,若 agent 輸出中包含非標準格式的 API 金鑰(如短於 40 字元的 secret、自定義 Token 標頭)、資料庫連線字串或敏感個資(PII),此種黑名單防線極易被繞過,進而將敏感機密永久暴露於公開的 CI 日誌中。 **修改建議** 建議在預設情況下,CLI 失敗時只輸出 Exit Code 與錯誤類型的診斷資訊,而不主動傾印 stdout/stderr 的內容。只有當明確啟用 CI 偵錯模式(例如 `process.env.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}`); } // 僅在 Debug 模式下才輸出 stderr 與 stdout 片段,以降低機密外洩風險 if (process.env.ACTIONS_STEP_DEBUG === 'true') { const INPUT_LIMIT = 2_000; const stderr = redactSecrets(String((res && res.stderr) || '').slice(0, INPUT_LIMIT)); if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`); const stdout = redactSecrets(String((res && res.output) || '').slice(0, INPUT_LIMIT)); if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`); } else { parts.push('詳細錯誤已隱藏,請開啟 ACTIONS_STEP_DEBUG 重跑以檢視。'); } if (parts.length === 0) { parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(無診斷輸出)'); } return parts.join('|'); } ```
Member

🧩 code-review-resolve 處理進度

本議題為 AI Code Review 建問題模式的追蹤議題。以 --issue all 併同 .gitea/ai-review/findings/ 逐條處理後結果如下(對照目前程式碼與 exclusions.json):

# 等級 審查員 位置 處理結果 說明
1 🔴 嚴重 Assassin src/lib/gitrepo.js 第 102–149 行 ⏭️待人工 baseRef 直接拼進 fetch refspec,但 baseRef 為 PR 目標分支且經 execFileSync 無 shell 注入,是否構成路徑穿越屬安全邊界研判,依基準第三節不得硬改。
2 🟠 警告 Assassin src/lib/review.js 第 16–82 行 🚫誤報 agentFailureDetail 以 redactSecrets 遮罩後寫入 stderr/stdout 至 CI log 的安全風險,已列入 exclusions,不重複裁決。

小計 已解決 0 條、🚫 誤報(已列入 exclusions)1 條、⏭️ 待人工處理 1 條。

⏭️ 待人工處理項目屬設計/效能/慣例取捨(skill 規範不得硬改),已將「只來自議題」的待人工問題(依主題去重)寫回 .gitea/ai-review/findings/2026-07-20-18:31:07.json 追蹤,關閉本議題不會遺失待辦。

  • 已解決:問題於目前程式碼已修復(如 resolveMergeBase 補抓策略調整、deepen PR HEAD 改用 head SHA、留言閉包改名、push-token input 移除等)。
  • 🚫 誤報:與 .gitea/ai-review/exclusions.json 既有裁決等價,不重複新增排除條目。

處理完成,依 code-review-resolve 流程關閉本議題。

<!-- code-review-resolve --> ## 🧩 code-review-resolve 處理進度 本議題為 AI Code Review 建問題模式的追蹤議題。以 `--issue all` 併同 `.gitea/ai-review/findings/` 逐條處理後結果如下(對照目前程式碼與 `exclusions.json`): | # | 等級 | 審查員 | 位置 | 處理結果 | 說明 | | --- | --- | --- | --- | --- | --- | | 1 | 🔴 嚴重 | Assassin | `src/lib/gitrepo.js` 第 102–149 行 | ⏭️待人工 | baseRef 直接拼進 fetch refspec,但 baseRef 為 PR 目標分支且經 execFileSync 無 shell 注入,是否構成路徑穿越屬安全邊界研判,依基準第三節不得硬改。 | | 2 | 🟠 警告 | Assassin | `src/lib/review.js` 第 16–82 行 | 🚫誤報 | agentFailureDetail 以 redactSecrets 遮罩後寫入 stderr/stdout 至 CI log 的安全風險,已列入 exclusions,不重複裁決。 | **小計**:✅ 已解決 0 條、🚫 誤報(已列入 exclusions)1 條、⏭️ 待人工處理 1 條。 > ⏭️ 待人工處理項目屬設計/效能/慣例取捨(skill 規範不得硬改),已將「只來自議題」的待人工問題(依主題去重)寫回 `.gitea/ai-review/findings/2026-07-20-18:31:07.json` 追蹤,關閉本議題不會遺失待辦。 - ✅ 已解決:問題於目前程式碼已修復(如 `resolveMergeBase` 補抓策略調整、`deepen PR HEAD` 改用 head SHA、留言閉包改名、`push-token` input 移除等)。 - 🚫 誤報:與 `.gitea/ai-review/exclusions.json` 既有裁決等價,不重複新增排除條目。 處理完成,依 code-review-resolve 流程關閉本議題。
Sign in to join this conversation.
No labels
2 Participants
Notifications
Due Date
No due date set.
Reference: node-actions/ai-code-review#16