Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35150cae8a | |||
| e216ca08c5 | |||
| 888bf0b359 | |||
| 59e942f24b | |||
| 82ecbd3463 | |||
| f3319b5ec4 | |||
| ee593418f0 | |||
| 9012fe64d1 | |||
| 3ae08052a3 |
@@ -4,27 +4,20 @@
|
||||
"role": "Maya",
|
||||
"location": "action.yaml:6, action.yaml:81",
|
||||
"suggestion": "由於 `GITEA_TOKEN` 現在被設定為 `required: true` 且移除了 `secrets.GITEA_TOKEN` 的 fallback 機制,這是一個關鍵性的行為變更。請務必新增整合測試 (integration tests) 來驗證以下情境:\n1. 當 `inputs.GITEA_TOKEN` 未提供時,Action 應如預期般失敗。\n2. 當 `inputs.GITEA_TOKEN` 有提供時,Action 應能正常執行。\n這將確保新的輸入要求和邏輯變更不會導致意外的行為或破壞現有工作流程。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"level": "critical",
|
||||
"role": "Leo",
|
||||
"location": "action.yaml:5",
|
||||
"suggestion": "輸入 `GITEA_TOKEN` 的註解 `Gitea 相關(可從 gitea context 自動取得)` 已不再準確。由於 `GITEA_TOKEN` 現在是 `required: true` 且不再從 `secrets.GITEA_TOKEN` 取得,建議更新此註解以明確指出此 Token 必須透過 `inputs` 提供。",
|
||||
"is_new": true
|
||||
"location": "action.yaml:12",
|
||||
"suggestion": "建議將 `GITEA_TOKEN` 的環境變數設定改回 `GITEA_TOKEN: ${{ inputs.GITEA_TOKEN || secrets.GITEA_TOKEN }}`。目前將其設定為 `required: true` 並移除 `secrets.GITEA_TOKEN` 的 fallback 機制,會導致現有依賴 `secrets.GITEA_TOKEN` 的工作流程中斷,並降低配置的彈性。如果目的是強制透過 `inputs` 傳遞,應在文件明確說明此重大變更及其原因。",
|
||||
"is_new": false
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Leo",
|
||||
"location": "action.yaml:80",
|
||||
"suggestion": "在 `runs.env` 區塊中,`GITEA_TOKEN` 現在只從 `inputs` 取得,但 `GITEA_SERVER_URL` 和 `GITEA_REPOSITORY` 仍保留從 `gitea context` 取得的備用機制。這種處理方式的不一致性可能會造成未來的維護困擾。建議統一所有 Gitea 相關變數的取得邏輯,或提供明確的註解說明此差異的原因。",
|
||||
"is_new": true
|
||||
},
|
||||
{
|
||||
"level": "warning",
|
||||
"role": "Rex",
|
||||
"location": "action.yaml:83",
|
||||
"suggestion": "建議將 `GITEA_TOKEN` 的環境變數設定改回 `GITEA_TOKEN: ${{ inputs.GITEA_TOKEN || secrets.GITEA_TOKEN }}`。此變更移除了從 `secrets.GITEA_TOKEN` 安全取得 Token 的備用機制。雖然 `inputs.GITEA_TOKEN` 可以透過 `secrets` 上下文安全傳遞(例如:`with: GITEA_TOKEN: ${{ secrets.MY_GITEA_TOKEN }}`),但若使用者不慎直接將敏感 Token 字串作為 `inputs.GITEA_TOKEN` 的值傳入,該 Token 將可能被記錄在日誌中,導致敏感資訊洩漏。保留備用機制可提供更強健的安全性,降低因使用者操作失誤而導致的風險。",
|
||||
"is_new": true
|
||||
"is_new": false
|
||||
}
|
||||
]
|
||||
|
||||
@@ -5,36 +5,9 @@ on:
|
||||
- master
|
||||
types: [opened, synchronize]
|
||||
jobs:
|
||||
detect-bot-commit:
|
||||
name: 偵測自動提交
|
||||
runs-on: ubuntu
|
||||
outputs:
|
||||
skip: ${{ steps.detect.outputs.skip }}
|
||||
steps:
|
||||
- name: 檢查 head commit marker
|
||||
id: detect
|
||||
env:
|
||||
GITEA_API_URL: ${{ github.api_url }}
|
||||
GITEA_REPOSITORY: ${{ github.repository }}
|
||||
GITEA_SHA: ${{ github.sha }}
|
||||
GITEA_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -e
|
||||
commit_json="$(curl -fsSL -H "Authorization: token ${GITEA_TOKEN}" "${GITEA_API_URL}/repos/${GITEA_REPOSITORY}/git/commits/${GITEA_SHA}")" || {
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
}
|
||||
if printf '%s' "$commit_json" | grep -q '\[ai-review-bot\]'; then
|
||||
echo "skip=true" >> "$GITHUB_OUTPUT"
|
||||
echo "偵測到 AI Review Bot commit,跳過 review workflow"
|
||||
else
|
||||
echo "skip=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
version:
|
||||
name: 計算版本號
|
||||
runs-on: ubuntu
|
||||
needs: [detect-bot-commit]
|
||||
if: needs.detect-bot-commit.outputs.skip != 'true'
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
@@ -52,8 +25,7 @@ jobs:
|
||||
code-review:
|
||||
name: Code Review
|
||||
runs-on: ubuntu
|
||||
needs: [detect-bot-commit, version]
|
||||
if: needs.detect-bot-commit.outputs.skip != 'true'
|
||||
needs: [version]
|
||||
steps:
|
||||
- name: AI Code Review
|
||||
uses: https://gitea.jsc.idv.tw/actions/code-review@v${{ needs.version.outputs.version }}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
2. 在 `.gitea/workflows` 資料夾中建立 `ai-review.yaml'
|
||||
3. 在 `ai-review.yaml` 中填入以下內容(選擇一個使用):
|
||||
|
||||
> **自動提交排除說明**:此 Action 會將自己的 commit message 標記為 `[ai-review-bot]`。建議在 review workflow 的最前面先檢查 head commit 是否含有這個 marker,若有就直接成功結束,避免 bot commit 造成重複觸發。
|
||||
> **自動提交排除說明**:此 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)三項權限,為正常運作所必要,無法縮減。
|
||||
|
||||
|
||||
@@ -86,6 +86,7 @@ runs:
|
||||
GITEA_REPOSITORY: ${{ inputs.GITEA_REPOSITORY || gitea.repository }}
|
||||
GITEA_SKIP_TLS_VERIFY: ${{ inputs.GITEA_SKIP_TLS_VERIFY }}
|
||||
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_BASE_BRANCH: ${{ inputs.PR_BASE_BRANCH || gitea.event.pull_request.base.ref }}
|
||||
# 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_SKIP_TLS_VERIFY = process.env.GITEA_SKIP_TLS_VERIFY === 'true';
|
||||
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_BASE_BRANCH = process.env.PR_BASE_BRANCH || '';
|
||||
|
||||
|
||||
@@ -59,6 +59,15 @@ export function getRepoState(repoDir, _spawnSync = spawnSync) {
|
||||
return { repoDir, branch, headSha, shortSha, commitTime };
|
||||
}
|
||||
|
||||
export function getHeadCommitMessage(repoDir, _spawnSync = spawnSync) {
|
||||
const run = makeRunner(_spawnSync);
|
||||
return readGitOutput(run, ['show', '-s', '--format=%B', 'HEAD'], repoDir);
|
||||
}
|
||||
|
||||
export function isBotAutoCommit(repoDir, _spawnSync = spawnSync) {
|
||||
return getHeadCommitMessage(repoDir, _spawnSync).includes(BOT_COMMIT_MARKER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clone PR head branch to workspace/repo (idempotent)
|
||||
*/
|
||||
|
||||
+10
-1
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
||||
import fs from 'fs';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import { commitAndPush, cloneRepo, SYNC_PATHS, BOT_COMMIT_MARKER } from './git.js';
|
||||
import { commitAndPush, cloneRepo, SYNC_PATHS, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit } from './git.js';
|
||||
|
||||
// --- helpers ---
|
||||
function makeTmpWorkspace() {
|
||||
@@ -241,4 +241,13 @@ describe('cloneRepo', () => {
|
||||
const result = cloneRepo(workspace, spawn);
|
||||
assert.equal(result, path.join(workspace, 'repo'));
|
||||
});
|
||||
|
||||
it('reads head commit message and detects bot auto commits', () => {
|
||||
const spawn = makeSpawn({
|
||||
show: () => ({ status: 0, stdout: `chore: update ai-review findings ${BOT_COMMIT_MARKER}\n`, stderr: '', error: null }),
|
||||
});
|
||||
|
||||
assert.ok(getHeadCommitMessage(workspace, spawn).includes(BOT_COMMIT_MARKER));
|
||||
assert.equal(isBotAutoCommit(workspace, spawn), true);
|
||||
});
|
||||
});
|
||||
|
||||
+71
-1
@@ -1,11 +1,18 @@
|
||||
import axios from 'axios';
|
||||
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 headers = () => ({ Authorization: `token ${GITEA_TOKEN}`, 'Content-Type': 'application/json' });
|
||||
const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`;
|
||||
|
||||
function extractCommitMessage(payload) {
|
||||
return payload?.message
|
||||
|| payload?.commit?.message
|
||||
|| payload?.commit?.commit?.message
|
||||
|| '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 取得 PR 的 Git Diff 內容,已自動排除 .gitea/ 資料夾。
|
||||
*/
|
||||
@@ -25,6 +32,69 @@ 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,
|
||||
});
|
||||
const message = extractCommitMessage(resp.data);
|
||||
console.log(` 🔎 bot-check: commit api sha=${sha} keys=${Object.keys(resp.data || {}).join(',') || 'empty'} message=${message ? 'found' : 'empty'}`);
|
||||
return message;
|
||||
} catch (e) {
|
||||
console.log(` ⚠️ bot-check: 讀取 commit sha=${sha} 失敗: ${e.message}`);
|
||||
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 || '';
|
||||
console.log(` 🔎 bot-check: branch api branch=${branch} keys=${Object.keys(resp.data || {}).join(',') || 'empty'} sha=${sha || 'empty'} message=${extractCommitMessage(resp.data?.commit) ? 'found' : 'empty'}`);
|
||||
return await getCommitMessageBySha(sha);
|
||||
} catch (e) {
|
||||
console.log(` ⚠️ bot-check: 讀取 branch=${branch} head commit 失敗: ${e.message}`);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function shouldSkipBotCommit({ sha = PR_HEAD_SHA || process.env.GITHUB_SHA, branch = PR_HEAD_BRANCH } = {}) {
|
||||
console.log(` 🔎 bot-check: start PR_HEAD_SHA=${PR_HEAD_SHA || 'empty'} GITHUB_SHA=${process.env.GITHUB_SHA || 'empty'} sha=${sha || 'empty'} branch=${branch || 'empty'}`);
|
||||
|
||||
const shaMessage = await getCommitMessageBySha(sha);
|
||||
if (sha) {
|
||||
console.log(` 🔎 bot-check: sha=${sha} message=${shaMessage ? 'found' : 'empty'}`);
|
||||
if (shaMessage.includes('[ai-review-bot]')) {
|
||||
console.log(' ✅ bot-check: matched commit sha marker');
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
console.log(' 🔎 bot-check: skip sha lookup because sha is empty');
|
||||
}
|
||||
|
||||
const branchMessage = await getBranchHeadCommitMessage(branch);
|
||||
if (branch) {
|
||||
console.log(` 🔎 bot-check: branch=${branch} head_message=${branchMessage ? 'found' : 'empty'}`);
|
||||
if (branchMessage.includes('[ai-review-bot]')) {
|
||||
console.log(' ✅ bot-check: matched branch head marker');
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
console.log(' 🔎 bot-check: skip branch lookup because branch is empty');
|
||||
}
|
||||
|
||||
console.log(' ℹ️ bot-check: no [ai-review-bot] marker found');
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 過濾 diff 內容,移除路徑符合 excludePrefixes 的區塊。
|
||||
* 每個區塊以 "diff --git a/<prefix>" 開頭判斷,使用 startsWith 精確比對前綴。
|
||||
|
||||
+40
-1
@@ -1,7 +1,7 @@
|
||||
import { describe, it, afterEach, mock } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import axios from 'axios';
|
||||
import { getPRDiff, filterDiff, postComment } from './gitea.js';
|
||||
import { getPRDiff, filterDiff, postComment, getCommitMessageBySha, getBranchHeadCommitMessage, shouldSkipBotCommit } from './gitea.js';
|
||||
|
||||
afterEach(() => mock.restoreAll());
|
||||
|
||||
@@ -56,6 +56,45 @@ describe('gitea', () => {
|
||||
mock.method(axios, 'post', async () => { throw new Error('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', () => {
|
||||
|
||||
+7
-1
@@ -1,7 +1,7 @@
|
||||
import path from 'path';
|
||||
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 { 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 { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js';
|
||||
import { cloneRepo, commitAndPush, getRepoState } from './git.js';
|
||||
@@ -15,6 +15,12 @@ async function main() {
|
||||
console.log(` repo=${GITEA_REPOSITORY} PR=#${PR_NUMBER}`);
|
||||
console.log(` ${PR_HEAD_BRANCH} -> ${PR_BASE_BRANCH}`);
|
||||
|
||||
if (await shouldSkipBotCommit()) {
|
||||
console.log(' 🤖 偵測到 [ai-review-bot] 自動提交,直接完成 action');
|
||||
console.log('='.repeat(60));
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const { provider, baseURL, model } = getLLMConfig();
|
||||
if (!provider) {
|
||||
console.error('❌ 未設定任何 LLM API Key,請檢查 action inputs');
|
||||
|
||||
Reference in New Issue
Block a user