From 268cd052113dcfec9738670225b2b86551c639d8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Sat, 11 Jul 2026 11:56:18 +0000 Subject: [PATCH] =?UTF-8?q?docs(AI=20Code=20Review):=20=E8=A3=9C=E9=BD=8A?= =?UTF-8?q?=20action=20=E8=88=87=20workflow=20=E8=A8=BB=E8=A7=A3=E8=AA=AA?= =?UTF-8?q?=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yaml | 103 ++++++++++++++++++++++++++++------- .gitea/workflows/master.yaml | 53 +++++++++++++----- .gitea/workflows/readme.md | 56 ++++++++++++++++--- action.yml | 18 ++++++ src/config.js | 18 ++++++ src/findings.js | 6 ++ src/llm.js | 29 +++++++++- 7 files changed, 241 insertions(+), 42 deletions(-) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index cdc7ae1..eeca1a3 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,49 +1,112 @@ +# 用途:CI workflow 的 command-file 草稿,保留原始流程並補上逐行說明。 +# 更新時間:2026/07/11 19:00:45 +# workflow 名稱,對應 Gitea UI 中的顯示標題。 name: CI +# 定義此 workflow 的觸發事件。 on: + # 在 pull request 事件時執行。 pull_request: + # 只在建立與同步更新 PR 時觸發。 types: [opened, synchronize] +# workflow 內的工作列表。 jobs: + # 第一個 job:負責版本計算與 release 發佈。 build: + # job 顯示名稱,用來區分執行階段。 name: 1. BUILD + # 使用 Ubuntu runner 執行。 runs-on: ubuntu + # job 層級環境變數。 env: - VERSION: "0.0.0-beta.${{ gitea.run_number }}" + # 目標分支為 develop 時視為 beta。 IS_BETA: ${{ gitea.base_ref == 'develop' }} + # 對外輸出供後續 job 使用。 outputs: + # 輸出版本字串。 version: ${{ env.VERSION }} + # 輸出是否為 beta。 is_beta: ${{ env.IS_BETA }} + # build job 的執行步驟。 steps: - - name: Publishing Release - uses: akkuman/gitea-release-action@${{ vars.ACTION_GITEA_RELEASE_VERSION }} - with: - name: "${{ gitea.event.repository.name }} v${{ env.VERSION }}" - tag_name: "v${{ env.VERSION }}" - target_commitish: ${{ gitea.sha }} - prerelease: ${{ env.IS_BETA }} + # 先依 repo 狀態計算版本號。 + - name: Calculate Version + # 供後續步驟讀取輸出用的 step id。 + id: calculate-version + # 使用版本計算 action。 + uses: https://gitea.jsc.idv.tw/actions/calculate-version@${{ vars.ACTION_CALCULATE_VERSION }} + # 傳入 action 參數。 + with: + # 告知 action 是否為 beta 分支情境。 + is_beta: ${{ env.IS_BETA }} + # 依計算出的版本建立 release。 + - name: Publishing Release + # 使用 release action。 + uses: akkuman/gitea-release-action@${{ vars.ACTION_GITEA_RELEASE_VERSION }} + # 這個 step 的環境變數。 + env: + # 取前一步算出的版本號。 + VERSION: ${{ steps.calculate-version.outputs.version }} + # release action 的參數。 + with: + # release 名稱。 + name: "${{ gitea.event.repository.name }} v${{ env.VERSION }}" + # release tag 名稱。 + tag_name: "v${{ env.VERSION }}" + # 指向目前提交。 + target_commitish: ${{ gitea.sha }} + # beta 情境時標記為 prerelease。 + prerelease: ${{ env.IS_BETA }} + # 第二個 job:在 beta 情境執行 AI Code Review。 test: + # job 顯示名稱。 name: 2. TEST + # 使用 Ubuntu runner。 runs-on: ubuntu + # 必須等 build job 完成。 needs: [build] + # 只有 beta 情境才執行。 if: ${{ needs.build.outputs.is_beta == 'true' }} + # job 層級環境變數。 env: + # 使用 build job 輸出的版本號。 VERSION: ${{ needs.build.outputs.version }} + # test job 的步驟。 steps: - - name: Setup LLM CLI - uses: https://gitea.jsc.idv.tw/actions/setup-${{ vars.ACTION_SETUP_LLM_CLI }} - with: - oauth: ${{ secrets.LLM_OAUTH }} - - name: Run AI Code Review - id: ai-code-review - uses: https://gitea.jsc.idv.tw/actions/ai-code-review@v${{ env.VERSION }} - with: - token: ${{ secrets.TOKEN }} - model: ${{ vars.LLM_NAME }} + # 安裝或設定 LLM CLI。 + - name: Setup LLM CLI + # 使用對應的 setup action。 + uses: https://gitea.jsc.idv.tw/actions/setup-${{ vars.ACTION_SETUP_LLM_CLI }} + # 傳入設定。 + with: + # LLM CLI 的 OAuth 憑證。 + oauth: ${{ secrets.LLM_OAUTH }} + # 執行 AI Code Review action。 + - name: Run AI Code Review + # step id,方便追蹤。 + id: ai-code-review + # 使用本 repo 發佈的 action。 + uses: https://gitea.jsc.idv.tw/actions/ai-code-review@v${{ env.VERSION }} + # action 參數。 + with: + # 存取 Gitea API 的 token。 + token: ${{ secrets.TOKEN }} + # 指定 LLM 模型名稱。 + model: ${{ vars.LLM_NAME }} + # 第三個 job:輸出最終版本資訊。 result: + # job 顯示名稱。 name: 3. RESULT + # 使用 Ubuntu runner。 runs-on: ubuntu + # 需等待前兩個 job。 needs: [build,test] + # job 層級環境變數。 env: + # 延續 build 的版本號。 VERSION: ${{ needs.build.outputs.version }} + # result job 的步驟。 steps: - - name: Show Version - run: echo "$VERSION" + # 顯示版本號。 + - name: Show Version + # 將版本輸出到 log。 + run: echo "$VERSION" diff --git a/.gitea/workflows/master.yaml b/.gitea/workflows/master.yaml index 31f0a57..d4e4d89 100644 --- a/.gitea/workflows/master.yaml +++ b/.gitea/workflows/master.yaml @@ -1,25 +1,52 @@ +# 用途:master workflow 的 command-file 草稿,保留原始流程並補上逐行說明。 +# 更新時間:2026/07/11 19:00:45 +# workflow 名稱,對應 Gitea UI 中的顯示標題。 name: CD +# 定義此 workflow 的觸發事件。 on: + # 在 push 事件時執行。 push: + # 限定觸發分支。 branches: - - master + # 只有推送到 master 分支才執行。 + - master +# workflow 內的工作列表。 jobs: + # 單一 job:輸出 context、檢查 commit tag。 deploy: + # job 顯示名稱。 name: DEPLOY + # 使用 Ubuntu runner。 runs-on: ubuntu + # job 層級環境變數。 env: + # 將完整 Gitea context 轉成 JSON 字串。 GITEA_CONTEXT: ${{ toJSON(gitea) }} + # 取第二筆 commit 的 id 作為查詢目標。 COMMIT_SHA: ${{ gitea.event.commits[1].id }} + # deploy job 的步驟。 steps: - - name: Show Gitea Context - run: echo "$GITEA_CONTEXT" | jq . - - name: Source Code Checkout - uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} - with: - fetch-depth: 0 - fetch-tags: true - - name: Get Commit Tag - id: commit - run: echo "tag=$(git describe --contains ${{ env.COMMIT_SHA }})" >> $GITEA_OUTPUT - - name: Show Tag - run: echo "${{ steps.commit.outputs.tag }}" + # 顯示 Gitea context。 + - name: Show Gitea Context + # 將 context 格式化輸出。 + run: echo "$GITEA_CONTEXT" | jq . + # 取回完整原始碼與 tags。 + - name: Source Code Checkout + # 使用 checkout action。 + uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} + # checkout 參數。 + with: + # 取得完整歷史。 + fetch-depth: 0 + # 一併抓取 tags。 + fetch-tags: true + # 查詢指定 commit 對應的 tag。 + - name: Get Commit Tag + # 供後續步驟讀取輸出。 + id: commit + # 把查到的 tag 寫入 action 輸出。 + run: echo "tag=$(git describe --contains ${{ env.COMMIT_SHA }})" >> $GITEA_OUTPUT + # 顯示剛查到的 tag。 + - name: Show Tag + # 將 tag 印到 log。 + run: echo "${{ steps.commit.outputs.tag }}" diff --git a/.gitea/workflows/readme.md b/.gitea/workflows/readme.md index c376ad2..d8c10be 100644 --- a/.gitea/workflows/readme.md +++ b/.gitea/workflows/readme.md @@ -1,9 +1,49 @@ -# GITEA NODE ACTION 的工作流列表 +# GITEA NODE ACTION 工作流說明草稿 -- CI - - BUILD - - TEST - - RESULT -- CD - - BUILD - - DEPLOY +更新時間:2026/07/11 18:54:51 + +## 總覽 + +此專案目前包含兩個 workflow: + +- `CI`:處理 pull request 期間的版本計算、釋出與 AI 程式碼審查。 +- `CD`:處理推送到 `master` 分支後的部署相關檢查與資訊輸出。 + +## Workflow 明細 + +### CI + +- 檔案位置:`.gitea/workflows/ci.yaml` +- 用途:在 pull request 事件中計算版本,必要時建立 release,並在 beta 情境下執行 AI 程式碼審查。 +- 觸發條件:`pull_request`,事件類型為 `opened` 與 `synchronize`。 +- 主要輸入 / 環境參數: + - `gitea.base_ref`:用來判斷是否為 `develop`,進而決定 `IS_BETA`。 + - `vars.ACTION_CALCULATE_VERSION`:提供 `calculate-version` action 的版本。 + - `vars.ACTION_GITEA_RELEASE_VERSION`:提供 release action 的版本。 + - `secrets.LLM_OAUTH`:設定 LLM CLI 的 OAuth。 + - `secrets.TOKEN`:提供 AI 程式碼審查 action 存取 Gitea API。 + - `vars.LLM_NAME`:指定審查使用的模型名稱。 +- 重要注意事項: + - `test` job 只會在 `IS_BETA == true` 時執行,也就是 pull request 目標分支為 `develop` 時。 + - `Publishing Release` 會使用 `VERSION` 與 `gitea.sha` 建立 release 與 tag。 + - 若變數或 secret 未設定,對應步驟會失敗,需人工確認部署前置條件。 + +### CD + +- 檔案位置:`.gitea/workflows/master.yaml` +- 用途:在 `master` 分支推送後輸出 Gitea context、檢查提交標籤,作為後續部署流程的基礎。 +- 觸發條件:`push` 到 `master` 分支。 +- 主要輸入 / 環境參數: + - `gitea` 事件內容:轉成 `GITEA_CONTEXT` 後交給 `jq` 顯示。 + - `gitea.event.commits[1].id`:作為 `COMMIT_SHA`,用來查詢 commit tag。 + - `vars.ACTION_CHECKOUT_VERSION`:提供 `actions/checkout` 的版本。 + - `GITEA_OUTPUT`:寫入 `git describe --contains` 的結果。 +- 重要注意事項: + - `COMMIT_SHA` 取用 commits 陣列的第 2 筆資料,若 push 事件實際只有 1 筆 commit,需人工確認是否會發生索引風險。 + - `Get Commit Tag` 依賴完整的 git 歷史與 tags,因此 checkout 已設定 `fetch-depth: 0` 與 `fetch-tags: true`。 + - `Show Gitea Context` 會輸出完整事件內容,若包含敏感資訊,需注意執行環境的日誌保存策略。 + +## 備註 + +- 本檔為草稿版本,僅整理 workflow 行為與設定重點,不修改任何 workflow 實際邏輯。 +- 若後續要覆蓋正式檔,請先確認 `ci.yaml` 與 `master.yaml` 的變數與 secret 已在目標環境中正確配置。 diff --git a/action.yml b/action.yml index 1d59e9f..7b4a042 100644 --- a/action.yml +++ b/action.yml @@ -1,16 +1,34 @@ +# 用途:AI Code Review Action 的設定檔草稿,保留原始輸入與 Node 入口,並補上逐行說明。 +# 更新時間:2026/07/11 18:54:51 +# Action 顯示名稱,讓工作流程與使用者介面可以辨識此動作。 name: 'AI Code Review' +# Action 的簡短用途說明。 description: 'AI 程式碼審查' +# 作者或維護者名稱。 author: 'Jeffery' +# 定義此 Action 對外提供的輸入參數。 inputs: + # 用於存取 Gitea API 的授權 Token。 token: + # token 參數的用途說明。 description: '操作 Gitea API 的 Token' + # 此參數為必要,執行時必須提供。 required: true + # 用於存取 Gitea Commit API 的授權 Token。 comment_token: + # comment_token 參數的用途說明。 description: '操作 Gitea Commit API 的 Token' + # 此參數為選填,沒有提供時不影響 Action 啟動。 required: false + # 指定執行 AI 程式碼審查時所使用的模型名稱。 model: + # model 參數的用途說明。 description: '執行 AI 程式碼審查使用的 LLM 名稱' + # 此參數為選填,未提供時由執行環境或預設值決定。 required: false +# 宣告 Action 的執行方式與主要進入點。 runs: + # 使用 Node.js 24 執行此 Action。 using: 'node24' + # Action 的主要進入檔,實際邏輯從此檔案開始。 main: 'src/main.js' diff --git a/src/config.js b/src/config.js index 646068a..722127d 100644 --- a/src/config.js +++ b/src/config.js @@ -51,6 +51,12 @@ export const EXCLUSIONS_PATH = '.gitea/ai-review/exclusions.json'; * @returns {import('https').Agent} 已關閉憑證驗證的 HTTPS Agent 單例。 */ let _insecureHttpsAgent = null; +/** + * 取得一個關閉 TLS 憑證驗證的 HTTPS Agent 單例,供內部服務連線使用。 + * + * @remarks 只應在信任的內網或測試環境使用;若需要完整 TLS 安全性,應改用預設 + * `https.Agent`,不要調用這個函式。 + */ export function getInsecureHttpsAgent() { return (_insecureHttpsAgent ??= new https.Agent({ rejectUnauthorized: false })); } @@ -86,10 +92,22 @@ const CLI_CANDIDATES = [ }, ]; +/** + * 取得目前支援的 LLM CLI 指令名稱清單。 + * + * @remarks 內容直接取自 `CLI_CANDIDATES`,若日後候選清單增減,輸出會同步變動。 + */ export function getLLMCLICommands() { return CLI_CANDIDATES.map(c => c.command); } +/** + * 檢查指定 CLI 指令是否可在目前環境中執行。 + * + * @param {string} command - 要檢查的指令名稱。 + * @returns {boolean} 找得到指令時回傳 `true`,否則回傳 `false`。 + * @remarks 透過 `/bin/sh -lc "command -v "` 檢查,屬於同步存在性檢查。 + */ function commandExists(command) { try { execFileSync('/bin/sh', ['-lc', `command -v ${command}`], { stdio: 'ignore' }); diff --git a/src/findings.js b/src/findings.js index 9553d6f..e65ef81 100644 --- a/src/findings.js +++ b/src/findings.js @@ -111,6 +111,12 @@ function cleanText(value) { * 以模組層級 Map 對「字串輸入」做 memoization,避免重複跑 NFKC/正則替換。 */ const _normalizeTextCache = new Map(); +/** + * 將文字正規化成比對用形式。 + * + * @param {*} value - 任意值。 + * @remarks 適合用於誤報過濾與排除條目比對。 + */ export function normalizeText(value) { if (typeof value === 'string' && _normalizeTextCache.has(value)) return _normalizeTextCache.get(value); const result = cleanText(value) diff --git a/src/llm.js b/src/llm.js index b534bdc..1e9e4d9 100644 --- a/src/llm.js +++ b/src/llm.js @@ -56,6 +56,16 @@ function buildPrompt(systemPrompt, userContent) { ].join('\n'); } +/** + * 依不同 AI provider 產生 CLI 參數。 + * + * @param {*} provider - AI provider 名稱。 + * @param {*} model - 模型名稱。 + * @param {*} promptFile - prompt 檔路徑,供 `opencode` 使用。 + * @param {*} prompt - 直接傳給 CLI 的 prompt 文字,供部分 provider 使用。 + * @remarks 適合把不同 CLI 的參數差異集中管理。 + * @remarks 目前支援的 provider 名稱是硬編碼的,新增 provider 時需人工確認是否同步更新所有呼叫端。 + */ function cliArgs({ provider, model, promptFile = null, prompt = null }) { if (provider === 'codex') { return ['exec', '--model', model, '--sandbox', 'read-only', '--skip-git-repo-check', '-']; @@ -92,12 +102,30 @@ export function extractMeaningfulError(raw, limit = 1000) { return picked.length > limit ? picked.slice(-limit) : picked; } +/** + * 將 CLI 例外整理成較精簡的錯誤摘要。 + * + * @param {*} e - 被拋出的錯誤物件,可能含 `stderr`、`stdout`、`message`。 + * @remarks 適合在 log 與錯誤重新拋出前先整理訊息。 + * @remarks 若錯誤物件結構和預期不同,仍會退回字串化處理,屬保守容錯。 + */ function summarizeCliError(e) { const stderr = String(e.stderr || '').trim(); const stdout = String(e.stdout || '').trim(); return extractMeaningfulError(stderr || stdout || e.message || String(e)); } +/** + * 執行 AI 助理 CLI 並回傳純文字結果。 + * + * @param {*} provider - CLI provider 名稱。 + * @param {*} command - 實際可執行指令。 + * @param {*} model - 要使用的模型名稱。 + * @param {*} prompt - 送給 CLI 的完整 prompt 內容。 + * @remarks 適合用在需呼叫外部 AI CLI 的情境。 + * @remarks 逾時與輸出上限由環境變數控制,預設值是保守設定。 + * @remarks 若子行程回傳非 0,錯誤訊息會由上層摘要處理。 + */ async function runAssistantCLI({ provider, command, model }, prompt) { let tempDir = null; let promptFile = null; @@ -120,7 +148,6 @@ async function runAssistantCLI({ provider, command, model }, prompt) { child.kill('SIGTERM'); reject(new Error(`${provider} CLI 逾時 (${timeout}ms)`)); }, timeout); - const append = (kind, chunk) => { if (kind === 'stdout') stdout += chunk; else stderr += chunk;