fix(diagnostics): 處理 ai review findings #32
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# 用途:定義 AI Code Review Node action 的名稱、輸入參數與 Node.js 24 進入點,供 Gitea / GitHub workflow 以 uses 引用。
|
# 用途:定義 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):
|
# Gitea / GitHub node action 的 manifest(action.yml):
|
||||||
# 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs),
|
# 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# AI Code Review
|
# 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]`)供下次觸發快速回報。
|
AI 多角色 code review 的 Gitea **node action**(`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]`/`[failure]`)供下次觸發快速回報。
|
||||||
|
|
||||||
|
|||||||
+17
-18
@@ -4,7 +4,7 @@
|
|||||||
console.log('================================================');
|
console.log('================================================');
|
||||||
console.log('Action : AI Code Review');
|
console.log('Action : AI Code Review');
|
||||||
console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings');
|
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('================================================');
|
console.log('================================================');
|
||||||
|
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -126,15 +126,14 @@ function commitFindings({ cwd, ctx, files, result }) {
|
|||||||
* 建問題模式會把審查情境與每條 finding 發到追蹤 issue;沒有保留 finding 時不建立 issue、PR 也不留言。
|
* 建問題模式會把審查情境與每條 finding 發到追蹤 issue;沒有保留 finding 時不建立 issue、PR 也不留言。
|
||||||
* 嚴重 finding 會寫入 failure 結果 commit,警告與建議只建立追蹤資訊,不直接阻擋合併。
|
* 嚴重 finding 會寫入 failure 結果 commit,警告與建議只建立追蹤資訊,不直接阻擋合併。
|
||||||
*
|
*
|
||||||
* @returns {Promise<number>} process exit code:本輪「審查」一律回傳 0(不因嚴重問題直接讓檢查失敗——
|
* @returns {Promise<number>} process exit code:沒有嚴重問題且結果 commit 已成功推送時回傳 0;
|
||||||
* 失敗改由推出的 `[ai-review-bot][failure]` 結果 commit,於下一輪在步驟 1 讀 commit 訊息時回報);
|
* 若步驟 1 偵測到 `[ai-review-bot][failure]`、前置條件不足,或需要推送結果檔卻未成功推送,
|
||||||
* 回傳 1 僅發生於:步驟 1 偵測到 `[ai-review-bot][failure]` 結果 commit,或前置條件不足
|
* 則回傳 1,避免 token 權限不足或遠端競態讓 workflow 靜默通過。
|
||||||
* (缺 PR 編號/token、找不到 AI 工具)等無法進行審查的情況。
|
|
||||||
* @remarks
|
* @remarks
|
||||||
* 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))`;
|
* 使用情境:由本檔尾端的頂層呼叫端執行 —— `main().then((code) => process.exit(code))`;
|
||||||
* 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記,
|
* 非預期例外由頂層 `catch` 記 ERR log 後以 exit code 1 收場,且刻意不 commit 結果標記,
|
||||||
* 讓下一次 workflow 觸發時重新完整審查。警告+建議等級不影響結果標記,只有「嚴重」會使結果 commit
|
* 讓下一次 workflow 觸發時重新完整審查。警告+建議等級不影響結果標記,只有「嚴重」會使結果 commit
|
||||||
* 標記為 failure;而「失敗檢查(exit 1)」只由步驟 1 讀到該 failure 結果 commit 時產生,審查本輪不直接 exit 1。
|
* 標記為 failure;若結果檔需要 push 卻失敗,本輪直接 exit 1,避免缺權限時無聲放行。
|
||||||
* 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變上述結果標記判定。
|
* 建問題模式只改變問題明細的落地方式(issue 留言取代 findings 進版控),不改變上述結果標記判定。
|
||||||
*/
|
*/
|
||||||
async function main() {
|
async function main() {
|
||||||
@@ -193,7 +192,7 @@ async function main() {
|
|||||||
return created;
|
return created;
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
|
* 建問題模式:開啟追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
|
||||||
* 並把 `pendingIssueCommentBodies` 內暫存的情境留言依流程順序寫入 issue;
|
* 並把 `pendingIssueCommentBodies` 內暫存的情境留言依流程順序寫入 issue;
|
||||||
* 設定閉包變數 `trackingIssue` 供後續留言直接發到 issue。
|
* 設定閉包變數 `trackingIssue` 供後續留言直接發到 issue。
|
||||||
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
|
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
|
||||||
@@ -202,17 +201,18 @@ async function main() {
|
|||||||
* 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。
|
* 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。
|
||||||
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `trackingIssue` 與 issue 留言。
|
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `trackingIssue` 與 issue 留言。
|
||||||
*/
|
*/
|
||||||
const createIssueAndFlushBufferedComments = async (labelIds = []) => {
|
const openTrackingIssue = async (labelIds = []) => {
|
||||||
trackingIssue = await gitea.createIssue(ctx, {
|
trackingIssue = await gitea.createIssue(ctx, {
|
||||||
title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`,
|
title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`,
|
||||||
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
|
body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }),
|
||||||
labels: labelIds,
|
labels: labelIds,
|
||||||
});
|
});
|
||||||
log('建問題', 'INF', `已建立追蹤 issue #${trackingIssue.number},寫入 ${pendingIssueCommentBodies.length} 則情境留言。`);
|
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);
|
await gitea.createCommentOnIssue(ctx, trackingIssue.number, body);
|
||||||
|
pendingIssueCommentBodies.shift();
|
||||||
}
|
}
|
||||||
pendingIssueCommentBodies.length = 0;
|
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* 建問題模式降級:追蹤 issue 無法建立或寫入時,改把已暫存的情境留言發回 PR,後續沿用一般模式。
|
* 建問題模式降級:追蹤 issue 無法建立或寫入時,改把已暫存的情境留言發回 PR,後續沿用一般模式。
|
||||||
@@ -337,7 +337,7 @@ async function main() {
|
|||||||
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
|
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await createIssueAndFlushBufferedComments(labelIds);
|
await openTrackingIssue(labelIds);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log('建問題', 'WRN', `建立或寫入追蹤 issue 失敗(${err.message}),改用 PR 留言與 findings 檔流程。`);
|
log('建問題', 'WRN', `建立或寫入追蹤 issue 失敗(${err.message}),改用 PR 留言與 findings 檔流程。`);
|
||||||
await fallbackToPrComments();
|
await fallbackToPrComments();
|
||||||
@@ -377,7 +377,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─
|
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯);僅在有嚴重問題時才讓 PR 相依於該 issue ─
|
||||||
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → createIssueAndFlushBufferedComments),此處不再補掛。
|
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → openTrackingIssue),此處不再補掛。
|
||||||
if (issueModeActive && trackingIssue) {
|
if (issueModeActive && trackingIssue) {
|
||||||
await review.resolveOldIssueLinkComments({ ctx, gitea });
|
await review.resolveOldIssueLinkComments({ ctx, gitea });
|
||||||
await gitea.createIssueComment(
|
await gitea.createIssueComment(
|
||||||
@@ -420,14 +420,13 @@ async function main() {
|
|||||||
} else {
|
} else {
|
||||||
log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。');
|
log('收尾', 'INF', '建問題模式且 exclusions.json 無變更,略過 commit/push。');
|
||||||
}
|
}
|
||||||
// 本輪「審查」一律以成功收場、不直接讓檢查失敗;有嚴重問題時已推出 [failure] 結果 commit,
|
// 需要推送結果檔卻沒成功時直接失敗;這通常代表 token 權限不足、非快轉或分支保護阻擋。
|
||||||
// 由它再觸發的下一輪在步驟 1 讀 commit 訊息時才回報失敗(exit 1)。如此失敗檢查落在帶有結果
|
if (review.shouldFailMissingResultCommit({ filesToCommit, resultCommitted })) {
|
||||||
// 標記的最新 head 上,與合併判定一致。(result 僅用於上方 commit 訊息的結果標記。)
|
log('收尾', 'ERR', '審查結果檔需要 push 但未成功產生結果 commit;直接回報失敗避免缺權限時靜默通過。');
|
||||||
if (result === 'failure') {
|
|
||||||
if (!resultCommitted) {
|
|
||||||
log('收尾', 'ERR', '本輪有嚴重問題,但未成功產生 [failure] 結果 commit;直接回報失敗避免 fail-open。');
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
// 嚴重問題已推出 [failure] 結果 commit 時,由它再觸發的下一輪在步驟 1 讀 commit 訊息回報失敗。
|
||||||
|
if (result === 'failure') {
|
||||||
log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。');
|
log('收尾', 'INF', '本輪有嚴重問題:已標記結果 commit 為 [failure],失敗檢查由下一輪步驟 1 讀 commit 訊息回報。');
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -579,6 +579,18 @@ function resultFilesToCommit({ createIssue, severeCount, relativePath, exclusion
|
|||||||
return files;
|
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:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。
|
* 就地排序 findings:依 嚴重→警告→建議、再依檔案路徑、再依起始行遞增。
|
||||||
*
|
*
|
||||||
@@ -914,6 +926,7 @@ module.exports = {
|
|||||||
sortFindings,
|
sortFindings,
|
||||||
appendExclusions,
|
appendExclusions,
|
||||||
resultFilesToCommit,
|
resultFilesToCommit,
|
||||||
|
shouldFailMissingResultCommit,
|
||||||
selectLabels,
|
selectLabels,
|
||||||
postSevereToIssue,
|
postSevereToIssue,
|
||||||
postOthersToIssue,
|
postOthersToIssue,
|
||||||
|
|||||||
@@ -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', () => {
|
test('resolveMergeBase 會在 git fetch 前拒絕不安全 baseRef', () => {
|
||||||
assert.throws(
|
assert.throws(
|
||||||
() => gitrepo.resolveMergeBase(process.cwd(), '../../hooks/pre-push'),
|
() => gitrepo.resolveMergeBase(process.cwd(), '../../hooks/pre-push'),
|
||||||
|
|||||||
@@ -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 () => {
|
test('resolveOldIssueLinkComments 只標註舊追蹤 issue 連結', async () => {
|
||||||
const edited = [];
|
const edited = [];
|
||||||
const fakeGitea = {
|
const fakeGitea = {
|
||||||
|
|||||||
Reference in New Issue
Block a user