Compare commits
3 Commits
v0.1.1-beta.3
...
v0.1.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 8bf791a829 | |||
| c88c0d02c8 | |||
| f43ba63f0f |
@@ -6,8 +6,8 @@
|
||||
|
||||
1. 服務名稱、模型名稱、角色資訊(個性、符合個性的英文名稱、工作內容),Comment 到 Push Request
|
||||
2. 每個角色個別分析 Git Diff 的內容產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議)
|
||||
3. 讀取所有未解決的舊問題(問題檔案 `.gitea/ai-review/findings.json` 存在於使用此 Action 的專案固定位置)加上新問題後,去除重複產生本次 Push Request 的問題表格(PR問題表格)覆蓋問題檔案
|
||||
4. 讀取排除問題檔案(`.gitea/ai-review/exclusions.json` 存在於使用此 Action 的專案固定位置),用來過濾PR問題表格中不需要處理的問題
|
||||
3. 讀取來源分支中的所有未解決舊問題(問題檔案 `.gitea/ai-review/findings.json`)加上新問題後,去除重複產生本次 Push Request 的問題表格(PR問題表格)覆蓋問題檔案
|
||||
4. 讀取來源分支中的排除問題檔案(`.gitea/ai-review/exclusions.json`),用來過濾PR問題表格中不需要處理的問題
|
||||
5. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request
|
||||
6. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request
|
||||
7. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request
|
||||
@@ -23,7 +23,7 @@
|
||||
5. 將提示詞放到 ./app/prompts 內供程式讀取
|
||||
6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1
|
||||
7. 讀取 Git Diff 時排除 `.gitea/`、`.amazonq/`、`.claude/`、`.codex/`、`.gemini/`、`.github/` 資料夾,以及 `CLAUDE.md`、`GEMINI.md`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼
|
||||
8. 階段七驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 `[]`
|
||||
8. 階段七驗證來源分支中的 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 `[]`
|
||||
9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量
|
||||
|
||||
# 使用說明
|
||||
@@ -227,4 +227,4 @@ Amazon Q:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
||||
|
||||
### 版本包含
|
||||
|
||||
提交時一併包含 `triage-findings` skill 與各平台入口檔;已存在檔案一律覆蓋,同步到最新內容;若 workspace 沒有某個同步檔,記憶區會保留原檔,不做刪除。寫入 `.gitea/ai-review/exclusions.json` 時,盡量保留原始問題文字的語言與語意,避免過度改寫。未來若新增任何 skill 或新增其他平台的 skill 入口,必須同時把對應檔案複製進 Docker image,並把同步清單更新到會使用此 action 的目標專案,避免 action 與目標專案內容脫節。
|
||||
提交時一併包含 `triage-findings` skill 與各平台入口檔;已存在檔案一律覆蓋,同步到最新內容;若 workspace 沒有某個同步檔,記憶區會保留原檔,不做刪除。`findings.json` 與 `exclusions.json` 都從使用此 action 的存取庫來源分支讀取,而不是從 action 本地 workspace 讀取。寫入 `.gitea/ai-review/exclusions.json` 時,盡量保留原始問題文字的語言與語意,避免過度改寫。未來若新增任何 skill 或新增其他平台的 skill 入口,必須同時把對應檔案複製進 Docker image,並把同步清單更新到會使用此 action 的目標專案,避免 action 與目標專案內容脫節。
|
||||
|
||||
+23
-3
@@ -34,8 +34,14 @@ function readJSONArray(fullPath, label) {
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeExclusions(data) {
|
||||
if (Array.isArray(data)) return data;
|
||||
if (data && Array.isArray(data.excluded_findings)) return data.excluded_findings;
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* 讀取舊 findings(從 workspace 的 FINDINGS_PATH)
|
||||
* 讀取舊 findings(從來源分支的 cloned repoDir 中的 FINDINGS_PATH)
|
||||
*/
|
||||
export function loadOldFindings(workspace) {
|
||||
const old = readJSONArray(path.join(workspace, FINDINGS_PATH), '舊 findings ').map(f => ({ ...f, is_new: false }));
|
||||
@@ -104,10 +110,24 @@ export async function deduplicateWithAI(findings) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 讀取排除問題檔案(從 workspace 的 EXCLUSIONS_PATH)
|
||||
* 讀取排除問題檔案(從來源分支的 cloned repoDir 中的 EXCLUSIONS_PATH)
|
||||
*/
|
||||
export function loadExclusions(workspace) {
|
||||
const exclusions = readJSONArray(path.join(workspace, EXCLUSIONS_PATH), '排除問題');
|
||||
const fullPath = path.join(workspace, EXCLUSIONS_PATH);
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
console.log(' 排除問題檔案不存在,視為空');
|
||||
console.log(' 讀取排除問題: 0 筆');
|
||||
return [];
|
||||
}
|
||||
|
||||
let exclusions = [];
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
|
||||
exclusions = normalizeExclusions(data);
|
||||
} catch (e) {
|
||||
console.log(` ⚠️ 讀取排除問題失敗: ${e.message},視為空`);
|
||||
exclusions = [];
|
||||
}
|
||||
console.log(` 讀取排除問題: ${exclusions.length} 筆`);
|
||||
return exclusions;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { describe, it, beforeEach, afterEach } from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { loadExclusions, applyExclusions } from './findings.js';
|
||||
import { EXCLUSIONS_PATH } from './config.js';
|
||||
|
||||
describe('findings exclusions', () => {
|
||||
let workspace;
|
||||
|
||||
beforeEach(() => {
|
||||
workspace = fs.mkdtempSync(path.join(os.tmpdir(), 'findings-test-'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
fs.rmSync(workspace, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('loads excluded_findings wrapper format', () => {
|
||||
const fullPath = path.join(workspace, EXCLUSIONS_PATH);
|
||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||
fs.writeFileSync(fullPath, JSON.stringify({
|
||||
excluded_findings: [
|
||||
{ location: 'entrypoint.sh:180', title: 'fetch_package_versions jq overhead' },
|
||||
],
|
||||
}, null, 2));
|
||||
|
||||
const exclusions = loadExclusions(workspace);
|
||||
|
||||
assert.equal(exclusions.length, 1);
|
||||
assert.equal(exclusions[0].location, 'entrypoint.sh:180');
|
||||
assert.equal(exclusions[0].title, 'fetch_package_versions jq overhead');
|
||||
});
|
||||
|
||||
it('applies exclusions loaded from wrapper format', () => {
|
||||
const findings = [
|
||||
{ location: 'entrypoint.sh:180', role: 'Maya', suggestion: 'keep' },
|
||||
{ location: 'README.md:12', role: 'Maya', suggestion: 'keep' },
|
||||
];
|
||||
const exclusions = [
|
||||
{ location: 'entrypoint.sh:180', title: 'fetch_package_versions jq overhead' },
|
||||
];
|
||||
|
||||
const filtered = applyExclusions(findings, exclusions);
|
||||
|
||||
assert.equal(filtered.length, 1);
|
||||
assert.equal(filtered[0].location, 'README.md:12');
|
||||
});
|
||||
});
|
||||
@@ -109,8 +109,12 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync,
|
||||
|
||||
const out = run(['commit', '-m', 'chore: update ai-review findings [skip ci]'], repoDir);
|
||||
const commitHash = out.match(/\[.+ ([a-f0-9]+)\]/)?.[1] || 'unknown';
|
||||
try {
|
||||
run(['push', remoteUrl, PR_HEAD_BRANCH], repoDir, credEnv);
|
||||
console.log(` ✅ persisted findings commit=${commitHash} push=${PR_HEAD_BRANCH}`);
|
||||
} catch (pushErr) {
|
||||
console.log(` ⚠️ Step7 commit 成功但 push 失敗: commit=${commitHash} push=${PR_HEAD_BRANCH} error=${pushErr.message}`);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(` ⚠️ Runner failed: commit/push 失敗: ${e.message}`);
|
||||
|
||||
@@ -150,6 +150,32 @@ describe('commitAndPush', () => {
|
||||
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
||||
await assert.doesNotReject(() => commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn, sourceRoot));
|
||||
});
|
||||
|
||||
it('logs push failures separately from commit failures', async () => {
|
||||
const repoDir = path.join(workspace, 'repo');
|
||||
fs.mkdirSync(path.join(workspace, '.gitea/ai-review'), { recursive: true });
|
||||
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/findings.json'), '[]\n');
|
||||
fs.writeFileSync(path.join(workspace, '.gitea/ai-review/exclusions.json'), '[]\n');
|
||||
fs.mkdirSync(path.join(repoDir, '.gitea/ai-review'), { recursive: true });
|
||||
fs.writeFileSync(path.join(repoDir, '.gitea/ai-review/findings.json'), '[]\n');
|
||||
fs.writeFileSync(path.join(repoDir, '.gitea/ai-review/exclusions.json'), '[]\n');
|
||||
|
||||
const spawn = makeSpawn({
|
||||
push: () => ({ status: 1, stdout: '', stderr: 'remote: error: pre-receive hook declined', error: null }),
|
||||
});
|
||||
const logs = [];
|
||||
const originalLog = console.log;
|
||||
console.log = (...args) => { logs.push(args.join(' ')); };
|
||||
|
||||
try {
|
||||
await commitAndPush(workspace, repoDir, spawn, sourceRoot);
|
||||
} finally {
|
||||
console.log = originalLog;
|
||||
}
|
||||
|
||||
assert.ok(logs.some(line => line.includes('Step7 commit 成功但 push 失敗')));
|
||||
assert.ok(logs.some(line => line.includes('pre-receive hook declined')));
|
||||
});
|
||||
});
|
||||
|
||||
describe('cloneRepo', () => {
|
||||
|
||||
Reference in New Issue
Block a user