Compare commits
20 Commits
v0.0.4-beta.3
...
v0.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
| f241f70898 | |||
| 7186098edf | |||
| 46da713fa7 | |||
| 515ccb0509 | |||
| 69e3b33558 | |||
| c70a818986 | |||
| 684c35bc00 | |||
| 93c602b86a | |||
| b397b76a7a | |||
| c5c3f1d7e1 | |||
| 12980d6ca4 | |||
| aa8b3ae89a | |||
| 1ad87ac4a4 | |||
| fb5c28114d | |||
| c871a27c9a | |||
| 4492fcbdd6 | |||
| 3338a518fe | |||
| 098d4aea97 | |||
| 850b2d770e | |||
| 5478918e25 |
@@ -0,0 +1,14 @@
|
|||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
When the task is to triage review findings, follow this workflow:
|
||||||
|
|
||||||
|
1. Merge all findings into one list.
|
||||||
|
2. Remove duplicates.
|
||||||
|
3. Sort by severity: `critical` -> `warning` -> `info`.
|
||||||
|
4. Renumber from 1 after sorting.
|
||||||
|
5. Fix real issues with the smallest safe change.
|
||||||
|
6. Add false positives to `.gitea/ai-review/exclusions.json`.
|
||||||
|
7. Add or update tests when behavior changes.
|
||||||
|
8. Re-check the issue after each fix.
|
||||||
|
|
||||||
|
Use the repo-local `triage-findings` skill for the same workflow when running in Codex.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: triage-findings
|
||||||
|
description: Triage findings, fix real issues, and exclude false positives.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Merge all findings.
|
||||||
|
2. Sort by severity:
|
||||||
|
- critical
|
||||||
|
- warning
|
||||||
|
- info
|
||||||
|
3. Renumber from 1.
|
||||||
|
4. Fix real issues.
|
||||||
|
5. Put false positives into `.gitea/ai-review/exclusions.json`.
|
||||||
|
6. Add tests when behavior changes.
|
||||||
|
|
||||||
|
## Output Rules
|
||||||
|
|
||||||
|
- Keep the final list short.
|
||||||
|
- Keep numbering contiguous.
|
||||||
|
- Preserve file path, location, and fix.
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: triage-findings
|
||||||
|
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
## When To Use
|
||||||
|
|
||||||
|
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
|
||||||
|
It is also used when some findings are false positives and should be moved into the exclusions list.
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Collect all findings into one list.
|
||||||
|
2. Merge duplicates into a single finding when they describe the same issue.
|
||||||
|
3. Sort the final list by severity:
|
||||||
|
- critical
|
||||||
|
- warning
|
||||||
|
- info
|
||||||
|
4. Renumber the sorted list from 1 upward.
|
||||||
|
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
|
||||||
|
6. If a finding is a false positive, do not keep it in the final list.
|
||||||
|
7. Add false positives to the exclusions list using the existing schema in the repo or task context.
|
||||||
|
|
||||||
|
## Resolution Flow
|
||||||
|
|
||||||
|
After the list is merged and ordered, resolve the remaining findings one by one.
|
||||||
|
|
||||||
|
1. Start from the highest severity item.
|
||||||
|
2. Identify the root cause in the relevant file or context.
|
||||||
|
3. Apply the smallest safe change that fixes the issue.
|
||||||
|
4. Add or update tests when behavior changes.
|
||||||
|
5. Re-check the issue after the change.
|
||||||
|
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
|
||||||
|
7. Continue until the list is either fixed or explicitly excluded.
|
||||||
|
|
||||||
|
## Output Rules
|
||||||
|
|
||||||
|
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
|
||||||
|
- Keep numbering contiguous after filtering and merging.
|
||||||
|
- Preserve useful details like file path, location, and suggested fix.
|
||||||
|
- Keep exclusions entries minimal and consistent with the project schema.
|
||||||
|
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
interface:
|
||||||
|
display_name: "Triage Findings"
|
||||||
|
short_description: "Triage, sort, fix, and exclude review findings"
|
||||||
|
default_prompt: "Use $triage-findings to merge review findings, sort and renumber them by severity, resolve real issues one by one, and add false positives to exclusions."
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
---
|
||||||
|
name: triage-findings
|
||||||
|
description: Triage findings, fix real issues, and exclude false positives.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
## Use
|
||||||
|
|
||||||
|
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
|
||||||
|
|
||||||
|
## Workflow
|
||||||
|
|
||||||
|
1. Merge all findings.
|
||||||
|
2. Sort by severity:
|
||||||
|
- critical
|
||||||
|
- warning
|
||||||
|
- info
|
||||||
|
3. Renumber from 1.
|
||||||
|
4. Fix real issues.
|
||||||
|
5. Put false positives into `.gitea/ai-review/exclusions.json`.
|
||||||
|
6. Add tests when behavior changes.
|
||||||
|
|
||||||
|
## Output Rules
|
||||||
|
|
||||||
|
- Keep the final list short.
|
||||||
|
- Keep numbering contiguous.
|
||||||
|
- Preserve file path, location, and fix.
|
||||||
@@ -279,5 +279,33 @@
|
|||||||
{
|
{
|
||||||
"location": "app/json.js",
|
"location": "app/json.js",
|
||||||
"suggestion": "validateJSONArrayFile 只在 JSON 格式錯誤時才啟動 AI 修正,屬例外路徑;再加上檔案大小限制後,並不存在實際的無上限讀檔或資源消耗問題。"
|
"suggestion": "validateJSONArrayFile 只在 JSON 格式錯誤時才啟動 AI 修正,屬例外路徑;再加上檔案大小限制後,並不存在實際的無上限讀檔或資源消耗問題。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/json.test.js",
|
||||||
|
"suggestion": "邊界值測試已存在,`MAX_JSON_BYTES` 等於上限時可正常讀取,這不是未解決問題。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/gitea.test.js:64",
|
||||||
|
"suggestion": "`describe` 已改為同步 callback,`async` 不再出現在這個區塊。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/git.test.js:13",
|
||||||
|
"suggestion": "`makeTmpWorkspace` 已直接使用 `app/git.js` 匯出的 `SYNC_PATHS`,不再維護重複清單。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/gitea.js:32",
|
||||||
|
"suggestion": "`filterDiff` 內層縮排已符合專案的 2-space 風格,這是誤報。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/json.test.js:76",
|
||||||
|
"suggestion": "1MB 上限下的 JSON 讀取不需要改成串流解析;現有實作已先做大小檢查,這個建議屬過度設計。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/json.test.js:7",
|
||||||
|
"suggestion": "檔案大小限制已在 `readJSONText` / `validateJSONArrayFile` 中實作,這不是額外缺陷。"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"location": "app/json.test.js:10",
|
||||||
|
"suggestion": "`MAX_JSON_BYTES` 是 `json.js` 的內部限制常數,不需要匯出成公開 API。"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,16 +1 @@
|
|||||||
[
|
[]
|
||||||
{
|
|
||||||
"level": "warning",
|
|
||||||
"role": "Maya",
|
|
||||||
"location": "app/json.test.js",
|
|
||||||
"suggestion": "在 `readJSONText` 相關的測試中,除了測試檔案過大的情況,也建議增加一個測試案例,驗證當檔案大小剛好等於 `MAX_JSON_BYTES` 時,檔案能夠被成功讀取且不會拋出錯誤。這能確保邊界條件的處理是正確的。",
|
|
||||||
"is_new": true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"level": "info",
|
|
||||||
"role": "Maya",
|
|
||||||
"location": "app/json.test.js",
|
|
||||||
"suggestion": "在 `validateJSONArrayFile` 函數中,寫入修復後的 JSON 時,有判斷是否需要添加換行符 (`repaired.endsWith('\\n') ? repaired : `${repaired}\\n``)。目前的測試案例只驗證了最終結果包含換行符,但沒有明確測試兩種情況:當 AI 回傳的內容已經包含換行符時,以及不包含換行符時,都能正確處理。建議增加一個測試案例來覆蓋這兩種情況。",
|
|
||||||
"is_new": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
Use the triage-finding workflow for review issue lists:
|
||||||
|
|
||||||
|
1. Merge findings into one list.
|
||||||
|
2. Remove duplicates.
|
||||||
|
3. Sort by severity: `critical` -> `warning` -> `info`.
|
||||||
|
4. Renumber from 1.
|
||||||
|
5. Fix real issues with the smallest safe change.
|
||||||
|
6. Put false positives into `.gitea/ai-review/exclusions.json`.
|
||||||
|
7. Add or update tests when behavior changes.
|
||||||
|
8. Re-check after each fix.
|
||||||
|
|
||||||
|
The full reusable skill lives in `.claude/skills/triage-findings/SKILL.md`.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
Use the triage-finding workflow for review issue lists:
|
||||||
|
|
||||||
|
1. Merge findings into one list.
|
||||||
|
2. Remove duplicates.
|
||||||
|
3. Sort by severity: `critical` -> `warning` -> `info`.
|
||||||
|
4. Renumber from 1.
|
||||||
|
5. Fix real issues with the smallest safe change.
|
||||||
|
6. Put false positives into `.gitea/ai-review/exclusions.json`.
|
||||||
|
7. Add or update tests when behavior changes.
|
||||||
|
8. Re-check after each fix.
|
||||||
|
|
||||||
|
The reusable skill lives in `.gemini/skills/triage-findings/SKILL.md`.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
When the task is to triage review findings, follow this workflow:
|
||||||
|
|
||||||
|
1. Merge all findings into one list.
|
||||||
|
2. Remove duplicates.
|
||||||
|
3. Sort by severity: `critical` -> `warning` -> `info`.
|
||||||
|
4. Renumber from 1 after sorting.
|
||||||
|
5. Fix real issues with the smallest safe change.
|
||||||
|
6. Add false positives to `.gitea/ai-review/exclusions.json`.
|
||||||
|
7. Add or update tests when behavior changes.
|
||||||
|
8. Re-check the issue after each fix.
|
||||||
|
|
||||||
|
Use the repo-local `triage-findings` skill for the same workflow when running in Codex.
|
||||||
|
|
||||||
|
Trigger it with `/triage-findings`.
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Triage Findings
|
||||||
|
|
||||||
|
Use the triage-finding workflow for review issue lists:
|
||||||
|
|
||||||
|
1. Merge findings into one list.
|
||||||
|
2. Remove duplicates.
|
||||||
|
3. Sort by severity: `critical` -> `warning` -> `info`.
|
||||||
|
4. Renumber from 1.
|
||||||
|
5. Fix real issues with the smallest safe change.
|
||||||
|
6. Put false positives into `.gitea/ai-review/exclusions.json`.
|
||||||
|
7. Add or update tests when behavior changes.
|
||||||
|
8. Re-check after each fix.
|
||||||
|
|
||||||
|
The reusable skill lives in `.gemini/skills/triage-findings/SKILL.md`.
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
5. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request
|
5. 從PR問題表格中取出所有舊問題,依照等級排序後 Comment 到 Push Request
|
||||||
6. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request
|
6. 從PR問題表格中取出所有新問題,排除嚴重等級的問題後 Comment 到 Push Request
|
||||||
7. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request
|
7. 從PR問題表格中取出所有新問題,將每個嚴重等級的問題 Comment 到 Push Request
|
||||||
8. Commit 問題檔案
|
8. Commit 問題檔案,將 workspace 中實際存在的同步檔覆蓋到記憶區;workspace 沒有的同步檔就略過,不會刪除記憶區既有內容
|
||||||
9. 如果PR問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)
|
9. 如果PR問題表格中有嚴重問題,則不要讓 workflow 執行成功(exit 1)
|
||||||
|
|
||||||
# 設計
|
# 設計
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行
|
4. 盡量將應用程式放在 ./app,修改 entrypoint.sh 與 Dockerfile 讓程式可以正常運行
|
||||||
5. 將提示詞放到 ./app/prompts 內供程式讀取
|
5. 將提示詞放到 ./app/prompts 內供程式讀取
|
||||||
6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1
|
6. API Key 支援逗號分隔傳入多個,隨機順序各嘗試一次,全部失敗則 exit 1
|
||||||
7. 讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼
|
7. 讀取 Git Diff 時排除 `.gitea/`、`.amazonq/`、`.claude/`、`.codex/`、`.gemini/`、`.github/` 資料夾,以及 `CLAUDE.md`、`GEMINI.md`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼
|
||||||
8. 階段七驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 `[]`
|
8. 階段七驗證 `findings.json` 與 `exclusions.json` 是否為合法 JSON 格式,格式錯誤時先嘗試透過 AI 修正內容,再重新驗證;修正後仍不合法才 exit 1;之後才檢查檔案是否存在,不存在則建立並寫入 `[]`
|
||||||
9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量
|
9. 傳給 AI 的 findings 只保留必要欄位(level、role、location、suggestion),排除 `is_new` 等內部欄位;system prompt 精簡為指令核心;exclusions hint 只傳 location 與 suggestion,減少 token 用量
|
||||||
|
|
||||||
@@ -199,3 +199,32 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
issues: write
|
issues: write
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Skill:Triage Findings
|
||||||
|
|
||||||
|
這份 skill 用來處理 review 問題清單。
|
||||||
|
|
||||||
|
### 規則
|
||||||
|
|
||||||
|
1. 合併問題。
|
||||||
|
2. 依嚴重度排序:`critical` -> `warning` -> `info`。
|
||||||
|
3. 重新編號。
|
||||||
|
4. 真問題就修。
|
||||||
|
5. 誤判就加到 `.gitea/ai-review/exclusions.json`。
|
||||||
|
6. 有變更就補測試。
|
||||||
|
|
||||||
|
### 使用方式
|
||||||
|
|
||||||
|
Codex:`$triage-findings 問題原始檔(文字或截圖)`
|
||||||
|
Copilot:`/triage-findings 問題原始檔(文字或截圖)`
|
||||||
|
Claude:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
||||||
|
Gemini:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
||||||
|
Amazon Q:直接輸入 `triage-findings 問題原始檔(文字或截圖)`
|
||||||
|
|
||||||
|
### 適用情境
|
||||||
|
|
||||||
|
`triage-findings 問題原始檔(文字或截圖)` 用在 review 問題整併、排序、修正、排除誤判。
|
||||||
|
|
||||||
|
### 版本包含
|
||||||
|
|
||||||
|
提交時一併包含 `triage-findings` skill 與各平台入口檔;已存在檔案一律覆蓋,同步到最新內容;若 workspace 沒有某個同步檔,記憶區會保留原檔,不做刪除。
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
- 已驗收:`code-review` job 的 log 已完整出現 `Step1` 到 `Step8`,並以 `Pipeline 完成` 結束。
|
- 已驗收:`code-review` job 的 log 已完整出現 `Step1` 到 `Step8`,並以 `Pipeline 完成` 結束。
|
||||||
|
|
||||||
## 階段二:Git Diff 排除 .gitea/ 資料夾
|
## 階段二:Git Diff 排除 .gitea/ 資料夾
|
||||||
- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,避免 AI 分析 workflow 設定等非業務程式碼。
|
- 目標:讀取 Git Diff 時排除 `.gitea/` 資料夾內的所有檔案,以及 `.amazonq/`、`.claude/`、`.codex/`、`.gemini/`、`.github/`、`CLAUDE.md`、`GEMINI.md`、`TODO.md`、`README.md`,避免 AI 分析 workflow 設定、skill 入口與文件等非業務程式碼。
|
||||||
- 驗收:PR 中有 `.gitea/` 路徑的變更時,diff 內容不包含該路徑的區塊,AI 分析結果不含 `.gitea/` 相關問題。
|
- 驗收:PR 中有上述路徑或檔案的變更時,diff 內容不包含該區塊,AI 分析結果不含這些路徑相關問題。
|
||||||
- 已驗收:`app/gitea.js` 已在取得 diff 時過濾 `.gitea/` 區塊,且相關單元測試已覆蓋。
|
- 已驗收:`app/gitea.js` 已在取得 diff 時過濾 `.gitea/` 區塊,且相關單元測試已覆蓋。
|
||||||
|
|
||||||
## 階段三:Findings 產生與合併
|
## 階段三:Findings 產生與合併
|
||||||
@@ -38,9 +38,9 @@
|
|||||||
- 已驗收:log 已明確顯示 `.gitea/ai-review/findings.json` 與 `.gitea/ai-review/exclusions.json` 都是 `JSON 格式正確`。
|
- 已驗收:log 已明確顯示 `.gitea/ai-review/findings.json` 與 `.gitea/ai-review/exclusions.json` 都是 `JSON 格式正確`。
|
||||||
|
|
||||||
## 階段八:記憶區 commit/push 與錯誤處理
|
## 階段八:記憶區 commit/push 與錯誤處理
|
||||||
- 目標:記憶區能成功 commit/push,錯誤時有明確 log,流程結束有總結訊息。
|
- 目標:記憶區能成功 commit/push,且一併包含 `triage-findings` skill 與各平台入口檔;skill 檔案已存在時一律以來源覆蓋,workspace 沒有的同步檔則保留記憶區既有內容,不做刪除;錯誤時有明確 log,流程結束有總結訊息。
|
||||||
- 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,錯誤時有「Runner failed: ...」等明確錯誤說明。
|
- 驗收:log 有「persisted findings」、「commit=...」、「push=...」等訊息,且能看出 skill 相關檔案已一併提交並被來源覆蓋;當 workspace 缺少某個同步檔時,記憶區中的對應檔案不會被刪除;錯誤時有「Runner failed: ...」等明確錯誤說明。
|
||||||
- 已驗收:log 已出現 `persisted findings commit=79506eb push=整理程式碼`,代表 commit/push 成功。
|
- 已驗收:log 已出現 `persisted findings commit=79506eb push=整理程式碼`,代表 commit/push 成功;本次已補上「來源覆蓋、缺檔不刪除」的同步規則,相關單元測試也已覆蓋。
|
||||||
|
|
||||||
## 階段九:阻擋嚴重問題 PR(第 8 點)
|
## 階段九:阻擋嚴重問題 PR(第 8 點)
|
||||||
- 目標:如果 PR 問題表格中有嚴重(critical)問題,workflow 需直接 exit 1,不讓流程成功。
|
- 目標:如果 PR 問題表格中有嚴重(critical)問題,workflow 需直接 exit 1,不讓流程成功。
|
||||||
|
|||||||
+27
-6
@@ -4,6 +4,16 @@ import path from 'path';
|
|||||||
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js';
|
import { GITEA_SERVER_URL, GITEA_REPOSITORY, GITEA_TOKEN, PR_HEAD_BRANCH, FINDINGS_PATH } from './config.js';
|
||||||
|
|
||||||
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
|
const remoteUrl = `${GITEA_SERVER_URL.replace(/\/$/, '')}/${GITEA_REPOSITORY}.git`;
|
||||||
|
export const SYNC_PATHS = [
|
||||||
|
FINDINGS_PATH,
|
||||||
|
'.amazonq/rules/triage-findings.md',
|
||||||
|
'.claude/skills/triage-findings/SKILL.md',
|
||||||
|
'.gemini/skills/triage-findings/SKILL.md',
|
||||||
|
'.github/copilot-instructions.md',
|
||||||
|
'.github/skills/triage-findings/SKILL.md',
|
||||||
|
'CLAUDE.md',
|
||||||
|
'GEMINI.md',
|
||||||
|
];
|
||||||
|
|
||||||
function makeRunner(spawn) {
|
function makeRunner(spawn) {
|
||||||
return function run(args, cwd, env) {
|
return function run(args, cwd, env) {
|
||||||
@@ -55,16 +65,27 @@ export async function commitAndPush(workspace, repoDir, _spawnSync = spawnSync)
|
|||||||
run(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir);
|
run(['config', 'user.email', 'ai-review[bot]@gitea'], repoDir);
|
||||||
run(['config', 'user.name', 'AI Review Bot'], repoDir);
|
run(['config', 'user.name', 'AI Review Bot'], repoDir);
|
||||||
|
|
||||||
const srcFindings = path.join(workspace, FINDINGS_PATH);
|
const existingSyncPaths = [];
|
||||||
const destFindings = path.join(repoDir, FINDINGS_PATH);
|
|
||||||
fs.mkdirSync(path.dirname(destFindings), { recursive: true });
|
|
||||||
fs.copyFileSync(srcFindings, destFindings);
|
|
||||||
|
|
||||||
run(['add', FINDINGS_PATH], repoDir);
|
// Copy action skill files into the target repo. Existing files are overwritten;
|
||||||
|
// missing source files are ignored so we do not delete target repo content.
|
||||||
|
for (const relPath of SYNC_PATHS) {
|
||||||
|
const src = path.join(workspace, relPath);
|
||||||
|
const dest = path.join(repoDir, relPath);
|
||||||
|
if (fs.existsSync(src)) {
|
||||||
|
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
||||||
|
fs.copyFileSync(src, dest);
|
||||||
|
existingSyncPaths.push(relPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingSyncPaths.length > 0) {
|
||||||
|
run(['add', ...existingSyncPaths], repoDir);
|
||||||
|
}
|
||||||
|
|
||||||
const status = run(['status', '--porcelain'], repoDir);
|
const status = run(['status', '--porcelain'], repoDir);
|
||||||
if (!status) {
|
if (!status) {
|
||||||
console.log(' findings.json 無變更,跳過 commit');
|
console.log(' sync files 無變更,跳過 commit');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+46
-5
@@ -3,17 +3,18 @@ import assert from 'node:assert/strict';
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { commitAndPush, cloneRepo } from './git.js';
|
import { commitAndPush, cloneRepo, SYNC_PATHS } from './git.js';
|
||||||
|
|
||||||
// --- helpers ---
|
// --- helpers ---
|
||||||
function makeTmpWorkspace() {
|
function makeTmpWorkspace() {
|
||||||
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'git-test-'));
|
const ws = fs.mkdtempSync(path.join(os.tmpdir(), 'git-test-'));
|
||||||
// Pre-create repo dir so clone branch is skipped
|
// Pre-create repo dir so clone branch is skipped
|
||||||
fs.mkdirSync(path.join(ws, 'repo'), { recursive: true });
|
fs.mkdirSync(path.join(ws, 'repo'), { recursive: true });
|
||||||
// Create a findings.json to copy
|
for (const relPath of SYNC_PATHS) {
|
||||||
const findingsDir = path.join(ws, '.gitea/ai-review');
|
const fullPath = path.join(ws, relPath);
|
||||||
fs.mkdirSync(findingsDir, { recursive: true });
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
fs.writeFileSync(path.join(findingsDir, 'findings.json'), '[]');
|
fs.writeFileSync(fullPath, relPath);
|
||||||
|
}
|
||||||
return ws;
|
return ws;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +86,46 @@ describe('commitAndPush', () => {
|
|||||||
assert.equal(commitCalled, false, 'commit should not run when there are no changes');
|
assert.equal(commitCalled, false, 'commit should not run when there are no changes');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('adds skill and entry files together with findings', async () => {
|
||||||
|
const spawn = makeSpawn();
|
||||||
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn);
|
||||||
|
const addCall = spawn.calls.find(c => c.args[0] === 'add');
|
||||||
|
assert.ok(addCall, 'expected git add to run');
|
||||||
|
assert.ok(addCall.args.includes('.github/skills/triage-findings/SKILL.md'));
|
||||||
|
assert.ok(addCall.args.includes('.claude/skills/triage-findings/SKILL.md'));
|
||||||
|
assert.ok(addCall.args.includes('.gemini/skills/triage-findings/SKILL.md'));
|
||||||
|
assert.ok(addCall.args.includes('.github/copilot-instructions.md'));
|
||||||
|
assert.ok(addCall.args.includes('.amazonq/rules/triage-findings.md'));
|
||||||
|
assert.ok(addCall.args.includes('CLAUDE.md'));
|
||||||
|
assert.ok(addCall.args.includes('GEMINI.md'));
|
||||||
|
assert.ok(!addCall.args.includes('README.md'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps repo copies when the source sync file is missing', async () => {
|
||||||
|
const missingPath = path.join(workspace, '.amazonq/rules/triage-findings.md');
|
||||||
|
fs.rmSync(missingPath, { force: true });
|
||||||
|
const repoPath = path.join(workspace, 'repo', '.amazonq/rules/triage-findings.md');
|
||||||
|
fs.writeFileSync(repoPath, 'stale');
|
||||||
|
const spawn = makeSpawn();
|
||||||
|
|
||||||
|
await commitAndPush(workspace, path.join(workspace, 'repo'), spawn);
|
||||||
|
|
||||||
|
const rmCall = spawn.calls.find(c => c.args[0] === 'rm');
|
||||||
|
assert.equal(rmCall, undefined, 'git rm should not run for missing source files');
|
||||||
|
assert.equal(fs.readFileSync(repoPath, 'utf8'), 'stale');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('overwrites existing repo copies with workspace files', async () => {
|
||||||
|
const repoDir = path.join(workspace, 'repo');
|
||||||
|
fs.writeFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'stale');
|
||||||
|
fs.writeFileSync(path.join(repoDir, 'CLAUDE.md'), 'stale');
|
||||||
|
|
||||||
|
await commitAndPush(workspace, repoDir, makeSpawn());
|
||||||
|
|
||||||
|
assert.equal(fs.readFileSync(path.join(repoDir, '.github/skills/triage-findings/SKILL.md'), 'utf8'), '.github/skills/triage-findings/SKILL.md');
|
||||||
|
assert.equal(fs.readFileSync(path.join(repoDir, 'CLAUDE.md'), 'utf8'), 'CLAUDE.md');
|
||||||
|
});
|
||||||
|
|
||||||
it('does not throw when git command fails', async () => {
|
it('does not throw when git command fails', async () => {
|
||||||
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
const failSpawn = () => ({ status: 1, stdout: '', stderr: 'fatal: error', error: null });
|
||||||
await assert.doesNotReject(() => commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn));
|
await assert.doesNotReject(() => commitAndPush(workspace, path.join(workspace, 'repo'), failSpawn));
|
||||||
|
|||||||
+17
-2
@@ -11,7 +11,18 @@ const api = (path) => `${GITEA_SERVER_URL.replace(/\/$/, '')}/api/v1${path}`;
|
|||||||
*/
|
*/
|
||||||
export async function getPRDiff() {
|
export async function getPRDiff() {
|
||||||
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent });
|
const resp = await axios.get(api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}.diff`), { headers: headers(), timeout: 60000, httpsAgent });
|
||||||
return filterDiff(resp.data, ['.gitea/']);
|
return filterDiff(resp.data, [
|
||||||
|
'.amazonq/',
|
||||||
|
'.claude/',
|
||||||
|
'.codex/',
|
||||||
|
'.gemini/',
|
||||||
|
'.gitea/',
|
||||||
|
'.github/',
|
||||||
|
'CLAUDE.md',
|
||||||
|
'GEMINI.md',
|
||||||
|
'README.md',
|
||||||
|
'TODO.md',
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -20,7 +31,11 @@ export async function getPRDiff() {
|
|||||||
*/
|
*/
|
||||||
export function filterDiff(diff, excludePrefixes) {
|
export function filterDiff(diff, excludePrefixes) {
|
||||||
return diff.split(/(?=^diff --git )/m)
|
return diff.split(/(?=^diff --git )/m)
|
||||||
.filter(block => !excludePrefixes.some(p => block.startsWith(`diff --git a/${p}`)))
|
.filter(block => !excludePrefixes.some(p => {
|
||||||
|
const prefix = `diff --git a/${p}`;
|
||||||
|
const singleFile = `diff --git a/${p} b/${p}`;
|
||||||
|
return block.startsWith(prefix) || block.startsWith(singleFile);
|
||||||
|
}))
|
||||||
.join('');
|
.join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+14
-15
@@ -1,12 +1,11 @@
|
|||||||
import { describe, it, afterEach, mock } from 'node:test';
|
import { describe, it, afterEach, mock } from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
import { getPRDiff, filterDiff, postComment } from './gitea.js';
|
||||||
|
|
||||||
afterEach(() => mock.restoreAll());
|
afterEach(() => mock.restoreAll());
|
||||||
|
|
||||||
describe('gitea', async () => {
|
describe('gitea', () => {
|
||||||
const { getPRDiff, filterDiff, postComment } = await import('./gitea.js');
|
|
||||||
|
|
||||||
it('getPRDiff calls Gitea diff API with Authorization header', async () => {
|
it('getPRDiff calls Gitea diff API with Authorization header', async () => {
|
||||||
let capturedUrl, capturedOpts;
|
let capturedUrl, capturedOpts;
|
||||||
mock.method(axios, 'get', async (url, opts) => {
|
mock.method(axios, 'get', async (url, opts) => {
|
||||||
@@ -59,27 +58,27 @@ describe('gitea', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('filterDiff', async () => {
|
describe('filterDiff', () => {
|
||||||
const { filterDiff } = await import('./gitea.js');
|
|
||||||
|
|
||||||
const block = (file) => `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-old\n+new\n`;
|
const block = (file) => `diff --git a/${file} b/${file}\n--- a/${file}\n+++ b/${file}\n@@ -1 +1 @@\n-old\n+new\n`;
|
||||||
|
|
||||||
it('filters out .gitea/ blocks', () => {
|
it('filters out configured folder blocks', () => {
|
||||||
const diff = block('.gitea/workflows/review.yaml') + block('src/index.js');
|
const diff = block('.gitea/workflows/review.yaml') + block('.amazonq/rules/triage-findings.md') + block('src/index.js');
|
||||||
const result = filterDiff(diff, ['.gitea/']);
|
const result = filterDiff(diff, ['.gitea/', '.amazonq/']);
|
||||||
assert.ok(!result.includes('.gitea/'));
|
assert.ok(!result.includes('.gitea/'));
|
||||||
|
assert.ok(!result.includes('.amazonq/'));
|
||||||
assert.ok(result.includes('src/index.js'));
|
assert.ok(result.includes('src/index.js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not filter non-.gitea/ blocks', () => {
|
it('filters out configured top-level file blocks', () => {
|
||||||
const diff = block('src/index.js') + block('README.md');
|
const diff = block('README.md') + block('src/index.js');
|
||||||
const result = filterDiff(diff, ['.gitea/']);
|
const result = filterDiff(diff, ['README.md', 'TODO.md']);
|
||||||
assert.equal(result, diff);
|
assert.ok(!result.includes('README.md'));
|
||||||
|
assert.ok(result.includes('src/index.js'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns empty string when all blocks are excluded', () => {
|
it('returns empty string when all blocks are excluded', () => {
|
||||||
const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json');
|
const diff = block('.gitea/workflows/review.yaml') + block('.gitea/ai-review/findings.json') + block('CLAUDE.md');
|
||||||
const result = filterDiff(diff, ['.gitea/']);
|
const result = filterDiff(diff, ['.gitea/', 'CLAUDE.md']);
|
||||||
assert.equal(result, '');
|
assert.equal(result, '');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import path from 'path';
|
|||||||
import { stripCodeFence, repairJSONArrayWithAI, validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
|
import { stripCodeFence, repairJSONArrayWithAI, validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
|
||||||
|
|
||||||
describe('json helpers', () => {
|
describe('json helpers', () => {
|
||||||
|
const MAX_JSON_BYTES = 1024 * 1024;
|
||||||
let workspace;
|
let workspace;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -76,6 +77,16 @@ describe('json helpers', () => {
|
|||||||
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[]\n');
|
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[]\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reads a valid JSON file whose size equals the maximum limit', async () => {
|
||||||
|
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, `[]${' '.repeat(MAX_JSON_BYTES - 2)}`, 'utf8');
|
||||||
|
|
||||||
|
const result = await validateJSONArrayFile(fullPath, '.gitea/ai-review/findings.json');
|
||||||
|
|
||||||
|
assert.deepEqual(result, { exists: true, valid: true, repaired: false });
|
||||||
|
});
|
||||||
|
|
||||||
it('repairs invalid JSON using AI output and rewrites the file', async () => {
|
it('repairs invalid JSON using AI output and rewrites the file', async () => {
|
||||||
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
@@ -90,6 +101,20 @@ describe('json helpers', () => {
|
|||||||
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[{"fixed":true}]\n');
|
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[{"fixed":true}]\n');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('preserves a trailing newline returned by AI repair', async () => {
|
||||||
|
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||||
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
fs.writeFileSync(fullPath, '{broken', 'utf8');
|
||||||
|
|
||||||
|
const result = await validateJSONArrayFile(fullPath, '.gitea/ai-review/findings.json', async (_fullPath, _label, original) => {
|
||||||
|
assert.equal(original, '{broken');
|
||||||
|
return '[{"fixed":true}]\n';
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.deepEqual(result, { exists: true, valid: true, repaired: true });
|
||||||
|
assert.equal(fs.readFileSync(fullPath, 'utf8'), '[{"fixed":true}]\n');
|
||||||
|
});
|
||||||
|
|
||||||
it('throws when AI repair fails', async () => {
|
it('throws when AI repair fails', async () => {
|
||||||
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
const fullPath = path.join(workspace, '.gitea/ai-review/findings.json');
|
||||||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||||||
|
|||||||
Reference in New Issue
Block a user