fix: detect ai review bot commits via api
This commit is contained in:
@@ -33,7 +33,7 @@
|
|||||||
2. 在 `.gitea/workflows` 資料夾中建立 `ai-review.yaml'
|
2. 在 `.gitea/workflows` 資料夾中建立 `ai-review.yaml'
|
||||||
3. 在 `ai-review.yaml` 中填入以下內容(選擇一個使用):
|
3. 在 `ai-review.yaml` 中填入以下內容(選擇一個使用):
|
||||||
|
|
||||||
> **自動提交排除說明**:此 Action 會將自己的 commit message 標記為 `[ai-review-bot]`,而且 action 執行時也會先檢查 head commit 是否含有這個 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)三項權限,為正常運作所必要,無法縮減。
|
||||||
|
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ runs:
|
|||||||
GITEA_REPOSITORY: ${{ inputs.GITEA_REPOSITORY || gitea.repository }}
|
GITEA_REPOSITORY: ${{ inputs.GITEA_REPOSITORY || gitea.repository }}
|
||||||
GITEA_SKIP_TLS_VERIFY: ${{ inputs.GITEA_SKIP_TLS_VERIFY }}
|
GITEA_SKIP_TLS_VERIFY: ${{ inputs.GITEA_SKIP_TLS_VERIFY }}
|
||||||
PR_NUMBER: ${{ inputs.PR_NUMBER || gitea.event.pull_request.number }}
|
PR_NUMBER: ${{ inputs.PR_NUMBER || gitea.event.pull_request.number }}
|
||||||
|
PR_HEAD_SHA: ${{ inputs.PR_HEAD_SHA || gitea.event.pull_request.head.sha }}
|
||||||
PR_HEAD_BRANCH: ${{ inputs.PR_HEAD_BRANCH || gitea.event.pull_request.head.ref }}
|
PR_HEAD_BRANCH: ${{ inputs.PR_HEAD_BRANCH || gitea.event.pull_request.head.ref }}
|
||||||
PR_BASE_BRANCH: ${{ inputs.PR_BASE_BRANCH || gitea.event.pull_request.base.ref }}
|
PR_BASE_BRANCH: ${{ inputs.PR_BASE_BRANCH || gitea.event.pull_request.base.ref }}
|
||||||
# LLM
|
# LLM
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ export const GITEA_SERVER_URL = process.env.GITEA_SERVER_URL || 'https://gitea.c
|
|||||||
export const GITEA_REPOSITORY = process.env.GITEA_REPOSITORY || '';
|
export const GITEA_REPOSITORY = process.env.GITEA_REPOSITORY || '';
|
||||||
export const GITEA_SKIP_TLS_VERIFY = process.env.GITEA_SKIP_TLS_VERIFY === 'true';
|
export const GITEA_SKIP_TLS_VERIFY = process.env.GITEA_SKIP_TLS_VERIFY === 'true';
|
||||||
export const PR_NUMBER = process.env.PR_NUMBER || '';
|
export const PR_NUMBER = process.env.PR_NUMBER || '';
|
||||||
|
export const PR_HEAD_SHA = process.env.PR_HEAD_SHA || '';
|
||||||
export const PR_HEAD_BRANCH = process.env.PR_HEAD_BRANCH || '';
|
export const PR_HEAD_BRANCH = process.env.PR_HEAD_BRANCH || '';
|
||||||
export const PR_BASE_BRANCH = process.env.PR_BASE_BRANCH || '';
|
export const PR_BASE_BRANCH = process.env.PR_BASE_BRANCH || '';
|
||||||
|
|
||||||
|
|||||||
+38
-1
@@ -1,6 +1,6 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import https from 'https';
|
import https from 'https';
|
||||||
import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_SKIP_TLS_VERIFY, PR_NUMBER } from './config.js';
|
import { GITEA_TOKEN, GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_SKIP_TLS_VERIFY, PR_NUMBER, PR_HEAD_SHA, PR_HEAD_BRANCH } from './config.js';
|
||||||
|
|
||||||
const httpsAgent = GITEA_SKIP_TLS_VERIFY ? new https.Agent({ rejectUnauthorized: false }) : undefined;
|
const httpsAgent = GITEA_SKIP_TLS_VERIFY ? new https.Agent({ rejectUnauthorized: false }) : undefined;
|
||||||
const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' });
|
const headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' });
|
||||||
@@ -25,6 +25,43 @@ export async function getPRDiff() {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getCommitMessageBySha(sha) {
|
||||||
|
if (!sha) return '';
|
||||||
|
try {
|
||||||
|
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/git/commits/${encodeURIComponent(sha)}`), {
|
||||||
|
headers: headers(),
|
||||||
|
timeout: 30000,
|
||||||
|
httpsAgent,
|
||||||
|
});
|
||||||
|
return resp.data?.message || '';
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBranchHeadCommitMessage(branch = PR_HEAD_BRANCH) {
|
||||||
|
if (!branch) return '';
|
||||||
|
try {
|
||||||
|
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/branches/${encodeURIComponent(branch)}`), {
|
||||||
|
headers: headers(),
|
||||||
|
timeout: 30000,
|
||||||
|
httpsAgent,
|
||||||
|
});
|
||||||
|
const sha = resp.data?.commit?.id || resp.data?.commit?.sha || '';
|
||||||
|
return await getCommitMessageBySha(sha);
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function shouldSkipBotCommit({ sha = PR_HEAD_SHA || process.env.GITHUB_SHA, branch = PR_HEAD_BRANCH } = {}) {
|
||||||
|
const candidates = [
|
||||||
|
await getCommitMessageBySha(sha),
|
||||||
|
await getBranchHeadCommitMessage(branch),
|
||||||
|
].filter(Boolean);
|
||||||
|
return candidates.some(message => message.includes('[ai-review-bot]'));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 過濾 diff 內容,移除路徑符合 excludePrefixes 的區塊。
|
* 過濾 diff 內容,移除路徑符合 excludePrefixes 的區塊。
|
||||||
* 每個區塊以 "diff --git a/<prefix>" 開頭判斷,使用 startsWith 精確比對前綴。
|
* 每個區塊以 "diff --git a/<prefix>" 開頭判斷,使用 startsWith 精確比對前綴。
|
||||||
|
|||||||
+40
-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 } from './gitea.js';
|
import { getPRDiff, filterDiff, postComment, getCommitMessageBySha, getBranchHeadCommitMessage, shouldSkipBotCommit } from './gitea.js';
|
||||||
|
|
||||||
afterEach(() => mock.restoreAll());
|
afterEach(() => mock.restoreAll());
|
||||||
|
|
||||||
@@ -56,6 +56,45 @@ describe('gitea', () => {
|
|||||||
mock.method(axios, 'post', async () => { throw new Error('api error'); });
|
mock.method(axios, 'post', async () => { throw new Error('api error'); });
|
||||||
await assert.rejects(() => postComment('test'), /api error/);
|
await assert.rejects(() => postComment('test'), /api error/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('getCommitMessageBySha reads commit message from Gitea API', async () => {
|
||||||
|
let capturedUrl;
|
||||||
|
mock.method(axios, 'get', async (url) => {
|
||||||
|
capturedUrl = url;
|
||||||
|
return { data: { message: 'chore: update ai-review findings [ai-review-bot]' } };
|
||||||
|
});
|
||||||
|
const message = await getCommitMessageBySha('abc123');
|
||||||
|
assert.ok(capturedUrl.includes('/git/commits/abc123'));
|
||||||
|
assert.ok(message.includes('[ai-review-bot]'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getBranchHeadCommitMessage reads branch head commit message from Gitea API', async () => {
|
||||||
|
const urls = [];
|
||||||
|
mock.method(axios, 'get', async (url) => {
|
||||||
|
urls.push(url);
|
||||||
|
if (url.includes('/branches/feat%2Ftest')) {
|
||||||
|
return { data: { commit: { id: 'abc123' } } };
|
||||||
|
}
|
||||||
|
return { data: { message: 'chore: update ai-review findings [ai-review-bot]' } };
|
||||||
|
});
|
||||||
|
const message = await getBranchHeadCommitMessage('feat/test');
|
||||||
|
assert.ok(urls.some(url => url.includes('/branches/feat%2Ftest')));
|
||||||
|
assert.ok(urls.some(url => url.includes('/git/commits/abc123')));
|
||||||
|
assert.ok(message.includes('[ai-review-bot]'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('shouldSkipBotCommit returns true when either sha or branch head is bot commit', async () => {
|
||||||
|
mock.method(axios, 'get', async (url) => {
|
||||||
|
if (url.includes('/git/commits/sha-bot')) {
|
||||||
|
return { data: { message: 'chore: update ai-review findings [ai-review-bot]' } };
|
||||||
|
}
|
||||||
|
if (url.includes('/branches/feat%2Ftest')) {
|
||||||
|
return { data: { commit: { id: 'sha-bot' } } };
|
||||||
|
}
|
||||||
|
return { data: { message: 'regular commit' } };
|
||||||
|
});
|
||||||
|
await assert.equal(await shouldSkipBotCommit({ sha: 'sha-bot', branch: 'feat/test' }), true);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filterDiff', () => {
|
describe('filterDiff', () => {
|
||||||
|
|||||||
+3
-3
@@ -1,10 +1,10 @@
|
|||||||
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, 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 } from './gitea.js';
|
import { getPRDiff, postComment, shouldSkipBotCommit } 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, isBotAutoCommit } 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';
|
||||||
@@ -15,7 +15,7 @@ async function main() {
|
|||||||
console.log(` repo=${GITEA_REPOSITORY} PR=#${PR_NUMBER}`);
|
console.log(` repo=${GITEA_REPOSITORY} PR=#${PR_NUMBER}`);
|
||||||
console.log(` ${PR_HEAD_BRANCH} -> ${PR_BASE_BRANCH}`);
|
console.log(` ${PR_HEAD_BRANCH} -> ${PR_BASE_BRANCH}`);
|
||||||
|
|
||||||
if (isBotAutoCommit(WORKSPACE)) {
|
if (await shouldSkipBotCommit()) {
|
||||||
console.log(' 🤖 偵測到 [ai-review-bot] 自動提交,直接完成 action');
|
console.log(' 🤖 偵測到 [ai-review-bot] 自動提交,直接完成 action');
|
||||||
console.log('='.repeat(60));
|
console.log('='.repeat(60));
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
|
|||||||
Reference in New Issue
Block a user