feat: 導入 AI 程式碼審查 action 並修正進入點與參數接線 #1

Merged
admin merged 30 commits from ai-review-resolve/develop-20260702-160700 into develop 2026-07-03 10:04:33 +00:00
2 changed files with 56 additions and 11 deletions
Showing only changes of commit 47aa199e5e - Show all commits
+13
View File
@@ -0,0 +1,13 @@
# AI Code Review 忽略清單
# 符合下列前綴/路徑的檔案不會納入送給 LLM 的 git diff。
# 規則:每行一個路徑前綴(相對 repo 根),# 開頭為註解,空行略過。
# 註:任何深度的 node_modules/ 一律排除(程式內建保險),此處列出僅為明示。
.gitea/
.github/
README.md
TODO.md
package-lock.json
src/package-lock.json
dist/
node_modules/
+42 -10
View File
1
@@ -41,15 +41,9 @@ export function getBotReviewOutcome(message) {
return match?.[1]?.toLowerCase() || 'unknown'; return match?.[1]?.toLowerCase() || 'unknown';
} }
/** // 找不到 .reviewignore 時(例如其他 repo 未提供)採用的內建預設排除清單。
* 取得目前 PR 的完整 Git diff,並排除 CI/文件等不需審查的路徑(.gitea/、.github/、README.md、TODO.md // 任何深度的 node_modules/ 另由 filterDiff 內建強制排除,不倚賴此清單
* 透過 Gitea `GET /repos/{repo}/pulls/{index}.diff`(純文字 diff),授權使用 GITEA_TOKEN。 export const DEFAULT_REVIEW_IGNORE = [
* @returns {Promise<string>} 過濾後的 diff 文字。
* @throws {Error} 當 Gitea API 請求失敗(網路錯誤、逾時或非 2xx 狀態)時拋出 axios 例外。
*/
export async function getPRDiff() {
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent });
return filterDiff(resp.data, [
'.gitea/', '.gitea/',
'.github/', '.github/',
'README.md', 'README.md',
@@ -57,7 +51,45 @@ export async function getPRDiff() {
'package-lock.json', 'package-lock.json',
'src/package-lock.json', 'src/package-lock.json',
'dist/', 'dist/',
]); ];
/**
* 解析 .reviewignore 文字為排除前綴陣列(gitignore 風格)。
* 規則:每行一個路徑前綴,trim 後略過空行與 `#` 開頭的註解行。
* @param {string} text - .reviewignore 檔案內容。
* @returns {string[]} 排除前綴清單。
*/
export function parseReviewIgnore(text) {
return String(text || '')
.split('\n')
.map(l => l.trim())
.filter(l => l && !l.startsWith('#'));
admin marked this conversation as resolved
Review

嚴重等級🔴 嚴重
審查員:Assassin
問題:這裡直接從 PR head 讀取 .reviewignore,再拿它當成排除規則。攻擊者可以在自己的分支塞入排除條目,讓 bot 故意跳過包含惡意變更的檔案或整個目錄,等於自己決定哪些地方不被審查。
建議:不要信任 PR head 裡的 .reviewignore 來決定安全掃描範圍;改從受保護的 base branch 或 maintainer 管控的位置讀取,且要與固定的預設排除清單合併,而不是讓它覆蓋預設規則。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:這裡直接從 PR head 讀取 `.reviewignore`,再拿它當成排除規則。攻擊者可以在自己的分支塞入排除條目,讓 bot 故意跳過包含惡意變更的檔案或整個目錄,等於自己決定哪些地方不被審查。 **建議**:不要信任 PR head 裡的 `.reviewignore` 來決定安全掃描範圍;改從受保護的 base branch 或 maintainer 管控的位置讀取,且要與固定的預設排除清單合併,而不是讓它覆蓋預設規則。
}
/**
* 從被審 PR 的 head ref 取得 `.reviewignore` 並解析為排除清單。
* 檔案不存在或為空時退回 {@link DEFAULT_REVIEW_IGNORE}。
admin marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Assassin
問題.reviewignore 是從被審查的 PR head 直接讀回來的,提交者自己就能在同一個 PR 裡新增排除規則,把惡意檔案或關鍵目錄整批從 diff 中消失。攻擊者只要加幾條前綴,就能讓這個審查流程根本看不到真正危險的變更。
建議:不要信任 PR 內容裡的 .reviewignore;改從受保護的 base branch、獨立設定檔或固定白名單載入,並禁止同一次 PR 修改忽略規則時自動生效,改為人工覆核。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:`.reviewignore` 是從被審查的 PR head 直接讀回來的,提交者自己就能在同一個 PR 裡新增排除規則,把惡意檔案或關鍵目錄整批從 diff 中消失。攻擊者只要加幾條前綴,就能讓這個審查流程根本看不到真正危險的變更。 **建議**:不要信任 PR 內容裡的 `.reviewignore`;改從受保護的 base branch、獨立設定檔或固定白名單載入,並禁止同一次 PR 修改忽略規則時自動生效,改為人工覆核。
* @returns {Promise<string[]>} 套用於 diff 過濾的排除前綴清單。
*/
export async function getReviewIgnore() {
const patterns = parseReviewIgnore(await getFileContentAtRef('.reviewignore'));
if (patterns.length > 0) {
line(`已套用 .reviewignore${patterns.length} 條排除規則`);
return patterns;
}
return DEFAULT_REVIEW_IGNORE;
}
/**
* 取得目前 PR 的完整 Git diff,並依 `.reviewignore`(讀不到時用內建預設)排除不需審查的路徑。
* 透過 Gitea `GET /repos/{repo}/pulls/{index}.diff`(純文字 diff),授權使用 GITEA_TOKEN。
* @returns {Promise<string>} 過濾後的 diff 文字。
admin marked this conversation as resolved
Review

嚴重等級🔴 嚴重
審查員:Mage
問題:這裡只要 commitbranch 訊息包含 [ai-review-bot] 就回傳 true,但沒有區分 [success][failure];最小重現:上一輪 bot commit 是 [ai-review-bot][failure],而 getCommitMessageBySha 讀取失敗時,流程會被當成「可跳過」直接結束,原本應該讓 workflow 失敗的訊號被吃掉。
建議:讓這個函式回傳結構化結果,例如 success / failure / unknown,或至少在偵測到 [failure] 時明確回傳失敗狀態,交由 main() 先處理失敗再決定是否跳過。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:這裡只要 `commit` 或 `branch` 訊息包含 `[ai-review-bot]` 就回傳 true,但沒有區分 `[success]` 與 `[failure]`;最小重現:上一輪 bot commit 是 `[ai-review-bot][failure]`,而 `getCommitMessageBySha` 讀取失敗時,流程會被當成「可跳過」直接結束,原本應該讓 workflow 失敗的訊號被吃掉。 **建議**:讓這個函式回傳結構化結果,例如 `success / failure / unknown`,或至少在偵測到 `[failure]` 時明確回傳失敗狀態,交由 `main()` 先處理失敗再決定是否跳過。
* @throws {Error} 當 Gitea 取 diff 的 API 請求失敗(網路錯誤、逾時或非 2xx 狀態)時拋出 axios 例外。
*/
export async function getPRDiff() {
const patterns = await getReviewIgnore();
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent });
return filterDiff(resp.data, patterns);
} }
/** /**
admin marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:shouldSkipBotCommit 目前只看到命中 bot marker 的測試,缺少「commit API 失敗、分支查詢失敗、sha/branch 都沒有 marker」時應回 false 的失敗與保守路徑驗證。這是避免 workflow 誤跳過審查的關鍵判斷,不能只測快樂路徑。
建議:新增測試讓 getCommitMessageBySha / getBranchHeadCommitMessage 對應的 axios 呼叫拋錯或回一般 commit message,斷言 shouldSkipBotCommit 回 false,且不會把查詢失敗誤判成 bot commit。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:shouldSkipBotCommit 目前只看到命中 bot marker 的測試,缺少「commit API 失敗、分支查詢失敗、sha/branch 都沒有 marker」時應回 false 的失敗與保守路徑驗證。這是避免 workflow 誤跳過審查的關鍵判斷,不能只測快樂路徑。 **建議**:新增測試讓 getCommitMessageBySha / getBranchHeadCommitMessage 對應的 axios 呼叫拋錯或回一般 commit message,斷言 shouldSkipBotCommit 回 false,且不會把查詢失敗誤判成 bot commit。
2