feat(ai-code-review): 將 develop 合併至 master #2
@@ -1,39 +1,96 @@
|
|||||||
|
# ============================================================================
|
||||||
|
# 用途:Gitea Actions CI workflow —— 當 PR 開啟或同步(推新 commit)到 master/develop 時,
|
||||||
|
# 自動 checkout 完整 git 歷史、計算下一個語意化版本號(develop 走 beta 預發布),
|
||||||
|
# 接著建立 Gitea Release(副作用:建立 git tag 與 Release),正式版並會清理舊 Release。
|
||||||
|
# 更新時間:2026/07/17 16:49:21
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
# workflow 名稱:顯示於 Gitea Actions 執行清單,方便辨識這條 CI pipeline
|
||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
|
# 觸發條件設定區塊
|
||||||
on:
|
on:
|
||||||
|
# 以 pull request 事件觸發(非 push),確保版本計算與發布綁定 PR 流程
|
||||||
pull_request:
|
pull_request:
|
||||||
|
# 只有目標分支(base branch)為下列分支的 PR 才會觸發
|
||||||
branches:
|
branches:
|
||||||
|
# 正式分支:PR 目標為 master 時發布正式版 Release
|
||||||
- master
|
- master
|
||||||
|
# 開發分支:PR 目標為 develop 時發布 beta 預發布(prerelease)
|
||||||
- develop
|
- develop
|
||||||
|
# 只在 PR 開啟(opened)與來源分支推新 commit 同步(synchronize)時觸發,
|
||||||
|
# 避免 reopened/labeled 等其他 PR 事件重複跑 CI
|
||||||
types: [opened, synchronize]
|
types: [opened, synchronize]
|
||||||
|
|
||||||
|
# workflow 層級環境變數:所有 job/step 皆可透過 env.* 取用
|
||||||
env:
|
env:
|
||||||
|
# 目前 repo 名稱,取自 Gitea 事件 payload,供後續步驟組合 Release 名稱等用途
|
||||||
REPOSITORY_NAME: ${{ gitea.event.repository.name }}
|
REPOSITORY_NAME: ${{ gitea.event.repository.name }}
|
||||||
|
# 是否為 beta(預發布)流程:PR 目標分支為 develop 時為 'true',否則為 'false';
|
||||||
|
# 注意此值為字串型別,後續 if 條件需以字串比較(== 'false')
|
||||||
IS_BETA: ${{ gitea.base_ref == 'develop' }}
|
IS_BETA: ${{ gitea.base_ref == 'develop' }}
|
||||||
|
|
||||||
|
# job 定義區塊
|
||||||
jobs:
|
jobs:
|
||||||
|
# 唯一的 job:建置與發布
|
||||||
build:
|
build:
|
||||||
|
# job 顯示名稱:於 Gitea Actions 介面顯示為 BUILD
|
||||||
name: BUILD
|
name: BUILD
|
||||||
|
# 指定 runner 標籤:由標籤為 ubuntu 的 Gitea Actions runner 執行
|
||||||
runs-on: ubuntu
|
runs-on: ubuntu
|
||||||
|
# 依序執行的步驟
|
||||||
steps:
|
steps:
|
||||||
|
# 步驟 1:checkout 原始碼與完整 git 歷史(含所有 tag),
|
||||||
|
# 版本計算需要既有 tag 才能推算下一個版本號
|
||||||
- name: 取得存取庫資訊 (含 Tag)
|
- name: 取得存取庫資訊 (含 Tag)
|
||||||
|
# 使用官方 checkout action;vars.ACTION_CHECKOUT_VERSION 為
|
||||||
|
# Gitea 組織/repo 層級變數(Actions variables)提供的 action 版本,集中管理升版
|
||||||
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
||||||
with:
|
with:
|
||||||
|
# fetch-depth: 0 代表抓取完整歷史(非 shallow clone),確保 tag 與 commit 歷史齊全
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
# 步驟 2:依既有 tag 計算下一個語意化版本號
|
||||||
- name: 計算下一個版本號
|
- name: 計算下一個版本號
|
||||||
|
# 使用自建的 calculate-next-version action(gitea.jsc.idv.tw 私有 Gitea 上的 docker action);
|
||||||
|
# vars.ACTION_CALCULATE_NEXT_VERSION 同為 Gitea 組織/repo 變數提供的版本
|
||||||
uses: https://gitea.jsc.idv.tw/docker-actions/calculate-next-version@${{ vars.ACTION_CALCULATE_NEXT_VERSION }}
|
uses: https://gitea.jsc.idv.tw/docker-actions/calculate-next-version@${{ vars.ACTION_CALCULATE_NEXT_VERSION }}
|
||||||
|
# step id:後續步驟以 steps.calculate-next-version.outputs.value 取得計算結果
|
||||||
id: calculate-next-version
|
id: calculate-next-version
|
||||||
with:
|
with:
|
||||||
|
# 傳入是否為 beta:true 時產生預發布版號(例如帶 -beta 後綴),false 時產生正式版號
|
||||||
is_beta: ${{ env.IS_BETA }}
|
is_beta: ${{ env.IS_BETA }}
|
||||||
|
# 步驟 3:建立 Gitea Release(副作用:建立對應 git tag 與 Release 頁面)
|
||||||
|
# 需人工確認:此步驟在 PR opened/synchronize 就會發布 Release 並打 tag,
|
||||||
|
# 亦即 PR 尚未合併前每次推 commit 都會產生 Release;請確認這是預期行為
|
||||||
|
# (通常發布會放在 push 到 master/develop 或 PR merged 之後)。
|
||||||
- name: 發布成品
|
- name: 發布成品
|
||||||
|
# 使用第三方 akkuman/gitea-release-action 建立 Release;
|
||||||
|
# vars.ACTION_GITEA_RELEASE_VERSION 為 Gitea 組織/repo 變數提供的 action 版本
|
||||||
|
# 需人工確認:此 action 通常需要 GITEA_TOKEN/token 才能建立 Release,
|
||||||
|
# 本設定未見明確傳入 token,請確認 runner 預設 token 權限是否足夠。
|
||||||
uses: akkuman/gitea-release-action@${{ vars.ACTION_GITEA_RELEASE_VERSION }}
|
uses: akkuman/gitea-release-action@${{ vars.ACTION_GITEA_RELEASE_VERSION }}
|
||||||
|
# step 層級環境變數
|
||||||
env:
|
env:
|
||||||
|
# 將上一步計算出的版本號存為 VERSION,供本步驟的 with 參數引用
|
||||||
VERSION: ${{ steps.calculate-next-version.outputs.value }}
|
VERSION: ${{ steps.calculate-next-version.outputs.value }}
|
||||||
with:
|
with:
|
||||||
|
# Release 顯示名稱:{repo 名稱} v{版本號}
|
||||||
name: "${{ gitea.event.repository.name }} v${{ env.VERSION }}"
|
name: "${{ gitea.event.repository.name }} v${{ env.VERSION }}"
|
||||||
|
# 要建立的 git tag 名稱:v{版本號}
|
||||||
tag_name: "v${{ env.VERSION }}"
|
tag_name: "v${{ env.VERSION }}"
|
||||||
|
# tag 指向的 commit:gitea.sha 為觸發此 workflow 的 commit
|
||||||
|
# 需人工確認:pull_request 事件的 sha 通常是 PR 的 merge commit(暫時性 ref),
|
||||||
|
# 對其打 tag 可能指向未實際合入分支的 commit,請確認 tag 目標是否符合預期。
|
||||||
target_commitish: "${{ gitea.sha }}"
|
target_commitish: "${{ gitea.sha }}"
|
||||||
|
# 是否標記為預發布:develop 目標的 PR 為 true(beta),master 為 false(正式版)
|
||||||
prerelease: ${{ env.IS_BETA }}
|
prerelease: ${{ env.IS_BETA }}
|
||||||
|
# 步驟 4:清理舊 Release,避免 Release 清單無限增長
|
||||||
- name: 清理舊成品
|
- name: 清理舊成品
|
||||||
|
# 使用自建的 clean-old-release action;
|
||||||
|
# vars.ACTION_CLEAN_OLD_RELEASE 為 Gitea 組織/repo 變數提供的 action 版本
|
||||||
|
# 需人工確認:其他兩個自建 action 的變數名皆以 _VERSION 結尾,
|
||||||
|
# 此變數名為 ACTION_CLEAN_OLD_RELEASE(無 _VERSION 後綴),請確認命名是否刻意如此。
|
||||||
uses: https://gitea.jsc.idv.tw/docker-actions/clean-old-release@${{ vars.ACTION_CLEAN_OLD_RELEASE }}
|
uses: https://gitea.jsc.idv.tw/docker-actions/clean-old-release@${{ vars.ACTION_CLEAN_OLD_RELEASE }}
|
||||||
|
# 僅在非 beta(正式版,PR 目標為 master)時執行清理;
|
||||||
|
# 因 env.IS_BETA 為字串,故以 == 'false' 比較
|
||||||
if: ${{ env.IS_BETA == 'false' }}
|
if: ${{ env.IS_BETA == 'false' }}
|
||||||
|
|||||||
@@ -1,272 +1,618 @@
|
|||||||
# Gitea Node Action 範本
|
# AI Code Review
|
||||||
|
|
||||||
Node(JavaScript)action 讓你用 JavaScript 撰寫 action 邏輯,直接跑在 runner 內建的 Node runtime 上。相較於 composite(純 YAML 組合 step)與 Docker(包 image)action,node action 適合需要**程式邏輯、呼叫 API、跨平台**的情境,且啟動速度比 Docker action 快。
|
> 更新時間:2026/07/17 16:49:21
|
||||||
|
|
||||||
本文件整理 node action 的 `action.yml` 中**所有可用參數、說明與限制**,並特別標出 **Gitea 與 GitHub Actions 的差異**。範例皆對應本 repo 的 [`action.yml`](./action.yml) 與 [`src/index.js`](./src/index.js)。
|
AI 多角色 code review 的 Gitea **node action**(`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]`/`[failure]`)供下次觸發快速回報。
|
||||||
|
|
||||||
> 語法基準:Gitea Actions 以相容 GitHub Actions metadata 語法為目標,但兩者有明確差異(見「Gitea vs GitHub」章節)。Gitea 端的行為亦受底層 [`act`](https://gitea.com/gitea/act) runner 版本影響——尤其**支援的 Node 版本**——實作前建議以測試機驗證。
|
## 使用方式
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 目錄
|
|
||||||
|
|
||||||
- [完整結構總覽](#完整結構總覽)
|
|
||||||
- [頂層參數](#頂層參數)
|
|
||||||
- [`inputs`(輸入參數)](#inputs輸入參數)
|
|
||||||
- [`outputs`(輸出)](#outputs輸出)
|
|
||||||
- [`runs`(執行設定)](#runs執行設定)
|
|
||||||
- [在 JavaScript 內取值 / 設值](#在-javascript-內取值--設值)
|
|
||||||
- [建置與打包(相依套件)](#建置與打包相依套件)
|
|
||||||
- [Node action 的限制與注意事項](#node-action-的限制與注意事項)
|
|
||||||
- [Gitea vs GitHub Actions 差異](#gitea-vs-github-actions-差異)
|
|
||||||
- [本 repo 範例對照](#本-repo-範例對照)
|
|
||||||
- [參考來源](#參考來源)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 完整結構總覽
|
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: 'Gitea Node Template' # 必填
|
# .gitea/workflows/review.yaml(呼叫端範例)
|
||||||
description: 'Gitea Node 範本' # 必填
|
name: AI-REVIEW
|
||||||
author: 'Jeffery' # 選填
|
on:
|
||||||
|
pull_request:
|
||||||
inputs: # 選填,定義輸入參數
|
branches: [master, develop]
|
||||||
message:
|
types: [opened, synchronize]
|
||||||
description: '輸入訊息'
|
jobs:
|
||||||
required: false
|
review:
|
||||||
default: 'Hello, World!'
|
runs-on: ubuntu
|
||||||
|
steps:
|
||||||
outputs: # 選填,定義輸出
|
- uses: actions/checkout@v4
|
||||||
message:
|
with:
|
||||||
description: '輸出訊息' # node action 只需 description,不需 value
|
fetch-depth: 0 # 需完整歷史以計算 merge-base
|
||||||
|
- uses: https://gitea.jsc.idv.tw/node-actions/ai-code-review@v1
|
||||||
runs: # 必填
|
with:
|
||||||
using: 'node24' # 必填,node runtime(最新版;見版本說明)
|
token: ${{ secrets.GITHUB_TOKEN }} # 必填:PR 留言與 push findings 用
|
||||||
main: 'src/index.js' # 必填,進入點 JS 檔
|
model: '' # 選填:指定 AI 模型(空=工具預設)
|
||||||
pre: 'setup.js' # 選填,main 之前執行
|
create-issue: 'false' # 選填:'true' 時問題另建 issue 追蹤
|
||||||
pre-if: "always()" # 選填,pre 的條件,預設 always()
|
|
||||||
post: 'cleanup.js' # 選填,main 之後執行
|
|
||||||
post-if: "always()" # 選填,post 的條件,預設 always()
|
|
||||||
|
|
||||||
branding: # 選填(Marketplace 用,Gitea 內部可省略)
|
|
||||||
icon: 'activity'
|
|
||||||
color: 'blue'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> 📌 檔名**只能**是 `action.yml` 或 `action.yaml`,放在 action repo 根目錄。
|
| input | 必填 | 預設 | 說明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `token` | ✅ | — | Gitea API token(PR 留言與 push findings 用;呼叫端以 secrets 傳入) |
|
||||||
|
| `model` | ❌ | `''` | 指定 AI 工具使用的模型(空值=各工具預設) |
|
||||||
|
| `create-issue` | ❌ | `'false'` | `'true'` 時建立 issue 逐條留言問題明細,收尾只 commit `exclusions.json` |
|
||||||
|
|
||||||
---
|
審查流程(10 步驟):
|
||||||
|
|
||||||
## 頂層參數
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
| 參數 | 必填 | 說明 |
|
S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure]
|
||||||
|------|------|------|
|
S1 -->|未命中| S2[2 偵測 AI 工具並留言]
|
||||||
| `name` | ✅ | Action 名稱。 |
|
S2 --> S3[3 讀 .reviewignore 整理 diff 並留言]
|
||||||
| `description` | ✅ | Action 簡短說明。 |
|
S3 --> S4[4 攻擊方登場留言]
|
||||||
| `author` | ❌ | 作者名稱。 |
|
S4 --> S5[5 攻擊方 sub agent 並行找問題]
|
||||||
| `inputs` | ❌ | 輸入參數定義(見下)。 |
|
S5 --> S6[6 防守方登場留言]
|
||||||
| `outputs` | ❌ | 輸出定義(見下)。 |
|
S6 --> S7[7 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json]
|
||||||
| `runs` | ✅ | 執行設定;node action 用 `using: 'node24'` + `main`。 |
|
S7 --> S8[8 舊留言標記解決]
|
||||||
| `branding` | ❌ | Marketplace 顯示用的 `icon` 與 `color`。 |
|
S8 --> S9[9 嚴重問題逐條掛行留言]
|
||||||
|
S9 --> S10[10 警告+建議彙整表格留言]
|
||||||
---
|
S10 --> E1[收尾 commit/push + exit code]
|
||||||
|
|
||||||
## `inputs`(輸入參數)
|
|
||||||
|
|
||||||
每個 input 是 `inputs.<input_id>` 底下的一組設定。`<input_id>` 必須以字母或底線開頭,只能含英數、`-`、`_`:
|
|
||||||
|
|
||||||
| 欄位 | 必填 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `description` | ✅ | 參數說明。 |
|
|
||||||
| `required` | ❌ | 是否必填,布林值,預設 `false`。 |
|
|
||||||
| `default` | ❌ | 預設值;呼叫端沒傳時採用。**只能是字串**。 |
|
|
||||||
| `deprecationMessage` | ❌ | 標記此 input 已棄用,使用時記錄警告訊息。 |
|
|
||||||
|
|
||||||
**呼叫端傳值**(用 `with`):
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
- uses: ./
|
|
||||||
with:
|
|
||||||
message: 'Hi there'
|
|
||||||
```
|
```
|
||||||
|
|
||||||
> ✅ **與 composite 的關鍵差異**:node action **會**自動把每個 input 轉成 `INPUT_<NAME>` 環境變數——名稱**轉大寫**、**空白換成底線**(例:input `my message` → `INPUT_MY_MESSAGE`)。在 JS 內即可用 `process.env.INPUT_MESSAGE` 或 `core.getInput('message')` 取值。
|
## 專案列表
|
||||||
>
|
|
||||||
> ⚠️ `required: true` **不會**在缺值時自動報錯——runner 只是標記語意,實際檢查要自己在程式裡做(或用 `core.getInput('x', { required: true })`)。
|
|
||||||
>
|
|
||||||
> input 值一律是**字串**;數字、布林傳進來也會變字串(例如 `"true"`),比較時要留意。
|
|
||||||
|
|
||||||
---
|
### 專案描述表
|
||||||
|
|
||||||
## `outputs`(輸出)
|
| 專案名稱 | 專案描述 |
|
||||||
|
| --- | --- |
|
||||||
|
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | Gitea node action:提供台北時區日誌工具、runner 上下文載入、git diff/commit 操作、Gitea REST API 客戶端(留言/review/issue/標籤)、AI CLI 工具偵測與 sub agent 執行、角色提示載入、固定留言模板,以及多角色審查編排(攻擊方找問題、防守方裁決、findings 保存、誤判回寫、建問題模式) |
|
||||||
|
|
||||||
node action 的 output **只需要 `description`**,**不需要**(也不該有)composite 那種 `value` 欄位——實際的值是在**執行時**由程式寫入:
|
### 參考專案表
|
||||||
|
|
||||||
| 欄位 | 必填 | 說明 |
|
| 專案名稱 | 參考專案列表 |
|
||||||
|------|------|------|
|
| --- | --- |
|
||||||
| `description` | ✅ | 輸出說明。 |
|
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | 無 |
|
||||||
|
|
||||||
**在 JS 內設定 output** → 寫入 `$GITHUB_OUTPUT` 檔案(或用 `core.setOutput`):
|
### NuGet 套件表
|
||||||
|
|
||||||
|
| 專案名稱 | NuGet 套件列表 |
|
||||||
|
| --- | --- |
|
||||||
|
| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | 無 |
|
||||||
|
|
||||||
|
## 功能列表
|
||||||
|
|
||||||
|
### ai-code-review
|
||||||
|
|
||||||
|
| 功能名稱 | 功能描述 |
|
||||||
|
| --- | --- |
|
||||||
|
| [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L20) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) |
|
||||||
|
| [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L41) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) |
|
||||||
|
| [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) |
|
||||||
|
| [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) |
|
||||||
|
| [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) |
|
||||||
|
| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) |
|
||||||
|
| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) |
|
||||||
|
| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) |
|
||||||
|
| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) |
|
||||||
|
| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) |
|
||||||
|
| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) |
|
||||||
|
| [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) |
|
||||||
|
| [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) |
|
||||||
|
| [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) |
|
||||||
|
| [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) |
|
||||||
|
| [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) |
|
||||||
|
| [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) |
|
||||||
|
| [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) |
|
||||||
|
| [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) |
|
||||||
|
| [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) |
|
||||||
|
| [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) |
|
||||||
|
| [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) |
|
||||||
|
| [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) |
|
||||||
|
| [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) |
|
||||||
|
| [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) |
|
||||||
|
| [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) |
|
||||||
|
| [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) |
|
||||||
|
| [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) |
|
||||||
|
| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L88) | [產生步驟 2 審查工具留言](#templatestoolcomment) |
|
||||||
|
| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L132) | [產生步驟 3 變更摘要留言](#templatesdiffcomment) |
|
||||||
|
| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L173) | [產生步驟 4/6 角色登場留言](#templatesrolescomment) |
|
||||||
|
| [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) |
|
||||||
|
| [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) |
|
||||||
|
| [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) |
|
||||||
|
| [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) |
|
||||||
|
| [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) |
|
||||||
|
| [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) |
|
||||||
|
| [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) |
|
||||||
|
| [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) |
|
||||||
|
| [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) |
|
||||||
|
| [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) |
|
||||||
|
| [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) |
|
||||||
|
| [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) |
|
||||||
|
| [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) |
|
||||||
|
| [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) |
|
||||||
|
| [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) |
|
||||||
|
| [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) |
|
||||||
|
| [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) |
|
||||||
|
| [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) |
|
||||||
|
| [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) |
|
||||||
|
|
||||||
|
## 使用範例
|
||||||
|
|
||||||
|
<a id="logtaipeinow"></a>
|
||||||
|
### log.taipeiNow
|
||||||
|
|
||||||
|
將指定時間(省略時為現在)轉為台北時區(Asia/Taipei)的 `yyyy/MM/dd HH:mm:ss` 字串,輸出不受主機系統時區影響;供日誌時間戳與 findings 產生時間使用。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const fs = require('fs');
|
const { taipeiNow } = require('./src/lib/log');
|
||||||
const os = require('os');
|
taipeiNow(); // '2026/07/17 16:46:13'
|
||||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `message=Hello${os.EOL}`);
|
taipeiNow(new Date('2026-01-01')); // '2026/01/01 08:00:00'
|
||||||
// 或(需要 @actions/core): core.setOutput('message', 'Hello');
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**呼叫端取用 output**:
|
<a id="logtaipeifilestamp"></a>
|
||||||
|
### log.taipeiFileStamp
|
||||||
|
|
||||||
```yaml
|
產生檔名用時間戳 `yyyy-MM-dd-HH:mm:ss`(空白換成 `-`);findings 檔案即以此命名。注意輸出含 `:`,Linux 檔名合法、不可移植到 Windows。
|
||||||
- id: node-template
|
|
||||||
uses: ./
|
|
||||||
- run: echo "${{ steps.node-template.outputs.message }}"
|
|
||||||
```
|
|
||||||
|
|
||||||
> 📏 **大小限制**:單一 job 的 outputs 上限 1 MB;一次 workflow run 全部 outputs 合計上限 50 MB。大量資料請改用 artifact。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## `runs`(執行設定)
|
|
||||||
|
|
||||||
node action 的 `runs` 欄位:
|
|
||||||
|
|
||||||
| 欄位 | 必填 | 說明 |
|
|
||||||
|------|------|------|
|
|
||||||
| `using` | ✅ | Node runtime。最新為 `node24`(本 repo 採用);亦可用 `node20` / `node16`。實際可用版本取決於 runner(見下方 Gitea 差異)。 |
|
|
||||||
| `main` | ✅ | 進入點 JS 檔(例:`src/index.js` 或打包後的 `dist/index.js`)。 |
|
|
||||||
| `pre` | ❌ | 在 `main` **之前**、job 開始時執行的 JS 檔(可做前置設定)。 |
|
|
||||||
| `pre-if` | ❌ | 決定 `pre` 是否執行的條件,預設 `always()`。 |
|
|
||||||
| `post` | ❌ | 在 `main` **之後**執行的 JS 檔(可做清理、即使 main 失敗仍會跑)。 |
|
|
||||||
| `post-if` | ❌ | 決定 `post` 是否執行的條件,預設 `always()`。 |
|
|
||||||
|
|
||||||
> ⚠️ **`pre` 不支援 local action**:直接放在同一 repo、用 `uses: ./` 呼叫的 local action **無法**使用 `runs.pre`。`pre` / `post` 也是 **node action 專屬**(composite / Docker 沒有)。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 在 JavaScript 內取值 / 設值
|
|
||||||
|
|
||||||
node action 進入點是一支普通的 Node 程式。兩種常見寫法:
|
|
||||||
|
|
||||||
**A) 零相依(本 repo 採用)**——直接讀環境變數、寫檔案,無需 `npm install`:
|
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const message = process.env.INPUT_MESSAGE ?? 'Hello, World!'; // 讀 input
|
const { taipeiFileStamp } = require('./src/lib/log');
|
||||||
fs.appendFileSync(process.env.GITHUB_OUTPUT, `message=${message}\n`); // 寫 output
|
taipeiFileStamp(); // '2026-07-17-16:46:13' → .gitea/ai-review/findings/2026-07-17-16:46:13.json
|
||||||
console.log(`message=${message}`); // 日誌
|
|
||||||
process.exit(1); // 讓 step 失敗
|
|
||||||
```
|
```
|
||||||
|
|
||||||
**B) 使用官方 toolkit `@actions/core`**——語意更清楚,處理跳脫與多行值較穩:
|
<a id="logtaipeifromiso"></a>
|
||||||
|
### log.taipeiFromIso
|
||||||
|
|
||||||
|
將 ISO 8601 時間字串轉為台北時區顯示字串;輸入為空或無法解析時回傳佔位符「—」不丟例外,適合直接嵌進留言表格。
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const core = require('@actions/core');
|
const { taipeiFromIso } = require('./src/lib/log');
|
||||||
const message = core.getInput('message'); // 讀 input(等同 INPUT_MESSAGE)
|
taipeiFromIso('2026-07-17T06:30:05Z'); // '2026/07/17 14:30:05'
|
||||||
core.setOutput('message', message); // 設 output
|
taipeiFromIso(''); // '—'
|
||||||
core.info('...'); // 日誌
|
|
||||||
core.setFailed('錯誤訊息'); // 記錄失敗並以非零碼結束
|
|
||||||
```
|
```
|
||||||
|
|
||||||
需要呼叫 Gitea / GitHub API 時再加 `@actions/github`(提供已驗證的 REST client 與 `github.context`)。
|
<a id="loglog"></a>
|
||||||
|
### log.log
|
||||||
|
|
||||||
---
|
以統一格式 `[yyyy/MM/dd HH:mm:ss][階段][等級]: 訊息` 輸出一行日誌到 stdout;stage 為空時省略階段區塊,等級約定限 INF/WRN/ERR/TRC/DBG。
|
||||||
|
|
||||||
## 建置與打包(相依套件)
|
```js
|
||||||
|
const { log } = require('./src/lib/log');
|
||||||
- **沒有相依套件**(如本 repo):`main` 直接指向原始 `src/index.js` 即可,Gitea **不需要** build 步驟。
|
log('步驟3', 'INF', '變更檔案 5 個,送審 3 個。');
|
||||||
- **有相依套件**(用了 `@actions/core` 等):runner **不會**幫你 `npm install`,你必須把相依一起帶進 repo。二選一:
|
// [2026/07/17 16:46:13][步驟3][INF]: 變更檔案 5 個,送審 3 個。
|
||||||
1. **打包(建議)**:用 [`@vercel/ncc`](https://github.com/vercel/ncc) 把原始碼與相依編成單一檔,再把 `main` 指到它:
|
|
||||||
```bash
|
|
||||||
npm i -D @vercel/ncc
|
|
||||||
npx ncc build src/index.js -o dist # 產生 dist/index.js
|
|
||||||
```
|
|
||||||
並改 `action.yml`:`main: 'dist/index.js'`。**打包後的 `dist/` 要 commit 進 repo。**
|
|
||||||
2. **直接 commit `node_modules`**:可行但體積大、易出問題,一般不建議。
|
|
||||||
|
|
||||||
> 用打包方式時,記得每次改 code 都重新 `ncc build` 並把 `dist/` 一起提交,否則 action 跑的是舊版。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Node action 的限制與注意事項
|
|
||||||
|
|
||||||
1. **相依不會自動安裝**
|
|
||||||
runner 不會在 action repo 內跑 `npm install`。要嘛零相依,要嘛把相依打包 / commit 進 repo(見上一節)。
|
|
||||||
|
|
||||||
2. **`required: true` 不會自動擋**
|
|
||||||
缺少必填 input 時 runner 不會報錯,要自己在程式裡驗證。
|
|
||||||
|
|
||||||
3. **input 一律是字串**
|
|
||||||
`INPUT_*` / `core.getInput` 拿到的都是字串,數字與布林需自行轉型。
|
|
||||||
|
|
||||||
4. **output 有大小上限**
|
|
||||||
單 job 1 MB、單次 run 合計 50 MB;超量請用 artifact。
|
|
||||||
|
|
||||||
5. **`main` 路徑相對於 action 根目錄**
|
|
||||||
`main: src/index.js` 指的是相對於 action repo 根目錄的路徑,不受呼叫端工作目錄影響。要讀 action 自帶的其他檔案時,用 `__dirname` 或 `process.env.GITHUB_ACTION_PATH` 定位,不要用相對於呼叫端的路徑。
|
|
||||||
|
|
||||||
6. **`pre` 不支援 local action、且 `pre`/`post` 為 node 專屬**
|
|
||||||
見 `runs` 章節。
|
|
||||||
|
|
||||||
7. **Node 版本要對得上 runner**
|
|
||||||
`using` 指定的版本必須是該 runner 支援的版本,否則 action 直接被拒(見下方 Gitea 差異)。
|
|
||||||
|
|
||||||
8. **跨 step 共享環境變數 / PATH**
|
|
||||||
在程式內寫入 `$GITHUB_ENV`、`$GITHUB_PATH` 指向的檔案,可讓**後續 step**取得對應的環境變數 / PATH。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Gitea vs GitHub Actions 差異
|
|
||||||
|
|
||||||
Gitea Actions **不是** GitHub Actions 的 100% 複製品。撰寫 node action 時特別注意:
|
|
||||||
|
|
||||||
| 項目 | Gitea 行為 |
|
|
||||||
|------|-----------|
|
|
||||||
| **支援的 Node 版本** | `runs.using` 可用的 node 版本**取決於 act runner 版本**:`node20` 需 runner ≥ v0.2.6;**`node24`(最新,本 repo 採用)需較新的 runner**。若 runner 太舊,會報錯 `The runs.using key in action.yml must be one of: [composite docker node12 node16 node20 go], got node24`——此時請**升級 act runner**,或暫時退回 `node20`。GitHub 端自 2026/03 起 `node24` 已為 JS action 預設。 |
|
|
||||||
| **`using: 'go'`** | Gitea 額外支援 `using: 'go'` 寫 Go action(GitHub 沒有)。 |
|
|
||||||
| **表達式函式** | 依官方比較文件,**僅保證支援 `always()`**;`success()` / `failure()` / `cancelled()` / `hashFiles()` 等其他函式視 `act` runner 版本而定,不保證可用——寫 `if:`(含 `pre-if` / `post-if`)前先在測試機驗證。 |
|
|
||||||
| **`uses` 支援絕對 URL** | 可寫 `uses: https://github.com/actions/checkout@v4` 或 `uses: http://your_gitea/owner/repo@branch`,不限同站 action。 |
|
|
||||||
| **context 檢查較寬鬆** | Gitea 不檢查 context 可用性,`env` context 可用在比 GitHub 更多的位置(但不代表可攜,跨到 GitHub 會失敗)。 |
|
|
||||||
| **被忽略的 job 欄位** | `jobs.<job_id>.timeout-minutes`、`jobs.<job_id>.continue-on-error`、`jobs.<job_id>.environment` 會被忽略。 |
|
|
||||||
| **`runs-on`** | 只接受簡單格式 `runs-on: xyz` 或 `runs-on: [xyz]`,不支援複雜表達式。 |
|
|
||||||
| **annotations / problem matchers** | 不支援,會被忽略。 |
|
|
||||||
| **`permissions` scope** | 支援 `permissions`,但沒有 GitHub 專屬的 `statuses` / `checks` / `deployments` / `id-token` / `security-events` / `pages`;Gitea 有自己的 `code` / `releases` / `wiki` / `projects`。 |
|
|
||||||
|
|
||||||
> 上表以 Gitea 官方文件為準;`act` runner 持續更新,部分限制(尤其表達式函式與 Node 版本)可能隨版本放寬,仍以你環境的實測為準。
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 本 repo 範例對照
|
|
||||||
|
|
||||||
- Action 定義:[`action.yml`](./action.yml)(`using: node24` + `main: src/index.js`)
|
|
||||||
- 進入點程式:[`src/index.js`](./src/index.js)(零相依:讀 `INPUT_MESSAGE`、寫 `$GITHUB_OUTPUT`)
|
|
||||||
- 專案設定:[`package.json`](./package.json)
|
|
||||||
- CI 呼叫範例:[`.gitea/workflows/ci.yaml`](./.gitea/workflows/ci.yaml)
|
|
||||||
|
|
||||||
CI 的 `BUILD` job 呼叫本 action、後續 job 取用其 output:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
build:
|
|
||||||
outputs:
|
|
||||||
message: ${{ steps.build.outputs.message }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }}
|
|
||||||
- id: build
|
|
||||||
uses: ./
|
|
||||||
result:
|
|
||||||
needs: [build, test]
|
|
||||||
steps:
|
|
||||||
- run: echo "${{ needs.build.outputs.message }}"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
<a id="contextloadcontext"></a>
|
||||||
|
### context.loadContext
|
||||||
|
|
||||||
## 參考來源
|
彙整 runner 注入的 `GITHUB_*` 環境變數、`INPUT_*` 輸入參數與事件 payload,組出審查流程所需的完整上下文(repo、PR 編號/標題/描述、head/base、token、model、createIssue、workspace、actionPath 等)。前置條件:於 Actions runner 環境執行;呼叫端應檢查 `prNumber` 與 `token` 是否有值。
|
||||||
|
|
||||||
- [GitHub Actions — Metadata syntax for actions](https://docs.github.com/en/actions/reference/workflows-and-actions/metadata-syntax)
|
```js
|
||||||
- [GitHub Actions — Creating a JavaScript action](https://docs.github.com/en/actions/tutorials/create-actions/create-a-javascript-action)
|
const { loadContext } = require('./src/lib/context');
|
||||||
- [Gitea — Compared to GitHub Actions](https://docs.gitea.com/usage/actions/comparison)
|
const ctx = loadContext();
|
||||||
- [Gitea Blog — Gitea Actions now Supports Node20 based actions](https://blog.gitea.com/node-20-actions-support/)
|
if (!ctx.prNumber || !ctx.token) process.exit(1); // 非 PR 事件或缺 token
|
||||||
- [Gitea — Act Runner](https://docs.gitea.com/usage/actions/act-runner)
|
```
|
||||||
- [@actions/core toolkit](https://github.com/actions/toolkit/tree/main/packages/core)
|
|
||||||
- [@vercel/ncc — 打包工具](https://github.com/vercel/ncc)
|
<a id="gitrepolatestcommitsubject"></a>
|
||||||
|
### gitrepo.latestCommitSubject
|
||||||
|
|
||||||
|
取得目前 HEAD 最新 commit 的訊息標題;主流程步驟 1 以此比對 `chore: update ai-review findings [ai-review-bot][success|failure]` 決定是否直接回報結果。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const gitrepo = require('./src/lib/gitrepo');
|
||||||
|
const subject = gitrepo.latestCommitSubject(process.cwd());
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="gitreporesolvemergebase"></a>
|
||||||
|
### gitrepo.resolveMergeBase
|
||||||
|
|
||||||
|
先嘗試 `git fetch origin <baseRef>`(失敗靜默沿用本地資料),再以 `git merge-base origin/<baseRef> HEAD` 取得共同祖先,作為 diff 比較基準,避免把 base 分支後續演進算進 PR 變更。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const base = gitrepo.resolveMergeBase(cwd, 'master'); // '3f2a…'(40 碼 SHA)
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="gitrepochangedfiles"></a>
|
||||||
|
### gitrepo.changedFiles
|
||||||
|
|
||||||
|
列出 base 與 HEAD 之間有變更的檔案(repo 相對路徑陣列);結果再經 `.reviewignore` 過濾後逐檔送審。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const files = gitrepo.changedFiles(cwd, base); // ['src/index.js', 'action.yml']
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="gitrepofilediff"></a>
|
||||||
|
### gitrepo.fileDiff
|
||||||
|
|
||||||
|
取得單一檔案在 base 與 HEAD 之間的 unified diff 原始文字(無變更時為空字串),供組進攻擊方提示。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const diff = gitrepo.fileDiff(cwd, base, 'src/index.js');
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="gitrepofilelastupdatediso"></a>
|
||||||
|
### gitrepo.fileLastUpdatedIso
|
||||||
|
|
||||||
|
取得檔案最後一次 commit 的 ISO 8601 時間;查不到(未 commit、git 失敗)回空字串,由呼叫端以「—」佔位。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const iso = gitrepo.fileLastUpdatedIso(cwd, 'src/index.js'); // '2026-07-17T15:00:00+08:00'
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="gitrepocommitandpushfindings"></a>
|
||||||
|
### gitrepo.commitAndPushFindings
|
||||||
|
|
||||||
|
以 `ai-review-bot` 身分將指定檔案 commit 並 push 回 PR 來源分支;HEAD 停在 merge commit 時先 detach 到 head sha,暫存區無差異時不建空 commit(回傳 `false`),origin push 失敗改用帶 token 的 URL 重試(該 URL 絕不可輸出到日誌)。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const committed = gitrepo.commitAndPushFindings(cwd, {
|
||||||
|
headRef: 'feature/x', headSha: ctx.headSha,
|
||||||
|
message: 'chore: update ai-review findings [ai-review-bot][success]',
|
||||||
|
files: ['.gitea/ai-review/findings/2026-07-17-16:46:13.json'],
|
||||||
|
token: ctx.token, serverUrl: ctx.serverUrl, repository: ctx.repository,
|
||||||
|
}); // true=已推送、false=無變更略過
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="giteawhoami"></a>
|
||||||
|
### gitea.whoAmI
|
||||||
|
|
||||||
|
取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 8 以 `login` 比對留言作者辨識本 action 發過的留言。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const gitea = require('./src/lib/gitea');
|
||||||
|
const me = await gitea.whoAmI(ctx); // { id, login, ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="giteacreatecommentonissue"></a>
|
||||||
|
### gitea.createCommentOnIssue
|
||||||
|
|
||||||
|
對指定編號的 issue(或 PR,Gitea 兩者共用留言機制)新增一般留言;建問題模式逐條留言問題明細即用本函式。
|
||||||
|
|
||||||
|
```js
|
||||||
|
await gitea.createCommentOnIssue(ctx, issue.number, '🔴 嚴重|...');
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="giteacreateissuecomment"></a>
|
||||||
|
### gitea.createIssueComment
|
||||||
|
|
||||||
|
對本次 PR(`ctx.prNumber`)新增一般留言;為 `createCommentOnIssue` 的便捷包裝,主流程各步驟的留言都經由它發出。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const created = await gitea.createIssueComment(ctx, '## 📋 變更摘要 ...');
|
||||||
|
// created.id 記入本回合留言集合,步驟 8 標註過時時跳過
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="gitealistlabels"></a>
|
||||||
|
### gitea.listLabels
|
||||||
|
|
||||||
|
列出存取庫可用標籤(自動分頁);建問題模式先取得標籤,再交給 `review.selectLabels` 由 AI 挑選。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const labels = await gitea.listLabels(ctx); // [{ id, name, color }, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="giteacreateissue"></a>
|
||||||
|
### gitea.createIssue
|
||||||
|
|
||||||
|
在存取庫建立 issue;`labels`(標籤 id 陣列)僅在非空時帶入。建問題模式以 PR 標題/描述為內容建立追蹤 issue。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const issue = await gitea.createIssue(ctx, { title: 'PR 標題', body: '…', labels: [3, 7] });
|
||||||
|
// issue.number 供後續逐條留言
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="gitealistissuecomments"></a>
|
||||||
|
### gitea.listIssueComments
|
||||||
|
|
||||||
|
列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 8 據此找出 bot 舊留言標註〔已過時〕。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const comments = await gitea.listIssueComments(ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="giteaeditissuecomment"></a>
|
||||||
|
### gitea.editIssueComment
|
||||||
|
|
||||||
|
以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 8 用來替舊留言加上〔已過時〕前綴。
|
||||||
|
|
||||||
|
```js
|
||||||
|
await gitea.editIssueComment(ctx, comment.id, `> 〔已過時〕…\n\n${comment.body}`);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="giteacreatereview"></a>
|
||||||
|
### gitea.createReview
|
||||||
|
|
||||||
|
建立 event 為 `COMMENT` 的 code review,並把行內留言逐條掛在檔案行號上;步驟 9 以單一 review 送出全部嚴重問題。若行號不在 PR diff 內會整包失敗,呼叫端(`review.postSevereComments`)會降級為一般留言。
|
||||||
|
|
||||||
|
```js
|
||||||
|
await gitea.createReview(ctx, '## 🔴 嚴重問題(共 2 條)…', [
|
||||||
|
{ path: 'src/a.js', new_position: 42, body: '…' },
|
||||||
|
]);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="gitealistreviews"></a>
|
||||||
|
### gitea.listReviews
|
||||||
|
|
||||||
|
列出 PR 全部 review(自動分頁);步驟 8 據此逐一取出行內留言嘗試解決。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const reviews = await gitea.listReviews(ctx);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="gitealistreviewcomments"></a>
|
||||||
|
### gitea.listReviewComments
|
||||||
|
|
||||||
|
列出指定 review 底下的全部行內留言(單次呼叫、未分頁)。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const comments = await gitea.listReviewComments(ctx, reviews[0].id);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="giteatryresolvereviewcomment"></a>
|
||||||
|
### gitea.tryResolveReviewComment
|
||||||
|
|
||||||
|
盡力將行內留言標記為已解決;resolve endpoint 依 Gitea 版本不一定存在(需人工確認),任何失敗一律回 `false` 不丟錯,呼叫端第一次失敗即停止嘗試。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const ok = await gitea.tryResolveReviewComment(ctx, reviewId, commentId);
|
||||||
|
if (!ok) { /* 版本不支援 → 記 WRN 後放棄後續 resolve */ }
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="agentsdetecttool"></a>
|
||||||
|
### agents.detectTool
|
||||||
|
|
||||||
|
依 antigravity → codex → claude 優先序,以 `<tool> --version`(30 秒逾時)偵測可用工具,第一個成功者中選並附版本字串;全部不可用回 `null`(主流程記 ERR 失敗收場)。antigravity 的非互動參數尚未驗證(需人工確認)。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const agents = require('./src/lib/agents');
|
||||||
|
const tool = agents.detectTool(); // { name: 'codex', version: 'codex-cli 0.144.5', ... } | null
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="agentsrunagent"></a>
|
||||||
|
### agents.runAgent
|
||||||
|
|
||||||
|
以非互動模式執行一次 sub agent:提示從 stdin 餵入,codex 改讀 `--output-last-message` 暫存檔取最終回覆。永不 reject——逾時、非零退出碼都以 `{ ok: false }` resolve,由呼叫端降級。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const res = await agents.runAgent(tool, { model: '', prompt: '…', cwd, timeoutMs: 600000 });
|
||||||
|
const data = res.ok ? agents.extractJson(res.output) : null;
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="agentsextractjson"></a>
|
||||||
|
### agents.extractJson
|
||||||
|
|
||||||
|
從 agent 自由文字回覆萃取 JSON:先剝 code fence、再以「陣列優先」的最大範圍切片嘗試 parse;失敗一律回 `null` 不丟例外。
|
||||||
|
|
||||||
|
```js
|
||||||
|
agents.extractJson('```json\n[{"a":1}]\n```'); // [{ a: 1 }]
|
||||||
|
agents.extractJson('雜訊 {"b":2} 雜訊'); // { b: 2 }
|
||||||
|
agents.extractJson('不是 JSON'); // null
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="rolesloadroles"></a>
|
||||||
|
### roles.loadRoles
|
||||||
|
|
||||||
|
載入目錄下全部 `*.md` 角色提示檔(依檔名排序),解析開頭 `---` 包夾的輕量 frontmatter(單行「鍵: 值」),回傳 `{ file, meta, body, raw }` 陣列。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { loadRoles } = require('./src/lib/roles');
|
||||||
|
const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'));
|
||||||
|
// roles[0].meta => { name: 'Assassin', side: 'attack', focus: 'security', ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="rolesattackersof"></a>
|
||||||
|
### roles.attackersOf
|
||||||
|
|
||||||
|
過濾出 `meta.side === 'attack'` 的攻擊方角色(現況 6 位:Assassin/Bard/Leo/Mage/Maya/Rogue),保留檔名排序、不改原陣列。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const attackers = attackersOf(roles); // 6 位攻擊方
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="rolesdefendersof"></a>
|
||||||
|
### roles.defendersOf
|
||||||
|
|
||||||
|
過濾出 `meta.side === 'defend'` 的防守方角色(現況 1 位:Paladin,focus: verdict)。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const defenders = defendersOf(roles); // [Paladin]
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="templatestoolcomment"></a>
|
||||||
|
### templates.toolComment
|
||||||
|
|
||||||
|
產生步驟 2 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 8 辨識。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const body = templates.toolComment({
|
||||||
|
toolName: 'codex', version: 'codex-cli 0.144.5', model: '',
|
||||||
|
sha: ctx.headSha, runNumber: ctx.runNumber,
|
||||||
|
runLink: `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="templatesdiffcomment"></a>
|
||||||
|
### templates.diffComment
|
||||||
|
|
||||||
|
產生步驟 3 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const body = templates.diffComment(diffRows, ignoredCount);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="templatesrolescomment"></a>
|
||||||
|
### templates.rolesComment
|
||||||
|
|
||||||
|
產生步驟 4/6 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const body = templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="templatesseverecommentbody"></a>
|
||||||
|
### templates.severeCommentBody
|
||||||
|
|
||||||
|
產生步驟 9 單條嚴重問題的留言內容(程式碼片段/問題/修改建議/建議寫法,結尾提示可回覆);降級為一般留言時以 `withLocation: true` 在內文標明檔案與行號。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const body = templates.severeCommentBody(finding, snippet);
|
||||||
|
const fallback = templates.severeCommentBody(finding, snippet, { withLocation: true });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="templatesseverereviewbody"></a>
|
||||||
|
### templates.severeReviewBody
|
||||||
|
|
||||||
|
產生步驟 9 嚴重問題 review 的總覽 body(標明總數,說明逐條掛行)。
|
||||||
|
|
||||||
|
```js
|
||||||
|
await gitea.createReview(ctx, templates.severeReviewBody(severe.length), comments);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="templatesotherscomment"></a>
|
||||||
|
### templates.othersComment
|
||||||
|
|
||||||
|
產生步驟 10 的警告+建議彙整表格留言(等級/審查員/檔案名稱/問題起訖行數/問題描述/修改建議),儲存格經防呆逸出。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const body = templates.othersComment(others); // others=非嚴重的保留問題
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="templatesissuebody"></a>
|
||||||
|
### templates.issueBody
|
||||||
|
|
||||||
|
產生建問題模式新 issue 的本文:PR 描述為主體(缺省以「(PR 無描述)」佔位),尾端附追溯引言標明來源 PR。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const body = templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="templatesissuefindingcomment"></a>
|
||||||
|
### templates.issueFindingComment
|
||||||
|
|
||||||
|
產生建問題模式單條問題的 issue 留言(固定模板:嚴重等級/位置起訖行數/問題描述/修改建議/建議寫法);issue 留言無法掛行,位置一律以內文標明。
|
||||||
|
|
||||||
|
```js
|
||||||
|
await gitea.createCommentOnIssue(ctx, issue.number, templates.issueFindingComment(finding));
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="templatesnothingtoreviewcomment"></a>
|
||||||
|
### templates.nothingToReviewComment
|
||||||
|
|
||||||
|
產生「無可審查變更」留言:套用 `.reviewignore` 後送審清單為空時取代變更摘要,宣告本回合視為審查通過。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const body = templates.nothingToReviewComment(ignoredCount);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewloadreviewignore"></a>
|
||||||
|
### review.loadReviewIgnore
|
||||||
|
|
||||||
|
讀取 repo 根目錄的 `.reviewignore`(每行一個路徑前綴、`#` 註解、空行略過);檔案不存在回空陣列。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const review = require('./src/lib/review');
|
||||||
|
const ignores = review.loadReviewIgnore(cwd); // ['.gitea/', 'README.md', ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewisignored"></a>
|
||||||
|
### review.isIgnored
|
||||||
|
|
||||||
|
判斷檔案是否忽略不送審:任何深度的 `node_modules/` 一律排除(內建保險),其餘依前綴比對。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const files = allFiles.filter((f) => !review.isIgnored(f, ignores));
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewcollectdiffrows"></a>
|
||||||
|
### review.collectDiffRows
|
||||||
|
|
||||||
|
為每個送審檔案取得 diff 並計算統計(行數/字元數/最後更新時間),套用單檔 16,000/總量 160,000 字元送審上限(超限記 WRN、不靜默截斷);`purpose` 先以「—」佔位。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const diffRows = review.collectDiffRows({ cwd, files, base, gitrepo });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewfillpurposes"></a>
|
||||||
|
### review.fillPurposes
|
||||||
|
|
||||||
|
以選定 AI 工具為每個送審檔案產生一行用途描述並就地寫回 `diffRows[].purpose`;失敗只記 WRN 保留「—」,不阻斷流程。
|
||||||
|
|
||||||
|
```js
|
||||||
|
await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewrunattackers"></a>
|
||||||
|
### review.runAttackers
|
||||||
|
|
||||||
|
步驟 5:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewrundefenders"></a>
|
||||||
|
### review.runDefenders
|
||||||
|
|
||||||
|
步驟 7:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewsortfindings"></a>
|
||||||
|
### review.sortFindings
|
||||||
|
|
||||||
|
就地排序:嚴重→警告→建議,再依檔案路徑、起始行遞增;供 findings 保存與步驟 9/10 分組留言使用。
|
||||||
|
|
||||||
|
```js
|
||||||
|
review.sortFindings(kept);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewappendexclusions"></a>
|
||||||
|
### review.appendExclusions
|
||||||
|
|
||||||
|
把防守方判定排除(誤判/重複)的問題附加到 `.gitea/ai-review/exclusions.json`(含各防守方理由與來源 PR 編號);既有檔案壞損或非陣列時不動原檔、記 WRN(需人工確認)。回傳是否有寫入,決定收尾是否一併 commit。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const changed = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewsortfindingsforissue"></a>
|
||||||
|
### review.sortFindingsForIssue
|
||||||
|
|
||||||
|
建問題模式的就地排序:檔案路徑→嚴重等級(嚴重→建議)→起始行,讓 issue 留言同檔集中、便於逐檔處理。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const sorted = [...kept];
|
||||||
|
review.sortFindingsForIssue(sorted);
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewselectlabels"></a>
|
||||||
|
### review.selectLabels
|
||||||
|
|
||||||
|
以 AI 依 PR 標題/描述與問題列表摘要,從存取庫可用標籤挑選子集合(白名單過濾幻覺名稱後轉標籤 id);無標籤或失敗一律回空陣列不阻斷。
|
||||||
|
|
||||||
|
```js
|
||||||
|
const labelIds = await review.selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewcreateissuewithfindings"></a>
|
||||||
|
### review.createIssueWithFindings
|
||||||
|
|
||||||
|
建問題模式主流程:AI 挑標籤 → 建立 issue(標題=PR 標題、本文=PR 描述+追溯)→ 問題依檔案→嚴重度排序逐條留言到 issue;建 issue 失敗記 ERR 回 `null` 不阻斷主流程。
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (ctx.createIssue && kept.length > 0) {
|
||||||
|
await review.createIssueWithFindings({ ctx, gitea, tool, model: ctx.model, cwd, findings: kept });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewresolveoldcomments"></a>
|
||||||
|
### review.resolveOldComments
|
||||||
|
|
||||||
|
步驟 8:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。
|
||||||
|
|
||||||
|
```js
|
||||||
|
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
|
||||||
|
```
|
||||||
|
|
||||||
|
<a id="reviewpostseverecomments"></a>
|
||||||
|
### review.postSevereComments
|
||||||
|
|
||||||
|
步驟 9:嚴重問題以單一 code review 逐條掛在對應程式碼行上(含問題區塊程式碼片段,最多 40 行);建立 review 失敗時降級為一般留言逐條發布並在內文標明位置。
|
||||||
|
|
||||||
|
```js
|
||||||
|
if (severe.length > 0) {
|
||||||
|
await review.postSevereComments({ ctx, gitea, severe, cwd });
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
Reference in New Issue
Block a user