Compare commits
4
Commits
d354f30c27
...
d8dd5ce92e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8dd5ce92e | ||
|
|
cc9c8afed7 | ||
|
|
0c108ab7f9 | ||
|
|
83dd56f37f |
@@ -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 失敗。"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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": []
|
||||
}
|
||||
+10
-10
@@ -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<void>} 無回傳值;結果反映在閉包變數 `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,
|
||||
|
||||
+2
-2
@@ -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<object>} 建立成功的 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。
|
||||
*/
|
||||
|
||||
+4
-1
@@ -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']);
|
||||
|
||||
+4
-2
@@ -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 執行失敗(無診斷輸出)');
|
||||
}
|
||||
|
||||
@@ -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,再從下方留言逐條查看問題明細。
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user