Compare commits

...

9 Commits

7 changed files with 95 additions and 18 deletions
+37 -1
View File
@@ -1 +1,37 @@
[]
[
{
"level": "critical",
"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": false
},
{
"level": "warning",
"role": "Leo",
"location": "action.yaml:5",
"suggestion": "輸入 `GITEA_TOKEN` 的註解 `Gitea 相關(可從 gitea context 自動取得)` 已不再準確。由於 `GITEA_TOKEN` 現在是 `required: true` 且不再從 `secrets.GITEA_TOKEN` 取得,建議更新此註解以明確指出此 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": false
},
{
"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": false
},
{
"level": "warning",
"role": "Maya",
"location": "action.yaml:80",
"suggestion": "GITEA_TOKEN 的來源已從 `inputs.GITEA_TOKEN || secrets.GITEA_TOKEN` 變更為僅 `inputs.GITEA_TOKEN`。雖然 `required: true` 已經設定,但仍建議在測試中明確涵蓋此邏輯變更,確保 GITEA_TOKEN 確實只從輸入取得,並且不再嘗試回溯到 secrets,以防止未來潛在的誤解或回歸。",
"is_new": true
}
]
+2 -3
View File
@@ -1,9 +1,8 @@
name: AI
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
version:
+10 -2
View File
@@ -2,7 +2,7 @@
這是一個 AI Code Review Action。Gitea Workflow 可以使用此 Action 讓 AI 助理根據不同面向分析 Push Request 中變更的內容後,將問題分級 Commnet 到 Push Request 中。
# 流程(新 Push Request、新 Commit (排除 AI 助理的 Commit) 觸發)
# 流程(新 Push Request、新 Commit 觸發;若偵測到 AI 助理的自動提交則直接跳過)
1. 服務名稱、模型名稱、角色資訊(個性、符合個性的英文名稱、工作內容),Comment 到 Push Request
2. 每個角色個別分析 Git Diff 的內容產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議)
@@ -11,7 +11,7 @@
5. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request
6. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request
7. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request
8. Commit 問題檔案,將 workspace 中實際存在的同步檔覆蓋到記憶區;workspace 沒有的同步檔就略過,不會刪除記憶區既有內容
8. Commit 問題檔案,將 workspace 中實際存在的同步檔覆蓋到記憶區;workspace 沒有的同步檔就略過,不會刪除記憶區既有內容。自動提交的 commit message 會帶上 `[ai-review-bot]`,供 workflow 判斷是否要跳過重跑
9. 如果 PR 問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)
# 設計
@@ -33,6 +33,8 @@
2.`.gitea/workflows` 資料夾中建立 `ai-review.yaml'
3.`ai-review.yaml` 中填入以下內容(選擇一個使用)
> **自動提交排除說明**:此 Action 會將自己的 commit message 標記為 `[ai-review-bot]`,而且 action 執行時也會先檢查 head commit 是否含有這個 marker,若有就直接成功結束,避免 bot commit 造成重複觸發。若外層 workflow 也能先檢查一次,效果最好。
> **權限說明**:此 Action 需要 `contents: write`(寫入 findings.json)、`pull-requests: write`(發佈 PR comment)、`issues: write`(發佈 issue comment)三項權限,為正常運作所必要,無法縮減。
### 1. OpenAI
@@ -54,6 +56,7 @@ jobs:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # 支援逗號分隔多個 Key
OPENAI_BASE_URL: https://api.openai.com/v1
OPENAI_MODEL: ${{ vars.OPENAI_MODEL }}
@@ -82,6 +85,7 @@ jobs:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENROUTER_API_KEY }},${{ secrets.OPENROUTER_API_KEY_1 }}
OPENAI_BASE_URL: https://openrouter.ai/api/v1
OPENAI_MODEL: ${{ vars.OPENROUTER_MODEL }}
@@ -110,6 +114,7 @@ jobs:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} # 支援逗號分隔多個 Key
CLAUDE_BASE_URL: https://api.anthropic.com/v1
permissions:
@@ -137,6 +142,7 @@ jobs:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }},${{ secrets.GEMINI_API_KEY_1 }},${{ secrets.GEMINI_API_KEY_2 }},${{ secrets.GEMINI_API_KEY_3 }},${{ secrets.GEMINI_API_KEY_4 }},${{ secrets.GEMINI_API_KEY_5 }},${{ secrets.GEMINI_API_KEY_6 }},${{ secrets.GEMINI_API_KEY_7 }},${{ secrets.GEMINI_API_KEY_8 }},${{ secrets.GEMINI_API_KEY_9 }},${{ secrets.GEMINI_API_KEY_10 }},${{ secrets.GEMINI_API_KEY_11 }},${{ secrets.GEMINI_API_KEY_12 }},${{ secrets.GEMINI_API_KEY_13 }},${{ secrets.GEMINI_API_KEY_14 }},${{ secrets.GEMINI_API_KEY_15 }},${{ secrets.GEMINI_API_KEY_16 }},${{ secrets.GEMINI_API_KEY_17 }},${{ secrets.GEMINI_API_KEY_18 }},${{ secrets.GEMINI_API_KEY_19 }}
GEMINI_BASE_URL: https://generativelanguage.googleapis.com/v1beta
GEMINI_MODEL: ${{ vars.GEMINI_MODEL }}
@@ -165,6 +171,7 @@ jobs:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
AMAZONQ_API_KEY: ${{ secrets.AMAZONQ_API_KEY }} # 支援逗號分隔多個 Key
AMAZONQ_BASE_URL: https://q.api.aws
permissions:
@@ -193,6 +200,7 @@ jobs:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
OLLAMA_BASE_URL: https://ollama.jsc.idv.me/v1
OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }}
permissions:
+3 -3
View File
@@ -5,7 +5,7 @@ inputs:
# Gitea 相關(可從 gitea context 自動取得)
GITEA_TOKEN:
description: 'Gitea API Token'
required: false
required: true
GITEA_SERVER_URL:
description: 'Gitea Server URL'
required: false
@@ -80,8 +80,8 @@ runs:
using: 'docker'
image: 'Dockerfile'
env:
# Gitea context優先用 inputs,否則從 gitea context 取)
GITEA_TOKEN: ${{ inputs.GITEA_TOKEN || secrets.GITEA_TOKEN }}
# Gitea context改為只從 inputs 取
GITEA_TOKEN: ${{ inputs.GITEA_TOKEN }}
GITEA_SERVER_URL: ${{ inputs.GITEA_SERVER_URL || gitea.server_url }}
GITEA_REPOSITORY: ${{ inputs.GITEA_REPOSITORY || gitea.repository }}
GITEA_SKIP_TLS_VERIFY: ${{ inputs.GITEA_SKIP_TLS_VERIFY }}
+11 -1
View File
@@ -7,6 +7,7 @@ import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDIN
const ACTION_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const GENERATED_SYNC_PATHS = [FINDINGS_PATH, '.gitea/ai-review/exclusions.json'];
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
export const BOT_COMMIT_MARKER = '[ai-review-bot]';
export const SYNC_PATHS = [
'.amazonq/rules/triage-findings.md',
'.codex/skills/triage-findings/SKILL.md',
@@ -58,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)
*/
@@ -124,7 +134,7 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync,
return;
}
const out = run(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir);
const out = run(['commit', '-m', `chore: update ai-review findings ${BOT_COMMIT_MARKER}`], repoDir);
const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown';
try {
run(['push', remoteUrl, PR_HEAD_BRANCH], repoDir, credEnv);
+19 -1
View File
@@ -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 } from './git.js';
import { commitAndPush, cloneRepo, SYNC_PATHS, BOT_COMMIT_MARKER, getHeadCommitMessage, isBotAutoCommit } from './git.js';
// --- helpers ---
function makeTmpWorkspace() {
@@ -60,6 +60,15 @@ describe('commitAndPush', () => {
}
});
it('tags auto commits with the bot marker for workflow filtering', async () => {
const spawn = makeSpawn();
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);
const commitCall = spawn.calls.find(c => c.args[0] === 'commit');
assert.ok(commitCall, 'expected git commit to run');
assert.ok(commitCall.args.some(arg => arg.includes(BOT_COMMIT_MARKER)), 'expected commit message to include bot marker');
});
it('uses GIT_ASKPASS env for network operations (fetch, push, clone)', async () => {
const spawn = makeSpawn();
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn, sourceRoot);
@@ -232,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);
});
});
+7 -1
View File
@@ -4,7 +4,7 @@ import { loadRoles, getRoleIntro } from './roles.js';
import { getPRDiff, postComment } 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';
import { cloneRepo, commitAndPush, getRepoState, isBotAutoCommit } from './git.js';
import { validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
const WORKSPACE = process.env.GITHUB_WORKSPACE || '/workspace';
@@ -15,6 +15,12 @@ async function main() {
console.log(` repo=${GITEA_REPOSITORY} PR=#${PR_NUMBER}`);
console.log(` ${PR_HEAD_BRANCH} -> ${PR_BASE_BRANCH}`);
if (isBotAutoCommit(WORKSPACE)) {
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');