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
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:
+69
-74
@@ -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__":
|
||||
|
||||
Reference in New Issue
Block a user