Compare commits
6
Commits
d464998e4f
...
10d826d1b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10d826d1b1 | ||
|
|
dbeb56589c | ||
|
|
d3a09d892e | ||
|
|
e44a8ed0cb | ||
|
|
35887f1a68 | ||
|
|
5c8332928b |
@@ -42,10 +42,13 @@ jobs:
|
||||
uses: ./
|
||||
# 傳入 action inputs。
|
||||
with:
|
||||
# Gitea / GitHub token,用於 PR 留言與 findings 寫回。
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Gitea Actions 自動注入的 repo 範圍 token,用於 PR 留言與 findings 寫回。
|
||||
token: ${{ gitea.token }}
|
||||
# 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。
|
||||
create-issue: 'true'
|
||||
# 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI,
|
||||
# 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。
|
||||
push-token: ${{ secrets.TOKEN }}
|
||||
# Codex 工具環境測試 job。
|
||||
test-codex:
|
||||
# Job 在 workflow UI 顯示的名稱。
|
||||
@@ -70,10 +73,13 @@ jobs:
|
||||
uses: ./
|
||||
# 傳入 action inputs。
|
||||
with:
|
||||
# Gitea / GitHub token,用於 PR 留言與 findings 寫回。
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Gitea Actions 自動注入的 repo 範圍 token,用於 PR 留言與 findings 寫回。
|
||||
token: ${{ gitea.token }}
|
||||
# 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。
|
||||
create-issue: 'true'
|
||||
# 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI,
|
||||
# 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。
|
||||
push-token: ${{ secrets.TOKEN }}
|
||||
# Claude 工具環境測試 job。
|
||||
test-claude:
|
||||
# Job 在 workflow UI 顯示的名稱。
|
||||
@@ -98,7 +104,10 @@ jobs:
|
||||
uses: ./
|
||||
# 傳入 action inputs。
|
||||
with:
|
||||
# Gitea / GitHub token,用於 PR 留言與 findings 寫回。
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Gitea Actions 自動注入的 repo 範圍 token,用於 PR 留言與 findings 寫回。
|
||||
token: ${{ gitea.token }}
|
||||
# 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。
|
||||
create-issue: 'true'
|
||||
# 推送 findings/exclusions commit 用的 PAT:以 PAT 身分推送才會讓結果 commit 再觸發 CI,
|
||||
# 由步驟 1 快速回報把結果蓋到新 head,避免自動 token 推送不觸發而卡合併。
|
||||
push-token: ${{ secrets.TOKEN }}
|
||||
|
||||
+11
@@ -41,6 +41,17 @@ inputs:
|
||||
required: false
|
||||
# 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用)。
|
||||
default: 'false'
|
||||
# 推送用 PAT:推送 findings/exclusions commit 時改以此 token 進行。
|
||||
push-token:
|
||||
# 參數用途說明:以自動 token(gitea.token / GITHUB_TOKEN)推送的 commit 不會再觸發 CI,
|
||||
# 導致新 head 缺檢查而卡合併;改用 PAT 推送會讓 PR 的 synchronize 事件再觸發 CI,
|
||||
# 由主程式步驟 1 快速回報([success]/[failure])廉價地把結果蓋到新 head。
|
||||
# 呼叫端以 secrets 傳入(例如 secrets.TOKEN)。留空=退回以 token 走 origin 推送(不會再觸發)。
|
||||
description: '推送 findings/exclusions commit 用的 PAT(讓 CI 再觸發;呼叫端以 secrets 傳入;留空=退回 token 推送、不再觸發)'
|
||||
# 選填:未指定時退回以 token 推送。
|
||||
required: false
|
||||
# 預設為空字串,代表不使用專用推送 PAT。
|
||||
default: ''
|
||||
# 執行方式區塊:宣告本 action 為 node action 及其進入點。
|
||||
runs:
|
||||
# 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite)。
|
||||
|
||||
+75
-42
@@ -90,6 +90,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) {
|
||||
* 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json;
|
||||
* 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 4 判定無可審查變更且非建問題模式時,
|
||||
* 也會以 result: 'success' 提交空 findings。
|
||||
* 推送以 `ctx.pushToken`(PAT)優先,使結果 commit 再觸發 CI、由步驟 1 快速回報;
|
||||
* 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。
|
||||
*/
|
||||
function commitFindings({ cwd, ctx, files, result }) {
|
||||
@@ -100,6 +101,7 @@ function commitFindings({ cwd, ctx, files, result }) {
|
||||
message: `${BOT_COMMIT_PREFIX}[${result}]`,
|
||||
files,
|
||||
token: ctx.token,
|
||||
pushToken: ctx.pushToken,
|
||||
serverUrl: ctx.serverUrl,
|
||||
repository: ctx.repository,
|
||||
});
|
||||
@@ -119,7 +121,9 @@ function commitFindings({ cwd, ctx, files, result }) {
|
||||
*
|
||||
* 流程概要(步驟 2~10 描述一般模式;建問題模式差異見末段):
|
||||
* 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審;
|
||||
* 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步);
|
||||
* 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步)——
|
||||
* 此步延後到「本回合審查已成功產生結果、即將發布問題留言前」才執行,避免工具偵測/diff/
|
||||
* 攻防裁決任一失敗時舊結果先被清掉卻沒有新結果(一般模式);
|
||||
* 3. 偵測 AI 工具(antigravity/codex/claude)並留言;
|
||||
* 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings,
|
||||
* 一般模式 commit success、建問題模式略過 commit,回傳 0);
|
||||
@@ -129,9 +133,11 @@ function commitFindings({ cwd, ctx, files, result }) {
|
||||
* 9. 嚴重問題逐條掛在程式碼行上留言;
|
||||
* 10. 警告+建議彙整為單一表格留言;
|
||||
* 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言
|
||||
* 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後才建立 issue 並一次寫入,
|
||||
* 嚴重問題與警告+建議亦發到該 issue);無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過);
|
||||
* 收束時依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結形成雙向關聯;
|
||||
* 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後先挑好標籤、連同標籤一次建立 issue
|
||||
* 並寫入暫存留言,嚴重問題與警告+建議再逐條發到該 issue,讓每條問題都能被個別回覆);
|
||||
* 無保留問題或無可審查變更則不建 issue、PR 也完全不留言(靜默通過);
|
||||
* 收束時在 PR 回貼 issue 連結形成雙向關聯,
|
||||
* 並讓 PR 相依於該 issue(addIssueDependency,issue 完成/關閉前 PR 無法合併;需 repo 啟用問題相依功能);
|
||||
* 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、
|
||||
* 建問題模式只 commit exclusions.json、無檔案可 commit 時略過;
|
||||
* commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。
|
||||
@@ -199,17 +205,19 @@ async function main() {
|
||||
return created;
|
||||
};
|
||||
/**
|
||||
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,先不掛標籤),
|
||||
* 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,連同挑好的標籤一次建立),
|
||||
* 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。
|
||||
* 僅於「確定有保留問題」時呼叫一次。
|
||||
* 僅於「確定有保留問題」時呼叫一次。標籤於建立時一次帶入,省去「先建空標籤 issue 再補掛」的多餘 API 往返。
|
||||
*
|
||||
* @param {number[]} [labelIds] - 建立 issue 時要一併掛上的標籤 id 陣列(由 `review.selectLabels` 事先挑選);
|
||||
* 空陣列或省略時不掛任何標籤(`gitea.createIssue` 對空陣列不帶 labels 欄位)。
|
||||
* @returns {Promise<void>} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。
|
||||
*/
|
||||
const ensureIssueCreated = async () => {
|
||||
const ensureIssueCreated = 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 }),
|
||||
labels: [],
|
||||
labels: labelIds,
|
||||
});
|
||||
log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`);
|
||||
for (const body of issueBuffer) {
|
||||
@@ -218,12 +226,12 @@ async function main() {
|
||||
issueBuffer.length = 0;
|
||||
};
|
||||
|
||||
// ── 步驟 2:將 PR 既有留言標記為解決(本回合留言除外)───────────────────
|
||||
// 僅一般模式執行;建問題模式不觸碰 PR 既有留言(審查內容改發到 issue)。
|
||||
// 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時(此時尚無本回合留言)。
|
||||
if (!ctx.createIssue) {
|
||||
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
||||
}
|
||||
// ── 步驟 2:延後執行 ───────────────────────────────────────────────────
|
||||
// 「將 PR 既有留言標記為解決」原本在此執行,但若工具偵測/diff/攻防裁決任一失敗,
|
||||
// 舊結果會先被清掉卻沒有新結果。故延後到「本回合審查已成功產生結果、發布問題留言前」
|
||||
// 才呼叫 review.resolveOldComments(見下方步驟 4 空變更路徑與步驟 9 前);
|
||||
// 屆時本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時。
|
||||
// 建問題模式全程不觸碰 PR 既有留言(審查內容改發到 issue)。
|
||||
|
||||
// ── 步驟 3:偵測 AI agent 工具並留言 ──────────────────────────────────
|
||||
const tool = agents.detectTool();
|
||||
@@ -259,6 +267,8 @@ async function main() {
|
||||
log('步驟4', 'INF', '建問題模式且無可審查變更:靜默通過(不建 issue、PR 不留言)。');
|
||||
} else {
|
||||
await postComment(templates.nothingToReviewComment(ignoredCount));
|
||||
// 已成功產生本回合結果留言(無可審查變更),此時才把舊留言標為過時(本回合留言已排除)。
|
||||
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
||||
}
|
||||
const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] });
|
||||
if (ctx.createIssue) {
|
||||
@@ -301,36 +311,15 @@ async function main() {
|
||||
log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`);
|
||||
|
||||
// ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入;
|
||||
// 無保留問題則不建 issue,改在 PR 留一則審查通過提示。 ──────────────────
|
||||
// 無保留問題則不建 issue、PR 也完全不留言(靜默通過,暫存的情境留言捨棄)。 ──────
|
||||
if (ctx.createIssue) {
|
||||
if (kept.length > 0) {
|
||||
await ensureIssueCreated();
|
||||
} else {
|
||||
// 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。
|
||||
log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式發到 issue)─
|
||||
if (severe.length > 0) {
|
||||
if (ctx.createIssue) {
|
||||
await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe });
|
||||
} else {
|
||||
await review.postSevereComments({ ctx, gitea, severe, cwd });
|
||||
}
|
||||
}
|
||||
|
||||
// ── 步驟 10:警告+建議彙整為單一表格留言(去向由 postComment 依模式決定)──
|
||||
if (others.length > 0) {
|
||||
await postComment(templates.othersComment(others));
|
||||
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
|
||||
}
|
||||
|
||||
// ── 建問題模式收束:依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結(雙向關聯)─
|
||||
if (ctx.createIssue && issue) {
|
||||
// 先依保留問題挑好標籤,於建立 issue 時一次帶入(省去「先建空標籤 issue 再補掛」的多餘 API 往返);
|
||||
// 標籤挑選失敗一律降級為不掛標籤,不阻斷建 issue 流程。
|
||||
let labelIds = [];
|
||||
try {
|
||||
const labels = await gitea.listLabels(ctx);
|
||||
const labelIds = await review.selectLabels({
|
||||
labelIds = await review.selectLabels({
|
||||
tool,
|
||||
model: ctx.model,
|
||||
cwd,
|
||||
@@ -339,10 +328,47 @@ async function main() {
|
||||
prBody: ctx.prBody,
|
||||
findings: kept,
|
||||
});
|
||||
await gitea.addLabelsToIssue(ctx, issue.number, labelIds);
|
||||
} catch (err) {
|
||||
log('建問題', 'WRN', `補掛標籤失敗(${err.message}),issue 不掛標籤。`);
|
||||
log('建問題', 'WRN', `標籤挑選失敗(${err.message}),issue 不掛標籤。`);
|
||||
}
|
||||
await ensureIssueCreated(labelIds);
|
||||
} else {
|
||||
// 無保留問題 → 不建 issue、PR 也不留言(靜默通過,暫存的情境留言捨棄)。
|
||||
log('建問題', 'INF', '沒有保留的問題:靜默通過(不建 issue、PR 不留言)。');
|
||||
}
|
||||
}
|
||||
|
||||
// ── 步驟 2(延後執行,一般模式):審查已成功產生結果,發布問題留言前才把舊留言標為過時 ─
|
||||
// 延後到此可避免工具偵測/diff/攻防裁決任一失敗時舊結果先被清掉卻無新結果;
|
||||
// 本回合的工具/diff/角色留言已登錄於 currentRunCommentIds,不會被誤標為過時;
|
||||
// 嚴重/其他問題留言於本步驟之後才發布,同樣不受影響。
|
||||
if (!ctx.createIssue) {
|
||||
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
||||
}
|
||||
|
||||
// ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式逐條發到 issue)─
|
||||
if (severe.length > 0) {
|
||||
if (ctx.createIssue) {
|
||||
await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe });
|
||||
} else {
|
||||
await review.postSevereComments({ ctx, gitea, severe, cwd });
|
||||
}
|
||||
}
|
||||
|
||||
// ── 步驟 10:警告+建議——一般模式彙整為單一表格留言到 PR;
|
||||
// 建問題模式逐條發到 issue,讓每條問題都能被個別回覆。 ──
|
||||
if (others.length > 0) {
|
||||
if (ctx.createIssue) {
|
||||
await review.postOthersToIssue({ ctx, gitea, issueNumber: issue.number, others });
|
||||
} else {
|
||||
await postComment(templates.othersComment(others));
|
||||
log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`);
|
||||
}
|
||||
}
|
||||
|
||||
// ── 建問題模式收束:在 PR 回貼 issue 連結(雙向關聯),並讓 PR 相依於該 issue ─
|
||||
// 標籤已於建立 issue 時一次帶入(見上方 selectLabels → ensureIssueCreated),此處不再補掛。
|
||||
if (ctx.createIssue && issue) {
|
||||
await gitea.createIssueComment(
|
||||
ctx,
|
||||
templates.issueLinkComment({
|
||||
@@ -352,6 +378,13 @@ async function main() {
|
||||
otherCount: others.length,
|
||||
}),
|
||||
);
|
||||
// 讓 PR 相依於此追蹤 issue:issue 完成/關閉前 PR 無法合併(需 repo 啟用「問題相依」功能)。
|
||||
try {
|
||||
await gitea.addIssueDependency(ctx, ctx.prNumber, issue.number);
|
||||
log('建問題', 'INF', `已將 PR #${ctx.prNumber} 設為相依於 issue #${issue.number}:issue 關閉前無法合併。`);
|
||||
} catch (err) {
|
||||
log('建問題', 'WRN', `設定 PR 相依失敗(可能未啟用「問題相依」功能):${err.message}。`);
|
||||
}
|
||||
log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`);
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ const path = require('path');
|
||||
* token: string,
|
||||
* model: string,
|
||||
* createIssue: boolean,
|
||||
* pushToken: string,
|
||||
* event: Object,
|
||||
* pr: (Object|null),
|
||||
* prNumber: (number|null),
|
||||
@@ -41,6 +42,8 @@ const path = require('path');
|
||||
* - model:action input `model`(INPUT_MODEL,已 trim),指定 AI 模型,缺值時為空字串。
|
||||
* - createIssue:action input `create-issue`(INPUT_CREATE-ISSUE),是否將問題建到
|
||||
* 存取庫的問題追蹤(建問題模式);trim + 小寫後與字串 'true' 嚴格比對,預設 false。
|
||||
* - pushToken:action input `push-token`(INPUT_PUSH-TOKEN),推送 findings/exclusions commit 用的 PAT;
|
||||
* 提供時以 PAT 身分推送使 CI 再觸發(配合步驟 1 快速回報);留空=退回以 token 走 origin 推送(不會再觸發)。
|
||||
* - event:事件 payload 解析後的完整物件;讀取失敗時為空物件。
|
||||
* - pr:payload 內的 pull_request 物件;非 PR 事件時為 null。
|
||||
* - prNumber:PR 編號,優先取 payload,退而從 GITHUB_REF(refs/pull/N/...)解析;皆無時為 null。
|
||||
@@ -92,6 +95,8 @@ function loadContext() {
|
||||
model: (process.env.INPUT_MODEL || '').trim(),
|
||||
// 是否將問題建到存取庫的問題追蹤(input: create-issue,字串 'true' 才啟用,預設否)。
|
||||
createIssue: (process.env['INPUT_CREATE-ISSUE'] || '').trim().toLowerCase() === 'true',
|
||||
// 推送 findings/exclusions commit 用的 PAT(input: push-token);提供時以 PAT 推送讓 CI 再觸發,留空=退回 token。
|
||||
pushToken: (process.env['INPUT_PUSH-TOKEN'] || '').trim(),
|
||||
event,
|
||||
pr,
|
||||
prNumber,
|
||||
|
||||
+35
-7
@@ -134,9 +134,9 @@ function createIssueComment(ctx, body) {
|
||||
* @returns {Promise<object[]>} 標籤物件陣列(每筆含 `id`、`name`、`color` 等欄位,
|
||||
* 依 Gitea API 回應而定);存取庫無標籤時為空陣列。
|
||||
* @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
||||
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 收束時
|
||||
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 於確定有保留問題後
|
||||
* 先以本函式取得可用標籤,再交給 `review.selectLabels` 讓 AI 挑出適合的標籤子集合,
|
||||
* 最後以 `addLabelsToIssue` 補掛到追蹤 issue 上。
|
||||
* 最後於建立追蹤 issue 時(`createIssue`)一次帶入這些標籤。
|
||||
*/
|
||||
function listLabels(ctx) {
|
||||
return listAll(ctx, `/repos/${ctx.owner}/${ctx.repo}/labels`);
|
||||
@@ -157,8 +157,8 @@ function listLabels(ctx) {
|
||||
* `html_url` 等欄位,依 Gitea API 回應而定)。
|
||||
* @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
||||
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 的 `ensureIssueCreated`
|
||||
* 以 PR 標題/描述為 issue 標題與本文(先不掛標籤)呼叫本函式建立追蹤問題的 issue,
|
||||
* 之後再把審查內容留言到該 issue、並以 `addLabelsToIssue` 補掛標籤。
|
||||
* 以 PR 標題/描述為 issue 標題與本文,並帶入 `review.selectLabels` 事先挑好的標籤 id
|
||||
* 呼叫本函式一次建立追蹤問題的 issue(連同標籤),之後再把審查內容逐條留言到該 issue。
|
||||
*/
|
||||
function createIssue(ctx, { title, body, labels }) {
|
||||
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues`, {
|
||||
@@ -180,15 +180,42 @@ function createIssue(ctx, { title, body, labels }) {
|
||||
* @returns {Promise<object[]|null>} 追加後該 issue 的標籤陣列(依 Gitea API 回應而定);
|
||||
* `labels` 為空時回傳 `null`(未發出請求)。
|
||||
* @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
||||
* @remarks 使用情境:建問題模式(input: create-issue)下先以空標籤建立 issue、
|
||||
* 待防守方裁決得到保留問題後,再以 `selectLabels` 挑出的標籤 id 呼叫本函式補掛,
|
||||
* 讓標籤挑選能參考最終的問題清單。
|
||||
* @remarks 使用情境:為既有 issue 動態追加標籤的通用工具(不影響既有標籤)。
|
||||
* 註:建問題模式主流程已改為「建立 issue 時一次帶入標籤」(見 `createIssue`),
|
||||
* 不再於事後補掛;本函式保留為 Gitea 客戶端的通用能力,供需要事後追加標籤的情境使用。
|
||||
*/
|
||||
function addLabelsToIssue(ctx, issueNumber, labels) {
|
||||
if (!labels || labels.length === 0) return Promise.resolve(null);
|
||||
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/labels`, { labels });
|
||||
}
|
||||
|
||||
/**
|
||||
* 建立「問題相依」關係:讓 URL 上的 issue/PR 相依於(被阻擋於)表單指定的 issue。
|
||||
* 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/dependencies`
|
||||
* (body 為 IssueMeta:`{index, owner, repo}`)。
|
||||
*
|
||||
* 語義:URL 的 issue(`issueNumber`)相依於 body 的 issue(`dependency`)——
|
||||
* 在 `dependency` 關閉前,`issueNumber` 無法合併/關閉。本 endpoint 需 repo 啟用
|
||||
* 「問題相依(issue dependencies)」功能,屬版本/設定相依;未啟用或不支援時 API 會回非 2xx。
|
||||
*
|
||||
* @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、
|
||||
* `owner`(repo 擁有者)、`repo`(repo 名稱)。
|
||||
* @param {number|string} issueNumber - 要被阻擋的 issue/PR 編號(相依方)。
|
||||
* @param {number} dependency - 作為阻擋來源的 issue 編號(同一 repo)。
|
||||
* @returns {Promise<object>} 建立成功的相依關係物件(依 Gitea API 回應而定)。
|
||||
* @throws {Error} 請求失敗(非 2xx,例如未啟用問題相依功能)由底層 `api` 丟出,錯誤附 `status`、`data`。
|
||||
* @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立追蹤 issue 後,
|
||||
* 以本函式把「PR(`ctx.prNumber`)相依於追蹤 issue」,讓 issue 完成/關閉前 PR 無法合併;
|
||||
* 呼叫端以 try/catch 降級(功能未啟用時記 WRN、不阻斷流程)。
|
||||
*/
|
||||
function addIssueDependency(ctx, issueNumber, dependency) {
|
||||
return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/dependencies`, {
|
||||
index: dependency,
|
||||
owner: ctx.owner,
|
||||
repo: ctx.repo,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出 PR 上的全部一般留言(自動分頁撈取,每頁 50 筆直到取完)。
|
||||
* 對應 endpoint:`GET /repos/{owner}/{repo}/issues/{prNumber}/comments`。
|
||||
@@ -324,6 +351,7 @@ module.exports = {
|
||||
listLabels,
|
||||
createIssue,
|
||||
addLabelsToIssue,
|
||||
addIssueDependency,
|
||||
listIssueComments,
|
||||
editIssueComment,
|
||||
createReview,
|
||||
|
||||
+67
-19
@@ -84,13 +84,17 @@ function latestCommitSubject(cwd) {
|
||||
* 解析 PR base 分支與目前 HEAD 的 merge-base commit SHA。
|
||||
*
|
||||
* 先以 refspec 明確更新 `origin/<baseRef>`,再以 `git merge-base origin/<baseRef> HEAD`
|
||||
* 取得共同祖先。若 checkout 是淺層歷史而導致 merge-base 失敗,會補抓完整或更深的
|
||||
* base/head 歷史後重試,避免 PR workflow 因 checkout 預設深度不足而中斷。
|
||||
* 取得共同祖先。若 checkout 是淺層歷史而導致 merge-base 失敗,會依序補抓更完整的
|
||||
* base/head 歷史;**每個補抓策略成功後立即重試 merge-base,一成功即回傳**,
|
||||
* 避免在已補到足夠歷史後仍多做無謂的 fetch 往返(例如 `--unshallow` 成功就不再 deepen)。
|
||||
* 各策略採資料驅動依序執行;全部用盡仍失敗時,丟出彙整了「哪個策略成功/失敗」診斷的錯誤,
|
||||
* 方便維護者判斷是哪一步補抓不足(診斷僅含策略名與成敗,不含 git 原始輸出以免洩漏遠端資訊)。
|
||||
*
|
||||
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
||||
* @param {string} baseRef - PR 目標(base)分支名稱,例如 'master' 或 'develop';不含 'origin/' 前綴。
|
||||
* @returns {string} merge-base 的 commit SHA(40 碼十六進位字串)。
|
||||
* @throws {Error} 補抓歷史後仍無法取得共同祖先時,丟出含 baseRef 的明確錯誤。
|
||||
* @throws {Error} 補抓歷史後仍無法取得共同祖先時,丟出含 baseRef 與各策略診斷的明確錯誤
|
||||
* (`error.cause` 保留首次 merge-base 失敗的原始錯誤)。
|
||||
* @remarks
|
||||
* 使用情境:AI code review 以此結果作為 diff 比較基準——
|
||||
* 先 `resolveMergeBase(cwd, pr.base.ref)` 取得基準 SHA,
|
||||
@@ -99,25 +103,57 @@ function latestCommitSubject(cwd) {
|
||||
*/
|
||||
function resolveMergeBase(cwd, baseRef) {
|
||||
const remoteBase = `origin/${baseRef}`;
|
||||
tryGit(cwd, 'fetch', '--no-tags', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`);
|
||||
try {
|
||||
return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD');
|
||||
} catch (firstError) {
|
||||
const isShallow = gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true';
|
||||
if (isShallow) {
|
||||
tryGit(cwd, 'fetch', '--no-tags', '--unshallow', 'origin');
|
||||
}
|
||||
tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`);
|
||||
tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD');
|
||||
const diagnostics = [];
|
||||
// 執行一個 fetch 策略並記錄成敗(只記策略名與成敗,不含 git 原始輸出,避免洩漏遠端資訊)。
|
||||
const runFetch = (label, ...args) => {
|
||||
const ok = tryGit(cwd, ...args);
|
||||
diagnostics.push(`${label}:${ok ? '成功' : '失敗'}`);
|
||||
return ok;
|
||||
};
|
||||
// 每個補抓策略後重試 merge-base:成功回傳 SHA,失敗記診斷並回傳 null。
|
||||
const tryMergeBase = (label) => {
|
||||
try {
|
||||
return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD');
|
||||
} catch {
|
||||
const error = new Error(`無法解析 origin/${baseRef} 與 HEAD 的 merge-base;請確認 checkout 有足夠歷史,或設定 checkout fetch-depth: 0。`);
|
||||
diagnostics.push(`merge-base(${label}):失敗`);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 先明確更新 origin/<baseRef>,再嘗試 merge-base。
|
||||
runFetch(`fetch base(${baseRef})`, 'fetch', '--no-tags', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`);
|
||||
let firstError;
|
||||
try {
|
||||
return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD');
|
||||
} catch (err) {
|
||||
firstError = err;
|
||||
diagnostics.push('merge-base(首次):失敗');
|
||||
}
|
||||
|
||||
// 資料驅動的補抓策略:淺層才 unshallow;其後依序 deepen base 與 HEAD。
|
||||
// 每個策略成功後立即重試 merge-base,成功即回傳,避免多餘往返。
|
||||
const strategies = [];
|
||||
if (gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true') {
|
||||
strategies.push(['unshallow', 'fetch', '--no-tags', '--unshallow', 'origin']);
|
||||
}
|
||||
strategies.push([
|
||||
'deepen base',
|
||||
'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`,
|
||||
]);
|
||||
strategies.push(['deepen HEAD', 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD']);
|
||||
|
||||
for (const [label, ...args] of strategies) {
|
||||
if (!runFetch(label, ...args)) continue; // fetch 失敗就換下一個策略。
|
||||
const sha = tryMergeBase(`${label} 後`);
|
||||
if (sha) return sha;
|
||||
}
|
||||
|
||||
const error = new Error(
|
||||
`無法解析 origin/${baseRef} 與 HEAD 的 merge-base;請確認 checkout 有足夠歷史,或設定 checkout fetch-depth: 0。診斷:${diagnostics.join(';')}`,
|
||||
);
|
||||
error.cause = firstError;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 列出 base 與 HEAD 之間有變更的檔案清單。
|
||||
@@ -187,7 +223,9 @@ function fileLastUpdatedIso(cwd, file) {
|
||||
* 若目前 HEAD 不在 PR head commit(例如 checkout 停在 merge commit),
|
||||
* 會先 `git checkout --detach <headSha>` 站上 head,避免把 merge 內容推回來源分支。
|
||||
* commit 以 `-c` 臨時覆寫 user.name / user.email,不改動 repo 的 git 設定。
|
||||
* push 先走 origin;失敗(遠端未帶認證)時改用帶 token 的 URL 重試。
|
||||
* push 策略:提供 `pushToken`(PAT)時直接以該 token 的 URL 推送(略過 origin)——因為 origin
|
||||
* 帶的是不會再觸發 CI 的自動 token,改以 PAT 身分推送才會讓 PR 的 synchronize 事件再觸發 CI;
|
||||
* 未提供 `pushToken` 時先走 origin,失敗(遠端未帶認證)再改用帶 `token` 的 URL 重試。
|
||||
*
|
||||
* @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。
|
||||
* @param {object} options - 提交與推送設定。
|
||||
@@ -195,7 +233,8 @@ function fileLastUpdatedIso(cwd, file) {
|
||||
* @param {string} [options.headSha] - PR head 的 commit SHA;有提供且與目前 HEAD 不同時會先 detach 到此 commit。可省略(falsy 時不 detach,直接於目前 HEAD 上 commit)。
|
||||
* @param {string} options.message - commit 訊息。
|
||||
* @param {string[]} options.files - 要加入 commit 的檔案路徑清單(相對 repo 根目錄);全數無實際變更時不 commit、回傳 false。
|
||||
* @param {string} options.token - 具該 repo push 權限的 Gitea access token;僅在 origin push 失敗時用於組出帶認證的重試 URL。
|
||||
* @param {string} options.token - 具該 repo push 權限的 Gitea access token;未提供 pushToken 時,於 origin push 失敗才用於組出帶認證的重試 URL。
|
||||
* @param {string} [options.pushToken] - 專用推送 token(PAT);提供時直接以此 token 的 URL 推送(略過 origin),使 push 以 PAT 身分進行以再觸發 CI;未提供時走 origin、失敗再退回 `token`。
|
||||
* @param {string} options.serverUrl - Gitea 伺服器根網址(例如 https://gitea.example.com),須為合法 URL。
|
||||
* @param {string} options.repository - repo 完整名稱(owner/repo 格式),與 serverUrl 組成 clone URL。
|
||||
* @returns {boolean} true=有變更且已 commit 並 push 到來源分支;false=暫存區與 HEAD 無差異,略過 commit/push。
|
||||
@@ -210,7 +249,7 @@ function fileLastUpdatedIso(cwd, file) {
|
||||
* 絕對不得將此 URL 輸出到日誌、錯誤訊息或任何 action 輸出,以免洩漏 token;
|
||||
* 若需記錄重試行為,只能記載「改用帶認證 URL 重試」而不得包含 URL 本身。
|
||||
*/
|
||||
function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, serverUrl, repository }) {
|
||||
function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, pushToken, serverUrl, repository }) {
|
||||
const current = gitTrim(cwd, 'rev-parse', 'HEAD');
|
||||
if (headSha && current !== headSha) {
|
||||
git(cwd, 'checkout', '--detach', headSha);
|
||||
@@ -228,6 +267,14 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
|
||||
'-c', 'user.email=ai-review-bot@noreply.gitea',
|
||||
'commit', '-m', message,
|
||||
);
|
||||
if (pushToken) {
|
||||
// 有專用 PAT → 直接以帶 token 的 URL 推送(略過 origin,因 origin 帶的是不會再觸發 CI 的自動 token);
|
||||
// 以 PAT 身分推送才會讓 PR 的 synchronize 事件再觸發 CI。不得把含 token 的 URL 輸出到日誌。
|
||||
const url = new URL(`${serverUrl}/${repository}.git`);
|
||||
url.username = 'ai-review-bot';
|
||||
url.password = pushToken;
|
||||
git(cwd, 'push', url.toString(), `HEAD:refs/heads/${headRef}`);
|
||||
} else {
|
||||
try {
|
||||
git(cwd, 'push', 'origin', `HEAD:refs/heads/${headRef}`);
|
||||
} catch {
|
||||
@@ -238,6 +285,7 @@ function commitAndPushFindings(cwd, { headRef, headSha, message, files, token, s
|
||||
url.password = token;
|
||||
git(cwd, 'push', url.toString(), `HEAD:refs/heads/${headRef}`);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+77
-15
@@ -14,18 +14,44 @@ const PER_FILE_DIFF_LIMIT = 16_000;
|
||||
const TOTAL_DIFF_LIMIT = 160_000;
|
||||
|
||||
/**
|
||||
* 從 `runAgent` 的失敗結果組出可診斷的一行摘要:退出碼/訊號/stderr/stdout 片段。
|
||||
* 遮罩單行診斷文字中的機密與控制字元,避免寫進 CI log 時外洩。
|
||||
*
|
||||
* AI CLI 失敗時常見 stderr 為空、真正原因印在 stdout(例如 CLI 用法錯誤、未認證或額度提示),
|
||||
* 若只記 `error.message` 會看不出原因。本函式把 exit code、stderr、stdout 各截前 500 字併成一行,
|
||||
* 供各失敗點的 WRN log 使用。純函式、不拋例外。
|
||||
* 處理順序:換行與控制字元一律壓成單一空白(避免注入假日誌行)→ 遮蔽
|
||||
* `Authorization` 標頭、`token=`/`token:` 型憑證、URL 內嵌帳密、以及常見長金鑰/
|
||||
* 長 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
|
||||
* `runAgent` 的回傳物件。
|
||||
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳 error.message 或「未知錯誤」。
|
||||
* @returns {string} 單行診斷摘要(各段以「|」分隔);無任何資訊時回傳固定字串。
|
||||
* @remarks
|
||||
* 使用情境:{@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) {
|
||||
@@ -37,11 +63,18 @@ function agentFailureDetail(res) {
|
||||
else if (err.code) parts.push(`code ${err.code}`);
|
||||
else if (err.signal) parts.push(`signal ${err.signal}`);
|
||||
}
|
||||
const stderr = String((res && res.stderr) || '').trim();
|
||||
// 預設不輸出 AI CLI 原始 stderr/stdout(可能含 token、PII 或原始碼祕密);
|
||||
// 僅在明確開啟 Actions step debug 時,附上「已遮罩+去控制字元」的片段作為診斷。
|
||||
const verbose = String(process.env.ACTIONS_STEP_DEBUG || '').trim().toLowerCase() === 'true';
|
||||
if (verbose) {
|
||||
const stderr = redactSecrets((res && res.stderr) || '');
|
||||
if (stderr) parts.push(`stderr:${stderr.slice(0, 500)}`);
|
||||
const stdout = String((res && res.output) || '').trim();
|
||||
const stdout = redactSecrets((res && res.output) || '');
|
||||
if (stdout) parts.push(`stdout:${stdout.slice(0, 500)}`);
|
||||
if (parts.length === 0) parts.push((err && err.message) || '未知錯誤');
|
||||
}
|
||||
if (parts.length === 0) {
|
||||
parts.push((err && err.message && redactSecrets(err.message)) || 'AI CLI 執行失敗(詳細輸出已隱藏)');
|
||||
}
|
||||
return parts.join('|');
|
||||
}
|
||||
|
||||
@@ -629,8 +662,8 @@ function sortFindings(findings) {
|
||||
* @returns {Promise<number[]>} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。
|
||||
* @remarks
|
||||
* 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)
|
||||
* 在建立追蹤 issue 後先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式依保留問題
|
||||
* 挑出標籤 id 子集合,交給 `gitea.addLabelsToIssue` 補掛到 issue 上。
|
||||
* 於確定有保留問題後、建立追蹤 issue 前先呼叫 `gitea.listLabels` 取得可用標籤,
|
||||
* 再以本函式依保留問題挑出標籤 id 子集合,於 `gitea.createIssue` 建立 issue 時一次帶入。
|
||||
*/
|
||||
async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) {
|
||||
if (labels.length === 0) return [];
|
||||
@@ -704,6 +737,32 @@ async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) {
|
||||
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 行。
|
||||
*
|
||||
@@ -746,12 +805,14 @@ function readSnippet(cwd, finding) {
|
||||
* @param {Object} params - 解構參數。
|
||||
* @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 {Set<number>} params.currentRunCommentIds - 本回合發出的一般留言 id 集合;這些留言不標註過時。
|
||||
* @param {Set<number>} params.currentRunCommentIds - 本回合已發出的一般留言 id 集合;這些留言不標註過時。
|
||||
* @returns {Promise<void>} 無回傳值;結果反映在 PR 留言狀態與日誌。
|
||||
* @remarks
|
||||
* 使用情境:審查流程「步驟 2」在步驟 1 快速回報與前置檢查之後、偵測工具(步驟 3)
|
||||
* 與所有本回合留言之前呼叫;此時本回合尚未發出任何留言(currentRunCommentIds 為空),
|
||||
* 之後發出的留言自然不受影響,確保 PR 上只有最新回合的審查結果醒目可見。
|
||||
* 使用情境:一般模式下,`src/index.js` 於「確定本回合審查已成功產生結果後、發布嚴重/
|
||||
* 其他問題留言之前」呼叫(見 `main()`),刻意延後到工具偵測、diff 整理與攻防裁決都成功之後,
|
||||
* 避免任一前置步驟失敗時舊結果已被清掉、PR 卻沒有新結果。此時本回合的工具/diff/角色留言
|
||||
* 已發出並登錄於 `currentRunCommentIds`,本函式據此排除、不會把這些「新產生的留言」誤標為過時;
|
||||
* 之後才發布的嚴重/其他問題留言更不受影響,確保 PR 上只有最新回合的審查結果醒目可見。
|
||||
*/
|
||||
async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) {
|
||||
let botLogin = '';
|
||||
@@ -861,6 +922,7 @@ module.exports = {
|
||||
appendExclusions,
|
||||
selectLabels,
|
||||
postSevereToIssue,
|
||||
postOthersToIssue,
|
||||
resolveOldComments,
|
||||
postSevereComments,
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ function cell(text) {
|
||||
* @param {string} focus - 審查面向代碼(例如 `'logic'`、`'security'`);可為 undefined。
|
||||
* @returns {string} 顯示字串:命中時如 `'邏輯(logic)'`;未命中時原樣回傳 `focus`;falsy 時回傳 `'—'`。
|
||||
* @remarks
|
||||
* 使用情境:審查流程步驟 5/6 的角色登場留言——`rolesComment()`
|
||||
* 使用情境:審查流程步驟 5/7 的角色登場留言——`rolesComment()`
|
||||
* 產生「角色|面向|個性」表格時,以本函式把每位審查員
|
||||
* (攻擊方/防守方)的 focus 代碼轉成中英並列的面向欄位內容。
|
||||
* 本函式未匯出,僅供模組內部使用。
|
||||
@@ -150,7 +150,7 @@ function diffComment(rows, ignoredCount) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 產生審查流程步驟 5/6 共用的「角色登場」PR 留言內容。
|
||||
* 產生審查流程步驟 5/7 共用的「角色登場」PR 留言內容。
|
||||
*
|
||||
* 以三欄表格(角色/面向/個性)列出本回合登場的審查員;
|
||||
* 攻擊方(步驟 5)與防守方(步驟 7)共用本模板,僅標題不同。
|
||||
|
||||
Reference in New Issue
Block a user