Compare commits
3
Commits
24b248884f
...
9e7f8a2a0a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e7f8a2a0a | ||
|
|
8b50a0d643 | ||
|
|
555611db07 |
@@ -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```",
|
"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
|
"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",
|
"id": "F022",
|
||||||
"reviewer": "Leo",
|
"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```",
|
"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
|
"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",
|
"id": "F029",
|
||||||
"reviewer": "Bard",
|
"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```",
|
"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
|
"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",
|
"id": "F040",
|
||||||
"reviewer": "Assassin",
|
"reviewer": "Assassin",
|
||||||
|
|||||||
+15
-10
@@ -210,7 +210,7 @@ async function main() {
|
|||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
|
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
|
||||||
* 並把 `issueBuffer` 內暫存的情境留言批次寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
|
* 並把 `issueBuffer` 內暫存的情境留言依流程順序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
|
||||||
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
|
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
|
||||||
*
|
*
|
||||||
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
|
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
|
||||||
@@ -224,9 +224,9 @@ async function main() {
|
|||||||
labels: labelIds,
|
labels: labelIds,
|
||||||
});
|
});
|
||||||
log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`);
|
log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`);
|
||||||
await Promise.all(
|
for (const body of issueBuffer) {
|
||||||
issueBuffer.map((body) => gitea.createCommentOnIssue(ctx, issue.number, body)),
|
await gitea.createCommentOnIssue(ctx, issue.number, body);
|
||||||
);
|
}
|
||||||
issueBuffer.length = 0;
|
issueBuffer.length = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -302,7 +302,9 @@ async function main() {
|
|||||||
await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
|
await queueOrPostComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders }));
|
||||||
|
|
||||||
// ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ──────────────────
|
// ── 步驟 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);
|
review.sortFindings(kept);
|
||||||
const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded });
|
const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded });
|
||||||
|
|
||||||
@@ -398,12 +400,15 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)───────
|
// ── 收尾: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 result = severe.length === 0 ? 'success' : 'failure';
|
||||||
const filesToCommit = ctx.createIssue ? [] : [relativePath];
|
const filesToCommit = review.resultFilesToCommit({
|
||||||
if (exclusionsChanged) {
|
createIssue: ctx.createIssue,
|
||||||
filesToCommit.push(path.join('.gitea', 'ai-review', 'exclusions.json'));
|
severeCount: severe.length,
|
||||||
}
|
relativePath,
|
||||||
|
exclusionsChanged,
|
||||||
|
});
|
||||||
if (filesToCommit.length > 0) {
|
if (filesToCommit.length > 0) {
|
||||||
commitFindings({ cwd, ctx, files: filesToCommit, result });
|
commitFindings({ cwd, ctx, files: filesToCommit, result });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
+24
-74
@@ -5,6 +5,7 @@ const path = require('path');
|
|||||||
|
|
||||||
const { log, taipeiFromIso, taipeiNow } = require('./log');
|
const { log, taipeiFromIso, taipeiNow } = require('./log');
|
||||||
const { runAgent, extractJson } = require('./agents');
|
const { runAgent, extractJson } = require('./agents');
|
||||||
|
const { agentFailureDetail } = require('./diagnostics');
|
||||||
const templates = require('./templates');
|
const templates = require('./templates');
|
||||||
|
|
||||||
// 審查流程核心:.reviewignore 過濾、diff 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。
|
// 審查流程核心:.reviewignore 過濾、diff 整理、攻擊方找問題、防守方裁決、排序分組與舊留言處理。
|
||||||
@@ -13,76 +14,6 @@ const templates = require('./templates');
|
|||||||
const PER_FILE_DIFF_LIMIT = 16_000;
|
const PER_FILE_DIFF_LIMIT = 16_000;
|
||||||
const TOTAL_DIFF_LIMIT = 160_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`,解析為忽略路徑前綴清單。
|
* 讀取工作目錄下的 `.reviewignore`,解析為忽略路徑前綴清單。
|
||||||
*
|
*
|
||||||
@@ -626,6 +557,28 @@ function appendExclusions({ cwd, excluded, prNumber }) {
|
|||||||
return true;
|
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:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。
|
* 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。
|
||||||
*
|
*
|
||||||
@@ -926,13 +879,10 @@ module.exports = {
|
|||||||
runDefenders,
|
runDefenders,
|
||||||
sortFindings,
|
sortFindings,
|
||||||
appendExclusions,
|
appendExclusions,
|
||||||
|
resultFilesToCommit,
|
||||||
selectLabels,
|
selectLabels,
|
||||||
postSevereToIssue,
|
postSevereToIssue,
|
||||||
postOthersToIssue,
|
postOthersToIssue,
|
||||||
resolveOldComments,
|
resolveOldComments,
|
||||||
postSevereComments,
|
postSevereComments,
|
||||||
__test: {
|
|
||||||
agentFailureDetail,
|
|
||||||
redactSecrets,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|||||||
+27
-2
@@ -4,12 +4,13 @@ const assert = require('node:assert/strict');
|
|||||||
const test = require('node:test');
|
const test = require('node:test');
|
||||||
|
|
||||||
const review = require('../src/lib/review');
|
const review = require('../src/lib/review');
|
||||||
|
const diagnostics = require('../src/lib/diagnostics');
|
||||||
|
|
||||||
test('agentFailureDetail 預設不輸出 stderr/stdout 片段', () => {
|
test('agentFailureDetail 預設不輸出 stderr/stdout 片段', () => {
|
||||||
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
|
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
|
||||||
delete process.env.ACTIONS_STEP_DEBUG;
|
delete process.env.ACTIONS_STEP_DEBUG;
|
||||||
try {
|
try {
|
||||||
const detail = review.__test.agentFailureDetail({
|
const detail = diagnostics.agentFailureDetail({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: Object.assign(new Error('boom'), { code: 1 }),
|
error: Object.assign(new Error('boom'), { code: 1 }),
|
||||||
stderr: 'token=super-secret-value',
|
stderr: 'token=super-secret-value',
|
||||||
@@ -27,7 +28,7 @@ test('agentFailureDetail 在 debug 模式輸出遮罩後片段', () => {
|
|||||||
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
|
const oldDebug = process.env.ACTIONS_STEP_DEBUG;
|
||||||
process.env.ACTIONS_STEP_DEBUG = 'true';
|
process.env.ACTIONS_STEP_DEBUG = 'true';
|
||||||
try {
|
try {
|
||||||
const detail = review.__test.agentFailureDetail({
|
const detail = diagnostics.agentFailureDetail({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: Object.assign(new Error('boom'), { code: 2 }),
|
error: Object.assign(new Error('boom'), { code: 2 }),
|
||||||
stderr: 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890',
|
stderr: 'Authorization: Bearer abcdefghijklmnopqrstuvwxyz1234567890',
|
||||||
@@ -72,3 +73,27 @@ test('postOthersToIssue 批次送出 issue 留言', async () => {
|
|||||||
assert.equal(calls[0].issueNumber, 7);
|
assert.equal(calls[0].issueNumber, 7);
|
||||||
assert.ok(maxActive > 1);
|
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'],
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user