Files
code-review/README.md
T

18 KiB
Raw Blame History

簡介

這是一個 AI Code Review Action。Gitea Workflow 可以使用此 Action 讓 AI 助理根據不同面向分析 Pull Request 中變更的內容後,將問題分級 Comment 到 Pull Request 中。

流程(Pull Request opened / synchronize 觸發;若偵測到 AI 助理的自動提交則直接跳過)

  1. Pipeline 啟動:輸出 repo、PR 編號、來源分支與目標分支等基本資訊
  2. 前置驗證(做任何分析或發 comment 前):檢查所有驗證相關設定是否都可用,全部通過才繼續;任何一項失敗就印出明確訊息並立即 exit 1
    • 必要環境變數齊全:GITEA_TOKENGITEA_REPOSITORYPR_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 官方 APIPOST /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」,不中斷整體流程
  3. 檢查是否為 AI 助理自動提交;若不是,選定 LLM provider/model、載入角色、取得 PR diff,將服務名稱、模型名稱與角色資訊 Comment 到 Pull Request,並讓每個角色個別分析 Git Diff 產生新問題表格(問題等級、角色名稱、問題位置或行數、修改建議)
  4. 讀取來源分支中的所有未解決舊問題(問題檔案 .gitea/ai-review/findings.json),先套用步驟 2.5 的對話收斂結果(移除已修復與誤報對應的問題、加回仍成立但已遺漏的問題;以「檔案路徑+建議內容」比對,避免行號漂移誤判),再加上新問題後,去除重複產生本次 PR 的問題表格(PR問題表格)覆蓋問題檔案
  5. 讀取來源分支中的排除問題檔案(.gitea/ai-review/exclusions.json),用來過濾 PR 問題表格中不需要處理的問題;接著由「防守方」角色(Paladin)對剩餘問題逐條判斷是否為誤報——每條問題各派一個 sub-agent,多條問題時平行處理,判為誤報者剔除、成立者保留(任一裁決失敗則保守保留該問題)
  6. 將 PR 問題表格寫入 .gitea/ai-review/findings.json,並發布一個 Gitea Review:Review 本文先以「嚴重/警告/建議/無法標示」四欄分列新問題與舊問題兩列的數量(無法標示=等級無法歸入前三類者),接著附上「AI 助理使用量」區塊(本次審查累計的 token 消耗,以及目前的帳號額度);之後只將「新問題」中可找出檔案與行數者依照嚴重等級排序後加入 Review Comments 內(舊問題只計入上方統計,不再重複標註檔案與行數),每個 Comment 包含嚴重等級/審查員/問題/建議,其中「問題」是審查員判斷該處有問題的原因,不是檔案路徑或行號
  7. 驗證來源分支中的 findings.jsonexclusions.json 是否為合法 JSON array;格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;檔案不存在則建立並寫入 []
  8. Commit 問題檔案,只將 workspace 中實際存在的 .gitea/ai-review/findings.json.gitea/ai-review/exclusions.json 覆蓋到記憶區;workspace 沒有的問題檔就略過。自動提交的 commit message 會帶上 [ai-review-bot],供 workflow 判斷是否要跳過重跑
  9. 如果 PR 問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)

設計

  1. Gitea 相關參數中,GITEA_TOKEN 必須由 inputs 明確提供;GITEA_SERVER_URLGITEA_REPOSITORYPR_NUMBERPR_HEAD_BRANCHPR_BASE_BRANCH 等欄位若 inputs 沒有定義,則從 ${{ gitea.* }} 取得
  2. BASE_URL 如果 inputs 沒有定義,則使用預設值
  3. Comment 加上些許 emoji 讓資訊有點活力
  4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行
  5. 將提示詞放到 ./app/prompts 內供程式讀取
  6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1
  7. 讀取 Git Diff 時排除 .gitea/.github/ 資料夾,以及 TODO.mdREADME.md,避免 AI 分析 workflow 設定與文件等非業務程式碼
  8. 階段七驗證來源分支中的 findings.jsonexclusions.json 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 []
  9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、problem、suggestion),排除 is_new 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量
  10. 執行時會額外記錄來源分支狀態、findings.json / exclusions.json 的檔案路徑、大小、mtime 與 raw/normalized 筆數,方便追查讀檔與分支內容不一致的問題
  11. action 一啟動就先做「前置驗證」(流程第 2 點):集中檢查 Gitea REST API token、comment token、git push 認證與 LLM 的所有驗證相關設定是否可用,全部通過才往下跑。驗證邏輯獨立成 app/preflight.jsgit push 驗證委派給 app/git.jsverifyRemoteAccess),由 main.js 在 Step1 之後、其餘步驟之前呼叫;任何一項失敗都印出是哪一項、原因為何後 exit 1,避免在分析到一半、發 comment 或最後 push 時才因 token / key / 認證無效而中斷
  12. PR 對話收斂(流程第 2.5 點)邏輯獨立成 app/resolve.js,由 main.js 在前置驗證與自動提交檢查之後以 Step2 呼叫:
    • 透過 app/gitea.jslistAllReviewComments 取得所有行內 review commentgroupConversations 以「path+line」收斂並偵測 resolver(已解決);reconcileConversations 取 PR head 最新檔案內容(getFileContentAtRefcontents API base64 解碼)取目標行附近視窗,交 judgeConversations 由 AI 批次判為 resolved / false_positive / open
    • reconcileConversations 對所有未解決對話一律呼叫 resolvePullReviewCommentPOST /pulls/comments/{id}/resolve)關閉,再依 AI 判斷把對應 finding 分流:resolvedresolvedFindings(供移除)、false_positiveexcludedFindings(供寫入 exclusions 並移除)、opencarriedFindings(加回舊問題)
    • 與既有 findings 流程的銜接:main.js 在 Step4 以 dropResolvedFindings 移除(已修復+誤報)、addCarriedFindings 加回仍成立者(以「檔案路徑+正規化建議內容」為簽章比對,對行號漂移與標點差異穩定);Step5 以 findings.jsappendExclusions 把誤報寫入 exclusions.jsonworkspace 與 cloned repo 各一份,供本次過濾與後續 commit)
    • 為降低 token 用量只送目標行附近視窗;任一外部呼叫失敗都降級為「視為 open」並繼續流程
  13. AI 助理使用量統計獨立成 app/usage.js,於 Step6 發布 Review 前蒐集,同時寫入 action log 與 Review 本文,核心是呈現「剩餘可用百分比」:
    • 本次 token 消耗:每次 LLM 呼叫都經由 app/llm.jschat 集中以 recordUsage 累計;extractUsage 容錯解析各平台回應的 usage 欄位(OpenAI 相容 usage、OpenAI Responses input/output_tokens、Gemini usageMetadata、Ollama eval_count、OpenCode tokens
    • 剩餘可用百分比(resolveRemainingPercent)依優先序擇一:(1) 帳號額度有上限時用「剩餘 credits ÷ 上限」;(2) 否則用回應 header 的速率配額「當前視窗剩餘 ÷ 上限」。recordRateLimit 從回應 header 擷取 x-ratelimit-*-tokensOpenAI 相容)或 anthropic-ratelimit-tokens-*Claude),缺 token 維度時退用 requests 維度——此來源零額外憑證、零 CLI,直接取自既有呼叫的回應
    • 帳號額度:fetchAccountQuota 依平台採不同策略——OpenRouter(openai slot 指向 openrouter.ai 時)以 GET /auth/key 取得 USD credits 已用/上限/剩餘;Ollama、OpenCode 為本地/自架服務回報「不適用」;OpenAI、Claude、Gemini、Amazon Q 的帳號額度需 org/admin 權限,API key 無法取得時誠實回報原因
    • 兩種來源皆無法取得(例如帳號無上限且回應無速率 header)時,降級為「無法計算百分比」並附原因,不中斷流程;formatUsageStats 產生 Review 本文區塊,formatUsageStatsLine 產生單行 log 摘要
  14. 誤報判斷套用「防守方」角色:app/findings.jsfilterFalsePositivesWithAIapp/roles.jsloadRole('Paladin') 載入防守方角色,並用 buildVerdictPrompt(role, exclusionHint) 組出帶其個性與裁決準則的 system prompt;對每一條 finding 各派一個防守方 sub-agentjudgeFindingIsFalsePositive)裁決 confirmedfalse_positive,多條問題時以 Promise.all 平行處理;判為誤報者剔除、成立者保留,任一 sub-agent 失敗(含解析失敗)保守視為成立保留,不中斷流程。角色檔遺失時 buildVerdictPrompt(null) 退回通用裁判 prompt。

使用說明

  1. 在 Gitea 專案中建立 .gitea/workflows 資料夾
  2. .gitea/workflows 資料夾中建立 ai-review.yaml
  3. 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=googlemodelID=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