簡介
這是一個 AI Code Review Action。Gitea Workflow 可以使用此 Action 讓 AI 助理根據不同面向分析 Pull Request 中變更的內容後,將問題分級 Comment 到 Pull Request 中。
流程(Pull Request opened / synchronize 觸發;若偵測到 AI 助理的自動提交則直接跳過)
- Pipeline 啟動:輸出 repo、PR 編號、來源分支與目標分支等基本資訊
- 前置驗證(做任何分析或發 comment 前):檢查所有驗證相關設定是否都可用,全部通過才繼續;任何一項失敗就印出明確訊息並立即
exit 1- 必要環境變數齊全:
GITEA_TOKEN、GITEA_REPOSITORY、PR_NUMBER(缺一即失敗) - Gitea API 可連線且
GITEA_TOKEN有權限讀取此 repo(呼叫GET /api/v1/repos/{repo}驗證 token 與 repo 同時有效) - 若有提供
GITEA_COMMENT_TOKEN,額外用它驗證可用(呼叫GET /api/v1/user),確保後續發 comment 不會因 token 失效而中斷 - git push 認證可用:用與第 8 點 commit/push 完全相同的 askpass + remote URL 機制跑一次唯讀的
git ls-remote,提前抓出 askpass 無法執行或 HTTP 認證失敗(例如could not read Username)的問題。此路徑與上面的 REST API 不同,API token 有效不代表 git push 一定能用,故獨立驗證 - 已選定一個 LLM provider,且其 API Key 至少有一把通過驗證:實際送出一個最小請求確認認證可用;逗號分隔的多把 Key 只要一把成功即可,逐把記錄成敗;Ollama 無 Key,改為檢查
OLLAMA_BASE_URL可連線 2.5. PR 對話收斂(前置驗證通過、且非 AI 助理自動提交後):讀取 PR 上所有行內 review comment,依「檔案路徑+行號」收斂成對話,跳過已 resolve 的對話;對所有未解決的對話一律呼叫 Gitea 官方 API(POST /repos/{repo}/pulls/comments/{id}/resolve)關閉(findings.json為唯一待辦來源,下次 review 會依其重新貼 comment)。接著取每個對話所在檔案在 PR head 的最新內容,請 AI 將每個對話判為三類,決定其在問題清單的去向:(a)resolved(程式碼已修復)→ Step4 從問題清單移除;(b)false_positive(誤報)→ 寫入exclusions.json並從問題清單移除;(c)open(仍成立)→ Step4 加入問題清單。納入判斷的對話包含所有人的留言;任一外部呼叫失敗都降級為「視為 open」,不中斷整體流程
- 必要環境變數齊全:
- 檢查是否為 AI 助理自動提交;若不是,選定 LLM provider/model、載入角色、取得 PR diff,將服務名稱、模型名稱與角色資訊 Comment 到 Pull Request,並讓每個角色個別分析 Git Diff 產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議)
- 讀取來源分支中的所有未解決舊問題(問題檔案
.gitea/ai-review/findings.json),先套用步驟 2.5 的對話收斂結果(移除已修復與誤報對應的問題、加回仍成立但已遺漏的問題;以「檔案路徑+建議內容」比對,避免行號漂移誤判),再加上新問題後,去除重複產生本次 PR 的問題表格(PR問題表格)覆蓋問題檔案 - 讀取來源分支中的排除問題檔案(
.gitea/ai-review/exclusions.json),用來過濾 PR 問題表格中不需要處理的問題;接著由「防守方」角色(Paladin)對剩餘問題逐條判斷是否為誤報——每條問題各派一個 sub-agent,多條問題時平行處理,判為誤報者剔除、成立者保留(任一裁決失敗則保守保留該問題) - 將 PR 問題表格寫入
.gitea/ai-review/findings.json,並發布一個 Gitea Review:Review 本文先以「嚴重/警告/建議/無法標示」四欄分列新問題與舊問題兩列的數量(無法標示=等級無法歸入前三類者),接著附上「AI 助理使用量」區塊(本次審查累計的 token 消耗,以及目前的帳號額度);之後只將「新問題」中可找出檔案與行數者依照嚴重等級排序後加入 Review Comments 內(舊問題只計入上方統計,不再重複標註檔案與行數),每個 Comment 包含嚴重等級/審查員/問題/建議,其中「問題」是審查員判斷該處有問題的原因,不是檔案路徑或行號 - 驗證來源分支中的
findings.json與exclusions.json是否為合法 JSON array;格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;檔案不存在則建立並寫入[] - Commit 問題檔案,只將 workspace 中實際存在的
.gitea/ai-review/findings.json與.gitea/ai-review/exclusions.json覆蓋到記憶區;workspace 沒有的問題檔就略過。自動提交的 commit message 會帶上[ai-review-bot],供 workflow 判斷是否要跳過重跑 - 如果 PR 問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)
設計
- Gitea 相關參數中,
GITEA_TOKEN必須由 inputs 明確提供;GITEA_SERVER_URL、GITEA_REPOSITORY、PR_NUMBER、PR_HEAD_BRANCH、PR_BASE_BRANCH等欄位若 inputs 沒有定義,則從${{ gitea.* }}取得 - BASE_URL 如果 inputs 沒有定義,則使用預設值
- Comment 加上些許 emoji 讓資訊有點活力
- 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行
- 將提示詞放到 ./app/prompts 內供程式讀取
- API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1
- 讀取 Git Diff 時排除
.gitea/、.github/資料夾,以及TODO.md、README.md,避免 AI 分析 workflow 設定與文件等非業務程式碼 - 階段七驗證來源分支中的
findings.json與exclusions.json是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入[] - 傳給 AI 的 findings 只保留必要欄位(level、role、location、problem、suggestion),排除
is_new等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量 - 執行時會額外記錄來源分支狀態、
findings.json/exclusions.json的檔案路徑、大小、mtime 與 raw/normalized 筆數,方便追查讀檔與分支內容不一致的問題 - action 一啟動就先做「前置驗證」(流程第 2 點):集中檢查 Gitea REST API token、comment token、git push 認證與 LLM 的所有驗證相關設定是否可用,全部通過才往下跑。驗證邏輯獨立成
app/preflight.js(git push 驗證委派給app/git.js的verifyRemoteAccess),由main.js在 Step1 之後、其餘步驟之前呼叫;任何一項失敗都印出是哪一項、原因為何後exit 1,避免在分析到一半、發 comment 或最後 push 時才因 token / key / 認證無效而中斷 - PR 對話收斂(流程第 2.5 點)邏輯獨立成
app/resolve.js,由main.js在前置驗證與自動提交檢查之後以Step2呼叫:- 透過
app/gitea.js的listAllReviewComments取得所有行內 review comment,groupConversations以「path+line」收斂並偵測resolver(已解決);reconcileConversations取 PR head 最新檔案內容(getFileContentAtRef,contents API base64 解碼)取目標行附近視窗,交judgeConversations由 AI 批次判為resolved/false_positive/open reconcileConversations先把每一個未解決的 comment(有 id 且無resolver,依 comment id 去重;不依賴path|line分組,故含無 path/position 變 null 者)一律呼叫resolvePullReviewComment(POST /pulls/comments/{id}/resolve)關閉,確保每個獨立 thread 都關到;再依 AI 判斷把對應 finding 分流:resolved→resolvedFindings(供移除)、false_positive→excludedFindings(供寫入 exclusions 並移除)、open→carriedFindings(加回舊問題)- 與既有 findings 流程的銜接:
main.js在 Step4 以dropResolvedFindings移除(已修復+誤報)、addCarriedFindings加回仍成立者(以「檔案路徑+正規化建議內容」為簽章比對,對行號漂移與標點差異穩定);Step5 以findings.js的appendExclusions把誤報寫入exclusions.json(workspace 與 cloned repo 各一份,供本次過濾與後續 commit) - 為降低 token 用量只送目標行附近視窗;任一外部呼叫失敗都降級為「視為 open」並繼續流程
- 透過
- AI 助理使用量統計獨立成
app/usage.js,於Step6發布 Review 前蒐集,同時寫入 action log 與 Review 本文,核心是呈現「剩餘可用百分比」:- 本次 token 消耗:每次 LLM 呼叫都經由
app/llm.js的chat集中以recordUsage累計;extractUsage容錯解析各平台回應的 usage 欄位(OpenAI 相容usage、OpenAI Responsesinput/output_tokens、GeminiusageMetadata、Ollamaeval_count、OpenCodetokens) - 剩餘可用百分比(
resolveRemainingPercent)依優先序擇一:(1) 帳號額度有上限時用「剩餘 credits ÷ 上限」;(2) 否則用回應 header 的速率配額「當前視窗剩餘 ÷ 上限」。recordRateLimit從回應 header 擷取x-ratelimit-*-tokens(OpenAI 相容)或anthropic-ratelimit-tokens-*(Claude),缺 token 維度時退用 requests 維度——此來源零額外憑證、零 CLI,直接取自既有呼叫的回應 - 帳號額度:
fetchAccountQuota依平台採不同策略——OpenRouter(openaislot 指向 openrouter.ai 時)以GET /auth/key取得 USD credits 已用/上限/剩餘;Ollama、OpenCode 為本地/自架服務回報「不適用」;OpenAI、Claude、Gemini、Amazon Q 的帳號額度需 org/admin 權限,API key 無法取得時誠實回報原因 - 兩種來源皆無法取得(例如帳號無上限且回應無速率 header)時,降級為「無法計算百分比」並附原因,不中斷流程;
formatUsageStats產生 Review 本文區塊,formatUsageStatsLine產生單行 log 摘要
- 本次 token 消耗:每次 LLM 呼叫都經由
- 誤報判斷套用「防守方」角色:
app/findings.js的filterFalsePositivesWithAI以app/roles.js的loadRole('Paladin')載入防守方角色,並用buildVerdictPrompt(role, exclusionHint)組出帶其個性與裁決準則的 system prompt;對每一條 finding 各派一個防守方 sub-agent(judgeFindingIsFalsePositive)裁決confirmed/false_positive,多條問題時以Promise.all平行處理;判為誤報者剔除、成立者保留,任一 sub-agent 失敗(含解析失敗)保守視為成立保留,不中斷流程。角色檔遺失時buildVerdictPrompt(null)退回通用裁判 prompt。
使用說明
- 在 Gitea 專案中建立
.gitea/workflows資料夾 - 在
.gitea/workflows資料夾中建立ai-review.yaml - 在
ai-review.yaml中填入以下內容(選擇一個使用):
自動提交排除說明:此 Action 會將自己的 commit message 標記為
[ai-review-bot][success]或[ai-review-bot][failure],而且 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)三項權限,為正常運作所必要,無法縮減。若你想讓 comment 用不同權限的 token,可額外傳GITEA_COMMENT_TOKEN,其餘 Gitea 操作仍使用GITEA_TOKEN。
1. OpenAI
name: AI
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
code-review:
name: Code Review
runs-on: ubuntu
steps:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }}
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} # 支援逗號分隔多個 Key
OPENAI_BASE_URL: https://api.openai.com/v1
OPENAI_MODEL: ${{ vars.OPENAI_MODEL }}
permissions:
contents: write
pull-requests: write
issues: write
OpenAI GPT-5.5 會透過 Responses API 呼叫;設定方式仍使用 OPENAI_*:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
OPENAI_BASE_URL: https://api.openai.com/v1
OPENAI_MODEL: gpt-5.5
2. OpenRouter
name: AI
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
code-review:
name: Code Review
runs-on: ubuntu
steps:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_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 }}
permissions:
contents: write
pull-requests: write
issues: write
3. Anthropic Claude
name: AI
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
code-review:
name: Code Review
runs-on: ubuntu
steps:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }}
CLAUDE_API_KEY: ${{ secrets.CLAUDE_API_KEY }} # 支援逗號分隔多個 Key
CLAUDE_BASE_URL: https://api.anthropic.com/v1
permissions:
contents: write
pull-requests: write
issues: write
4. Google Gemini
name: AI
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
code-review:
name: Code Review
runs-on: ubuntu
steps:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_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 }}
permissions:
contents: write
pull-requests: write
issues: write
5. Amazon Q
name: AI
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
code-review:
name: Code Review
runs-on: ubuntu
steps:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }}
AMAZONQ_API_KEY: ${{ secrets.AMAZONQ_API_KEY }} # 支援逗號分隔多個 Key
AMAZONQ_BASE_URL: https://q.api.aws
permissions:
contents: write
pull-requests: write
issues: write
6. OpenCode Server
name: AI
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
code-review:
name: Code Review
runs-on: ubuntu
steps:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }}
OPENCODE_BASE_URL: http://192.168.3.124:4096
OPENCODE_PROVIDER: google
OPENCODE_MODEL: gemini-2.5-flash
# 預設會跳過 OpenCode TLS 驗證;若要強制驗證憑證才需要設定:
# OPENCODE_SKIP_TLS_VERIFY: false
# 若 OpenCode server 有設定 OPENCODE_SERVER_PASSWORD,才需要提供:
# OPENCODE_SERVER_USERNAME: opencode
# OPENCODE_SERVER_PASSWORD: ${{ secrets.OPENCODE_SERVER_PASSWORD }}
permissions:
contents: write
pull-requests: write
issues: write
OpenCode Server 串接方式會呼叫 server root 的 /session 與 /session/{sessionID}/message,並把模型指定為 providerID=google、modelID=gemini-2.5-flash。可用的內部 OpenCode server:
OPENCODE_BASE_URL: https://opencode.jsc.idv.me
或:
OPENCODE_BASE_URL: http://192.168.3.124:4096
OpenCode server 本身必須已設定好 google provider 與 gemini-2.5-flash model;此 action 不會把 Google API key 傳給 OpenCode server。
7. Ollama
name: AI
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref }}
cancel-in-progress: true
on:
pull_request:
branches-ignore:
- master
types: [opened, synchronize]
jobs:
code-review:
name: Code Review
runs-on: ubuntu
steps:
- name: AI Code Review
uses: https://gitea.jsc.idv.tw/actions/code-review@${{ vars.ACTION_CODE_REVIEW_VERSION }}
with:
GITEA_TOKEN: ${{ secrets.RUNNER_TOKEN }}
GITEA_COMMENT_TOKEN: ${{ secrets.GITEA_TOKEN }}
OLLAMA_BASE_URL: https://ollama.jsc.idv.me/v1
OLLAMA_MODEL: ${{ vars.OLLAMA_MODEL }}
permissions:
contents: write
pull-requests: write
issues: write