From f6d0705eeb77b016cb2a8b66efcf36c10d9659d9 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Thu, 23 Jul 2026 09:50:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=94=A8develop=E7=89=88=E6=9C=AC?= =?UTF-8?q?=E7=9A=84pr=5Fauto=5Fscan.py=E8=A7=A3=E5=86=B3=E5=86=B2?= =?UTF-8?q?=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci/pr_auto_scan.py | 95 +++++++++++++++++++++++++++++++++++--- 1 file changed, 88 insertions(+), 7 deletions(-) diff --git a/scripts/ci/pr_auto_scan.py b/scripts/ci/pr_auto_scan.py index c4c5a3efb..f17753d27 100644 --- a/scripts/ci/pr_auto_scan.py +++ b/scripts/ci/pr_auto_scan.py @@ -2,10 +2,15 @@ """ PR自动扫描器:扫描所有open PR,对CI全绿的进行自动审批/合并 作为短作业模式的兜底机制,每5分钟运行一次 + +新增:AI审查联动 - AI代码审查发现严重问题时,不自动审批 """ import argparse import json +import os +import re +import sys import time import urllib.error import urllib.request @@ -108,7 +113,55 @@ def has_approval(token, repo, pr_number): return any(r.get("state") == "APPROVED" for r in reviews if isinstance(r, dict)) -def approve_pr(token, repo, pr_number): +def get_ai_review_result(token, repo, pr_number): + """ + 检查AI代码审查结果,返回 (has_critical, review_body) + has_critical: 是否有严重问题(需修改的问题 > 0) + review_body: 最新的AI审查评论文本 + """ + # AI审查评论标记 + AI_REVIEW_MARKER = "AI_CODE_REVIEW_AUTO_COMMENT" + + comments, code = api_request(token, repo, f"issues/{pr_number}/comments") + if code != 200: + return False, None + + # 找最新的AI审查评论 + ai_comments = [c for c in comments if isinstance(c, dict) and AI_REVIEW_MARKER in c.get("body", "")] + + if not ai_comments: + return False, None + + # 按时间排序,取最新的 + latest = max(ai_comments, key=lambda c: c.get("created_at", "")) + body = latest.get("body", "") + + # 解析严重问题数量 + # 匹配 "严重问题数量:X 个" 或 "需修改的问题(严重)" 下的列表 + critical_count = 0 + + # 方式1:直接匹配数字 + match = re.search(r"严重问题数量[::]\s*(\d+)\s*个", body) + if match: + critical_count = int(match.group(1)) + else: + # 方式2:数 "需修改的问题" 章节下的条目数 + critical_section = re.search( + r"###\s*[❌⚠️].*?(?:需修改|问题).*?\n(.*?)(?=\n###|\Z)", + body, + re.DOTALL, + ) + if critical_section: + section_text = critical_section.group(1) + # 数编号条目 1. 2. 3. + items = re.findall(r"^\d+\.\s+\*\*", section_text, re.MULTILINE) + critical_count = len(items) + + has_critical = critical_count > 0 + return has_critical, body + + +def approve_pr(token, repo, pr_number, reason="CI全绿,自动审批通过。"): """审批PR""" # 创建review data, code = api_request( @@ -116,7 +169,7 @@ def approve_pr(token, repo, pr_number): repo, f"pulls/{pr_number}/reviews", method="POST", - data={"event": "PENDING", "body": "CI全绿,自动审批通过。"}, + data={"event": "PENDING", "body": reason}, ) if code not in (200, 201): @@ -135,7 +188,7 @@ def approve_pr(token, repo, pr_number): repo, f"pulls/{pr_number}/reviews/{review_id}/events", method="POST", - data={"event": "APPROVED", "body": "CI全绿,自动审批通过。"}, + data={"event": "APPROVED", "body": reason}, ) if code2 in (200, 201): @@ -147,13 +200,25 @@ def approve_pr(token, repo, pr_number): repo, f"pulls/{pr_number}/reviews/{review_id}", method="POST", - data={"event": "APPROVED", "body": "CI全绿,自动审批通过。"}, + data={"event": "APPROVED", "body": reason}, ) if code3 in (200, 201): return True, "审批提交成功(备用端点)" return False, f"审批提交失败: HTTP {code2}/{code3}" +def add_pr_label(token, repo, pr_number, label): + """给PR添加标签""" + data, code = api_request( + token, + repo, + f"issues/{pr_number}/labels", + method="POST", + data={"labels": [label]}, + ) + return code in (200, 201) + + def merge_pr(token, repo, pr_number): """合并PR(squash merge)""" # 等待几秒让状态同步 @@ -198,6 +263,7 @@ def main(): parser.add_argument("--merge", action="store_true", help="执行自动合并") parser.add_argument("--dry-run", default="false", help="试运行模式") parser.add_argument("--max-prs", type=int, default=20, help="最多处理的PR数") + parser.add_argument("--skip-ai-review", action="store_true", help="跳过AI审查检查(强制审批)") args = parser.parse_args() @@ -231,12 +297,13 @@ def main(): approved_count = 0 merged_count = 0 skipped_count = 0 + ai_blocked_count = 0 for pr in prs[: args.max_prs]: pr_num = pr["number"] pr_title = pr["title"] head_sha = pr["head"]["sha"] - base_ref = pr.get("base", {}).get("ref", "") + base_ref = pr.get("base", {}).get("re", "") # 跳过draft if pr.get("draft"): @@ -267,8 +334,19 @@ def main(): # 检查审批用的CI状态 all_ok, pending, failed, _ = check_required_contexts(args.token, args.repo, head_sha, approve_contexts) + # === AI审查检查 === + ai_has_critical = False + if not args.skip_ai_review and all_ok and not failed and args.approve: + ai_has_critical, ai_body = get_ai_review_result(args.token, args.repo, pr_num) + if ai_has_critical: + print(" ⚠️ AI审查发现严重问题,阻止自动审批") + ai_blocked_count += 1 + # 给PR打标签便于人工识别 + if not dry_run: + add_pr_label(args.token, args.repo, pr_num, "ai-review/需修改") + # === 自动审批 === - if args.approve and all_ok and not failed: + if args.approve and all_ok and not failed and not ai_has_critical: if has_approval(args.token, args.repo, pr_num): print(" ✅ 已有审批,跳过") else: @@ -282,6 +360,8 @@ def main(): approved_count += 1 else: print(f" ❌ 审批失败: {msg}") + elif ai_has_critical: + print(" 🚫 AI审查阻止审批(人工可手动审批覆盖)") elif failed: print(" ❌ CI有失败项,跳过审批") elif pending: @@ -319,8 +399,9 @@ def main(): print(f" 处理PR数: {min(len(prs), args.max_prs)}") print(f" 自动审批: {approved_count} 个") print(f" 自动合并: {merged_count} 个") + print(f" AI审查阻止: {ai_blocked_count} 个") print(f" 跳过: {skipped_count} 个") - print(f" 模式: {'DRY-RUN' if dry_run else '正式执行'}") + print(" 模式: {'DRY-RUN' if dry_run else '正式执行'}") if __name__ == "__main__":