fix(ci): 修复AI代码审查3个bug - 过滤生效/评论去重/异常退出码
CI Build & Deploy Pipeline / Build Staging API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 10s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 50s
AI Code Review / AI Code Review (pull_request) Successful in 58s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 49s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m33s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m27s

- Bug1: 过滤逻辑实际从diff中移除跳过的文件(lock/图片等),节省token
- Bug2: 评论去重接上,每次新审查先删除旧的AI评论,避免刷屏
- Bug3: 异常情况sys.exit(1)替代sys.exit(0),配合continue-on-error也能看到失败
This commit is contained in:
2026-07-17 01:01:23 +08:00
parent ac1ae892ac
commit cdb68636bf
+69 -74
View File
@@ -150,70 +150,34 @@ class GiteaClient:
def get_existing_review_comments(self, pr_number: int, marker: str) -> list:
"""
获取 PR 上已有的审查评论(带标识),用于后续更新或删除旧评论
获取 PR 上已有的 AI 审查评论 ID 列表(带标识 marker)
"""
url = self._api_url(f"issues/{pr_number}/comments")
resp = self.session.get(url, timeout=GITEA_TIMEOUT)
if resp.status_code != 200:
logger.warning(f"获取评论列表失败: HTTP {resp.status_code}")
return []
comments = resp.json()
return [c for c in comments if marker in c.get("body", "")]
review_comment_ids = []
for c in comments:
body = c.get("body", "")
if marker in body:
review_comment_ids.append(c.get("id"))
logger.info(f"找到 {len(review_comment_ids)} 条旧的 AI 审查评论")
return review_comment_ids
def delete_pr_comment(self, pr_number: int, comment_id: int) -> bool:
"""
删除 PR 上的指定评论。
"""
url = self._api_url(f"issues/comments/{comment_id}")
resp = self.session.delete(url, timeout=GITEA_TIMEOUT)
if resp.status_code not in (200, 204):
logger.warning(f"删除评论 {comment_id} 失败: HTTP {resp.status_code}")
return False
return True
# ============== LLM 调用 ==============
def build_review_prompt(diff_text: str, pr_number: int, file_list: list) -> str:
"""构建代码审查的 Prompt"""
file_names = [f.get("filename", "") for f in file_list] if file_list else []
files_summary = ", ".join(file_names[:10]) if file_names else "未知"
if len(file_names) > 10:
files_summary += f"{len(file_names)} 个文件"
prompt = f"""你是一位资深代码审查专家,请对以下 Pull Request 的代码变更进行严格审查。
**PR 信息:**
- PR 编号:#{pr_number}
- 修改文件:{files_summary}
**审查重点:**
1. **严重问题**:逻辑错误、潜在 Bug、安全漏洞、数据不一致、空指针、资源泄漏、并发问题等
2. **代码质量**:边界条件处理、错误处理是否完善、异常场景覆盖
3. **性能隐患**:明显的性能问题、低效算法、不必要的重复计算
4. **最佳实践**:代码规范、可读性、可维护性、命名是否清晰
**审查原则:**
- 只针对变更的代码(diff)进行审查,不要审查未改动的代码
- 严重问题必须指出具体文件名和大致行号(根据 diff 中的行号推断)
- 给出明确、可操作的建议,不要空泛
- 如果代码质量很好、没有明显问题,也请如实说明
- 用中文回复
**输出格式要求(严格遵守,不要输出格式以外的内容):**
## 代码审查结果 - PR #{pr_number}
### ⚠️ 问题(N个需要修改)
1. **文件名 第X行**:问题描述(说明原因和可能的影响)
2. **文件名 第X行**:问题描述
### 💡 建议(N个可选)
1. 建议描述(可选优化、代码风格等)
---
✅ 格式检查通过 | ❌ 逻辑审查需修改 | ⚠️ 建议关注性能
**说明:** 底部的三个状态标签,根据审查结果勾选或取消对应标记(用 ✅/❌/⚠️ 表示):
- 格式检查:代码格式、命名规范等是否达标
- 逻辑审查:是否存在必须修改的逻辑问题
- 性能:是否存在需要关注的性能问题
**以下是代码 diff 内容:**
```diff
{diff_text}
```
"""
return prompt
def call_llm_openai(
@@ -509,9 +473,7 @@ def main():
if missing:
logger.error(f"缺少必要配置: {', '.join(missing)}")
# 审查失败不阻断 CI,返回 0
logger.info("审查脚本因配置缺失而跳过,退出码 0")
sys.exit(0)
sys.exit(1)
logger.info(f"开始审查 PR #{pr_number},仓库: {repo_name}")
logger.info(f"Gitea: {gitea_url}")
@@ -527,17 +489,45 @@ def main():
file_list = gitea.get_pr_files(pr_number)
except Exception as e:
logger.error(f"获取 PR 信息失败: {e}")
logger.info("审查脚本异常退出,退出码 0(不阻断 CI)")
sys.exit(0)
sys.exit(1)
# 3. 过滤掉不需要审查的文件(如 lock 文件、生成的文件等)
# 3. 过滤掉不需要审查的文件(如 lock 文件、生成的文件、二进制文件等)
skip_extensions = (".lock", ".sum", ".min.js", ".min.css", ".map", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".woff", ".woff2", ".ttf", ".eot")
skipped_files = []
if file_list:
skipped = [f.get("filename") for f in file_list
if f.get("filename", "").endswith(skip_extensions)
or f.get("status") == "removed"]
if skipped:
logger.info(f"跳过 {len(skipped)} 个非文本/已删除文件: {', '.join(skipped[:5])}...")
skipped_files = [f.get("filename") for f in file_list
if f.get("filename", "").endswith(skip_extensions)
or f.get("status") == "removed"]
if skipped_files:
logger.info(f"跳过 {len(skipped_files)} 个非文本/已删除文件: {', '.join(skipped_files[:5])}...")
# 实际从 diff 中移除跳过的文件(按文件边界切割)
if skipped_files:
diff_lines = diff_text.split("\n")
filtered_lines = []
current_file = None
skip_current = False
i = 0
while i < len(diff_lines):
line = diff_lines[i]
# 检测新文件开始: diff --git a/xxx b/xxx
if line.startswith("diff --git "):
# 提取文件名
parts = line.split(" ")
if len(parts) >= 4:
# b/ 后面的是目标文件名
current_file = parts[3][2:] if parts[3].startswith("b/") else parts[3]
skip_current = any(current_file == sf for sf in skipped_files) or any(
current_file.endswith(ext) for ext in skip_extensions
)
else:
skip_current = False
if not skip_current:
filtered_lines.append(line)
i += 1
original_len = len(diff_text)
diff_text = "\n".join(filtered_lines)
logger.info(f"Diff 过滤后: {original_len} -> {len(diff_text)} 字符 (减少 {original_len - len(diff_text)})")
# 4. 截断过大的 diff
diff_text, was_truncated = truncate_diff(diff_text, MAX_DIFF_CHARS)
@@ -561,9 +551,8 @@ def main():
)
if not review_result:
logger.error("LLM 审查失败,跳过发布评论")
logger.info("审查脚本异常退出,退出码 0(不阻断 CI)")
sys.exit(0)
logger.error("LLM 审查失败")
sys.exit(1)
# 7. 加上审查时间和标识(便于识别是自动审查)
from datetime import datetime
@@ -586,14 +575,22 @@ def main():
logger.info(f"... 共 {len(review_result.split(chr(10)))}")
logger.info("=" * 60)
# 9. 发布评论
# 9. 发布评论(先删除旧的审查评论,避免刷屏)
if args.dry_run:
logger.info("--dry-run 模式,跳过发布评论")
print(full_comment)
else:
# 去重:删除之前的 AI 审查评论
old_comments = gitea.get_existing_review_comments(pr_number, marker)
if old_comments:
logger.info(f"找到 {len(old_comments)} 条旧的 AI 审查评论,先删除")
for cid in old_comments:
gitea.delete_pr_comment(pr_number, cid)
# 发布新评论
success = gitea.post_pr_comment(pr_number, full_comment)
if not success:
logger.warning("评论发布失败,但不影响 CI 通过")
logger.error("评论发布失败")
sys.exit(1)
# 10. 判断是否有严重问题(可选阻断)
# 目前只做建议,不阻断合并,始终返回 0
@@ -606,9 +603,7 @@ def main():
except Exception as e:
logger.exception(f"审查脚本发生未预期的异常: {e}")
# 任何异常都不阻断 CI
logger.info("审查脚本异常退出,退出码 0(不阻断 CI)")
sys.exit(0)
sys.exit(1)
if __name__ == "__main__":