diff --git a/scripts/ci_code_review.py b/scripts/ci_code_review.py index 74ecb747b..ffecbf512 100644 --- a/scripts/ci_code_review.py +++ b/scripts/ci_code_review.py @@ -183,6 +183,35 @@ class GiteaClient: return False return True + def create_commit_status(self, sha: str, state: str, context: str, description: str = "", target_url: str = "") -> bool: + """ + 给指定 commit 打 status。 + state: pending / success / failure / error / warning + Gitea API: POST /repos/{owner}/{repo}/statuses/{sha} + """ + url = self._api_url(f"statuses/{sha}") + logger.info(f"设置 commit status: sha={sha[:12]}..., state={state}, context={context}") + + payload = { + "state": state, + "context": context, + "description": description[:200] if description else "", + } + if target_url: + payload["target_url"] = target_url + + resp = self.session.post( + url, + data=json.dumps(payload), + timeout=GITEA_TIMEOUT, + ) + if resp.status_code not in (200, 201): + logger.error(f"设置 status 失败: HTTP {resp.status_code} - {resp.text[:200]}") + return False + + logger.info(f"Status 设置成功: {context} = {state}") + return True + def call_llm_openai( prompt: str, @@ -416,7 +445,22 @@ def build_review_prompt(diff_text: str, pr_number: int, file_list: list) -> str: ``` ## 审查要求 -请从以下维度进行审查,重点关注严重问题: +请从以下维度进行审查,重点关注**阻塞级问题**: + +### 问题分级标准 +- **🔴 阻塞级(BLOCKER)**:必须修复,否则不允许合并。包括: + 1. **明显逻辑bug**:条件判断错误、死循环、返回值错误、空指针/None引用未处理、边界条件遗漏导致功能异常 + 2. **安全漏洞**:SQL注入、XSS、命令注入、敏感信息明文存储/泄露、权限绕过、认证缺失 + 3. **语法错误**:代码存在语法层面的错误,无法运行 + 4. **数据损坏风险**:可能导致数据丢失、数据不一致、脏数据写入的问题 + +- **💡 建议级(SUGGESTION)**:不阻塞合并,仅供参考改进。包括: + 1. 命名不规范、代码风格问题 + 2. 最佳实践建议、设计模式优化 + 3. 格式问题(缩进、空行、import顺序等) + 4. 代码可读性改进、注释补充 + 5. 非关键路径的轻微性能优化建议 + 6. 重复代码、过长函数等代码质量问题 1. **逻辑正确性**:是否有明显的逻辑错误、边界条件遗漏、空指针/None引用风险 2. **异常处理**:异常捕获是否合理,是否有裸except,错误处理是否完善 @@ -426,20 +470,24 @@ def build_review_prompt(diff_text: str, pr_number: int, file_list: list) -> str: 6. **安全问题**:是否有注入风险、敏感信息泄露、权限控制问题 ## 输出格式 -请使用以下格式输出,语言为中文: +请使用以下格式输出,语言为中文。**必须严格按照格式输出,尤其是【阻塞级判定】部分**: + +### 【阻塞级判定】 +- 是否存在阻塞级问题:(是 / 否) +- 阻塞级问题数量:X 个 ### 📊 审查概览 - 整体评价:(通过 / 有建议 / 需修改) -- 严重问题数量:X 个 -- 一般建议数量:X 个 +- 建议级问题数量:X 个 -### ❌ 需修改的问题(严重) -(如果没有严重问题,写"无") +### 🔴 阻塞级问题(必须修复) +(如果没有阻塞级问题,写"无") 1. **[文件: 行号] 问题标题** + - 问题类型:(逻辑bug / 安全漏洞 / 语法错误 / 数据损坏风险) - 问题描述:... - 修改建议:... -### 💡 改进建议(一般) +### 💡 改进建议(不阻塞合并) (如果没有建议,写"无") 1. **[文件: 行号] 建议标题** - 具体内容:... @@ -448,10 +496,47 @@ def build_review_prompt(diff_text: str, pr_number: int, file_list: list) -> str: (可选,列出值得肯定的地方) 请务必基于代码实际内容审查,不要编造不存在的问题。如果代码质量良好,直接给出通过结论即可。 +**重要:【阻塞级判定】必须准确,只有确实存在严重问题时才写"是"。** """ return prompt + +def parse_blocker_result(review_text: str) -> Tuple[bool, int]: + """ + 从审查结果中解析是否存在阻塞级问题。 + 返回 (has_blocker, blocker_count) + """ + # 先找【阻塞级判定】部分的明确标记 + pattern = r"【阻塞级判定】[\s\S]*?是否存在阻塞级问题[::]\s*(是|否)" + match = re.search(pattern, review_text) + if match: + has_blocker = match.group(1) == "是" + else: + # fallback 1: 找"阻塞级问题数量" + count_pattern = r"阻塞级问题数量[::]\s*(\d+)" + count_match = re.search(count_pattern, review_text) + if count_match: + has_blocker = int(count_match.group(1)) > 0 + else: + # fallback 2: 检查是否有"阻塞级问题"section且内容不是"无" + has_blocker = False + blocker_section = re.search(r"### 🔴 阻塞级问题[\s\S]*?(?=### |\Z)", review_text) + if blocker_section: + section_text = blocker_section.group(0) + # 如果有编号列表项,说明有问题 + if re.search(r"\d+\.\s*\*\*", section_text): + has_blocker = True + + # 提取数量 + count_pattern = r"阻塞级问题数量[::]\s*(\d+)" + count_match = re.search(count_pattern, review_text) + blocker_count = int(count_match.group(1)) if count_match else (1 if has_blocker else 0) + + logger.info(f"阻塞级问题解析: 存在={has_blocker}, 数量={blocker_count}") + return has_blocker, blocker_count + + def call_llm_for_review( diff_text: str, pr_number: int, @@ -669,12 +754,50 @@ def main(): logger.error("评论发布失败") sys.exit(1) - # 10. 判断是否有严重问题(可选阻断) - # 目前只做建议,不阻断合并,始终返回 0 - has_critical = "问题" in review_result and ("❌" in review_result or "需修改" in review_result) - if has_critical: - logger.warning("检测到需修改的问题,但当前配置为仅建议,不阻断合并") + # 10. 解析阻塞级问题并打 commit status + has_blocker, blocker_count = parse_blocker_result(review_result) + + # 获取 PR head SHA(用于打 commit status) + # GITHUB_SHA 在 PR 事件中是 merge commit 的 sha,需要用 head sha + pr_head_sha = os.getenv("PR_HEAD_SHA", "") + if not pr_head_sha or len(pr_head_sha) < 40: + # 从 PR 信息中获取 head sha + try: + pr_detail_url = gitea._api_url(f"pulls/{pr_number}") + resp = gitea.session.get(pr_detail_url, timeout=GITEA_TIMEOUT) + if resp.status_code == 200: + pr_data = resp.json() + pr_head_sha = pr_data.get("head", {}).get("sha", "") + except Exception as e: + logger.warning(f"获取 PR head sha 失败: {e}") + + # 兜底:用 GITHUB_SHA + if not pr_head_sha or len(pr_head_sha) < 40: + pr_head_sha = os.getenv("GITHUB_SHA", "") + + status_context = "AI Code Review / AI Code Review (pull_request)" + if has_blocker: + status_state = "failure" + status_desc = f"发现 {blocker_count} 个阻塞级问题,需修复后合并" + logger.warning(f"检测到 {blocker_count} 个阻塞级问题,commit status = failure") + else: + status_state = "success" + status_desc = "AI审查通过,无阻塞级问题" + logger.info("无阻塞级问题,commit status = success") + # 打 commit status + if pr_head_sha and len(pr_head_sha) >= 40: + gitea.create_commit_status( + sha=pr_head_sha, + state=status_state, + context=status_context, + description=status_desc, + ) + else: + logger.warning(f"无法获取有效的 commit SHA(当前: {pr_head_sha}),跳过 commit status") + + # 审查脚本本身始终以 0 退出(status 已通过 API 独立打出) + # workflow 级别的成功/失败不影响 status check logger.info("代码审查完成") sys.exit(0)