feat: report ai review commit status
This commit is contained in:
@@ -35,7 +35,7 @@
|
|||||||
|
|
||||||
> **自動提交排除說明**:此 Action 會將自己的 commit message 標記為 `[ai-review-bot]`,而且 action 執行時會先透過 Gitea API 檢查這次觸發的 PR head commit(優先用 `pull_request.head.sha`)是否含有這個 marker,若有就直接成功結束,避免 bot commit 造成重複觸發。若外層 workflow 也能先檢查一次,效果最好。
|
> **自動提交排除說明**:此 Action 會將自己的 commit message 標記為 `[ai-review-bot]`,而且 action 執行時會先透過 Gitea API 檢查這次觸發的 PR head commit(優先用 `pull_request.head.sha`)是否含有這個 marker,若有就直接成功結束,避免 bot commit 造成重複觸發。若外層 workflow 也能先檢查一次,效果最好。
|
||||||
|
|
||||||
> **權限說明**:此 Action 需要 `contents: write`(寫入 findings.json)、`pull-requests: write`(發佈 PR comment)、`issues: write`(發佈 issue comment)三項權限,為正常運作所必要,無法縮減。
|
> **權限說明**:此 Action 需要 `contents: write`(寫入 findings.json)、`pull-requests: write`(發佈 PR comment)、`issues: write`(發佈 issue comment)、以及 commit status 寫入權限,為正常運作所必要,無法縮減。
|
||||||
|
|
||||||
### 1. OpenAI
|
### 1. OpenAI
|
||||||
```yaml
|
```yaml
|
||||||
|
|||||||
@@ -95,6 +95,24 @@ export async function shouldSkipBotCommit({ sha = PR_HEAD_SHA || process.env.GIT
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function setCommitStatus(sha, state, description, context = 'ai-review/critical', targetUrl = '') {
|
||||||
|
if (!sha) throw new Error('commit sha is required for status update');
|
||||||
|
const payload = {
|
||||||
|
state,
|
||||||
|
context,
|
||||||
|
description,
|
||||||
|
};
|
||||||
|
if (targetUrl) payload.target_url = targetUrl;
|
||||||
|
|
||||||
|
const resp = await axios.post(api(`/repos/${GITEA_REPOSITORY}/statuses/${encodeURIComponent(sha)}`), payload, {
|
||||||
|
headers: headers(),
|
||||||
|
timeout: 30000,
|
||||||
|
httpsAgent,
|
||||||
|
});
|
||||||
|
console.log(` ✅ status: sha=${sha} state=${state} context=${context} description=${description}`);
|
||||||
|
return resp.data;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 過濾 diff 內容,移除路徑符合 excludePrefixes 的區塊。
|
* 過濾 diff 內容,移除路徑符合 excludePrefixes 的區塊。
|
||||||
* 每個區塊以 "diff --git a/<prefix>" 開頭判斷,使用 startsWith 精確比對前綴。
|
* 每個區塊以 "diff --git a/<prefix>" 開頭判斷,使用 startsWith 精確比對前綴。
|
||||||
|
|||||||
+20
-1
@@ -1,7 +1,7 @@
|
|||||||
import { describe, it, afterEach, mock } from 'node:test';
|
import { describe, it, afterEach, mock } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { getPRDiff, filterDiff, postComment, getCommitMessageBySha, getBranchHeadCommitMessage, shouldSkipBotCommit } from './gitea.js';
|
import { getPRDiff, filterDiff, postComment, getCommitMessageBySha, getBranchHeadCommitMessage, shouldSkipBotCommit, setCommitStatus } from './gitea.js';
|
||||||
|
|
||||||
afterEach(() => mock.restoreAll());
|
afterEach(() => mock.restoreAll());
|
||||||
|
|
||||||
@@ -95,6 +95,25 @@ describe('gitea', () => {
|
|||||||
});
|
});
|
||||||
await assert.equal(await shouldSkipBotCommit({ sha: 'sha-bot', branch: 'feat/test' }), true);
|
await assert.equal(await shouldSkipBotCommit({ sha: 'sha-bot', branch: 'feat/test' }), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('setCommitStatus posts commit status to Gitea API', async () => {
|
||||||
|
let capturedUrl, capturedBody, capturedOpts;
|
||||||
|
mock.method(axios, 'post', async (url, body, opts) => {
|
||||||
|
capturedUrl = url;
|
||||||
|
capturedBody = body;
|
||||||
|
capturedOpts = opts;
|
||||||
|
return { data: { state: body.state } };
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await setCommitStatus('sha-123', 'failure', 'found 2 critical issues', 'ai-review/critical', 'https://example.com/pr/1');
|
||||||
|
assert.equal(result.state, 'failure');
|
||||||
|
assert.ok(capturedUrl.includes('/statuses/sha-123'));
|
||||||
|
assert.equal(capturedBody.state, 'failure');
|
||||||
|
assert.equal(capturedBody.context, 'ai-review/critical');
|
||||||
|
assert.equal(capturedBody.description, 'found 2 critical issues');
|
||||||
|
assert.equal(capturedBody.target_url, 'https://example.com/pr/1');
|
||||||
|
assert.ok(capturedOpts.headers['Authorization'].startsWith('token '));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filterDiff', () => {
|
describe('filterDiff', () => {
|
||||||
|
|||||||
+24
-2
@@ -1,13 +1,22 @@
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig, FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js';
|
import { GITEA_REPOSITORY, GITEA_SERVER_URL, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig, FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js';
|
||||||
import { loadRoles, getRoleIntro } from './roles.js';
|
import { loadRoles, getRoleIntro } from './roles.js';
|
||||||
import { getPRDiff, postComment, shouldSkipBotCommit } from './gitea.js';
|
import { getPRDiff, postComment, shouldSkipBotCommit, setCommitStatus } from './gitea.js';
|
||||||
import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI } from './findings.js';
|
import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI } from './findings.js';
|
||||||
import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js';
|
import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js';
|
||||||
import { cloneRepo, commitAndPush, getRepoState } from './git.js';
|
import { cloneRepo, commitAndPush, getRepoState } from './git.js';
|
||||||
import { validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
|
import { validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
|
||||||
|
|
||||||
const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace';
|
const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace';
|
||||||
|
const REVIEW_STATUS_CONTEXT = 'ai-review/critical';
|
||||||
|
|
||||||
|
async function updateReviewStatus(sha, criticalCount) {
|
||||||
|
const state = criticalCount > 0 ? 'failure' : 'success';
|
||||||
|
const description = criticalCount > 0
|
||||||
|
? `found ${criticalCount} critical issue${criticalCount === 1 ? '' : 's'}`
|
||||||
|
: 'no critical issues found';
|
||||||
|
await setCommitStatus(sha, state, description, REVIEW_STATUS_CONTEXT, `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}`);
|
||||||
|
}
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
console.log('='.repeat(60));
|
console.log('='.repeat(60));
|
||||||
@@ -17,6 +26,17 @@ async function main() {
|
|||||||
|
|
||||||
if (await shouldSkipBotCommit()) {
|
if (await shouldSkipBotCommit()) {
|
||||||
console.log(' 🤖 偵測到 [ai-review-bot] 自動提交,直接完成 action');
|
console.log(' 🤖 偵測到 [ai-review-bot] 自動提交,直接完成 action');
|
||||||
|
let criticalCount = 0;
|
||||||
|
try {
|
||||||
|
const repoDir = cloneRepo(WORKSPACE);
|
||||||
|
const findings = loadOldFindings(repoDir || WORKSPACE);
|
||||||
|
criticalCount = findings.filter(f => f.level === 'critical').length;
|
||||||
|
console.log(` 🔎 bot-check: current findings critical=${criticalCount}`);
|
||||||
|
await updateReviewStatus(process.env.PR_HEAD_SHA || process.env.GITHUB_SHA, criticalCount);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(` ❌ bot-check: 無法回報 status: ${e.message}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
console.log('='.repeat(60));
|
console.log('='.repeat(60));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
@@ -42,6 +62,7 @@ async function main() {
|
|||||||
|
|
||||||
if (!diff.trim()) {
|
if (!diff.trim()) {
|
||||||
console.log(' ⚠️ diff 為空,無需審查');
|
console.log(' ⚠️ diff 為空,無需審查');
|
||||||
|
await updateReviewStatus(process.env.PR_HEAD_SHA || process.env.GITHUB_SHA, 0);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,6 +154,7 @@ async function main() {
|
|||||||
// Step9: 有 critical 問題則 exit 1
|
// Step9: 有 critical 問題則 exit 1
|
||||||
console.log('\n🚦 Step8: 嚴重問題檢查');
|
console.log('\n🚦 Step8: 嚴重問題檢查');
|
||||||
const criticalCount = filtered.filter(f => f.level === 'critical').length;
|
const criticalCount = filtered.filter(f => f.level === 'critical').length;
|
||||||
|
await updateReviewStatus(process.env.PR_HEAD_SHA || process.env.GITHUB_SHA, criticalCount);
|
||||||
if (criticalCount > 0) {
|
if (criticalCount > 0) {
|
||||||
console.log(` ❌ 發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1)`);
|
console.log(` ❌ 發現 ${criticalCount} 個嚴重問題,workflow 結束(exit 1)`);
|
||||||
console.log('='.repeat(60));
|
console.log('='.repeat(60));
|
||||||
|
|||||||
Reference in New Issue
Block a user