feat(ai-code-review): 建問題模式導向 issue,並整併流程調整、文件與 CI #6
+79
-17
@@ -14,18 +14,44 @@ const PER_FILE_DIFF_LIMIT = 16_000;
|
|||||||
const TOTAL_DIFF_LIMIT = 160_000;
|
const TOTAL_DIFF_LIMIT = 160_000;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號/stderr/stdout 片段。
|
* 遮罩單行診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。
|
||||||
*
|
*
|
||||||
* AI CLI 失敗時常見 stderr 為空、真正原因印在 stdout(例如 CLI 用法錯誤、未認證或額度提示),
|
* 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽
|
||||||
* 若只記 `error.message` 會看不出原因。本函式把 exit code、stderr、stdout 各截前 500 字併成一行,
|
* `Authorization` 標頭、`token=`/`token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/
|
||||||
* 供各失敗點的 WRN log 使用。純函式、不拋例外。
|
* 長 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
|
* @param {{error: (Error & {code?: number|string, signal?: string, killed?: boolean})|null, stderr?: string, output?: string}} res
|
||||||
* `runAgent` 的回傳物件。
|
* `runAgent` 的回傳物件。
|
||||||
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳 error.message 或「未知錯誤」。
|
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:{@link runAttackers}/{@link runDefenders}/{@link fillPurposes}/{@link selectLabels}
|
* 使用情境:{@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) {
|
function agentFailureDetail(res) {
|
||||||
@@ -37,11 +63,18 @@ function agentFailureDetail(res) {
|
|||||||
else if (err.code) parts.push(`code ${err.code}`);
|
else if (err.code) parts.push(`code ${err.code}`);
|
||||||
else if (err.signal) parts.push(`signal ${err.signal}`);
|
else if (err.signal) parts.push(`signal ${err.signal}`);
|
||||||
}
|
}
|
||||||
const stderr = String((res && res.stderr) || '').trim();
|
// 預設不輸出 AI CLI 原始 stderr/stdout(可能含 token、PII 或原始碼祕密);
|
||||||
if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`);
|
// 僅在明確開啟 Actions step debug 時,附上「已遮罩+去控制字元」的片段作為診斷。
|
||||||
const stdout = String((res && res.output) || '').trim();
|
const verbose = String(process.env.ACTIONS_STEP_DEBUG || '').trim().toLowerCase() === 'true';
|
||||||
if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`);
|
if (verbose) {
|
||||||
if (parts.length === 0) parts.push((err && err.message) || '未知錯誤');
|
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('|');
|
return parts.join('|');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -629,8 +662,8 @@ function sortFindings(findings) {
|
|||||||
* @returns {Promise<number[]>} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。
|
* @returns {Promise<number[]>} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)
|
* 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)
|
||||||
* 在建立追蹤 issue 後先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式依保留問題
|
* 於確定有保留問題後、建立追蹤 issue 前先呼叫 `gitea.listLabels` 取得可用標籤,
|
||||||
* 挑出標籤 id 子集合,交給 `gitea.addLabelsToIssue` 補掛到 issue 上。
|
* 再以本函式依保留問題挑出標籤 id 子集合,於 `gitea.createIssue` 建立 issue 時一次帶入。
|
||||||
*/
|
*/
|
||||||
async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) {
|
async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) {
|
||||||
if (labels.length === 0) return [];
|
if (labels.length === 0) return [];
|
||||||
@@ -704,6 +737,32 @@ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
|
|||||||
log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`);
|
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<Object>} params.others - severity 非「嚴重」(警告+建議)的 finding 列表(已排序;呼叫端保證非空)。
|
||||||
|
* @returns {Promise<void>} 無回傳值;結果反映在 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 行。
|
* 讀取 finding 對應的程式碼片段:新版檔案的 startLine..endLine,最多 40 行。
|
||||||
*
|
*
|
||||||
@@ -746,12 +805,14 @@ function readSnippet(cwd, finding) {
|
|||||||
* @param {Object} params - 解構參數。
|
* @param {Object} params - 解構參數。
|
||||||
* @param {Object} params.ctx - 執行環境 context(`loadContext()` 產出,含 repo/PR 編號/token 等 API 呼叫所需資訊)。
|
* @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 {Object} params.gitea - Gitea API 模組(`src/lib/gitea.js`),需提供 `whoAmI`/`listIssueComments`/`editIssueComment`/`listReviews`/`listReviewComments`/`tryResolveReviewComment`;以參數注入便於測試替換。
|
||||||
* @param {Set<number>} params.currentRunCommentIds - 本回合發出的一般留言 id 集合;這些留言不標註過時。
|
* @param {Set<number>} params.currentRunCommentIds - 本回合已發出的一般留言 id 集合;這些留言不標註過時。
|
||||||
* @returns {Promise<void>} 無回傳值;結果反映在 PR 留言狀態與日誌。
|
* @returns {Promise<void>} 無回傳值;結果反映在 PR 留言狀態與日誌。
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:審查流程「步驟 2」在步驟 1 快速回報與前置檢查之後、偵測工具(步驟 3)
|
* 使用情境:一般模式下,`src/index.js` 於「確定本回合審查已成功產生結果後、發布嚴重/
|
||||||
* 與所有本回合留言之前呼叫;此時本回合尚未發出任何留言(currentRunCommentIds 為空),
|
* 其他問題留言之前」呼叫(見 `main()`),刻意延後到工具偵測、diff 整理與攻防裁決都成功之後,
|
||||||
* 之後發出的留言自然不受影響,確保 PR 上只有最新回合的審查結果醒目可見。
|
* 避免任一前置步驟失敗時舊結果已被清掉、PR 卻沒有新結果。此時本回合的工具/diff/角色留言
|
||||||
|
* 已發出並登錄於 `currentRunCommentIds`,本函式據此排除、不會把這些「新產生的留言」誤標為過時;
|
||||||
|
* 之後才發布的嚴重/其他問題留言更不受影響,確保 PR 上只有最新回合的審查結果醒目可見。
|
||||||
*/
|
*/
|
||||||
async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
|
async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
|
||||||
let botLogin = '';
|
let botLogin = '';
|
||||||
@@ -861,6 +922,7 @@ module.exports = {
|
|||||||
appendExclusions,
|
appendExclusions,
|
||||||
selectLabels,
|
selectLabels,
|
||||||
postSevereToIssue,
|
postSevereToIssue,
|
||||||
|
postOthersToIssue,
|
||||||
resolveOldComments,
|
resolveOldComments,
|
||||||
postSevereComments,
|
postSevereComments,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user