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