diff --git a/.gitea/workflows/code-review.yml b/.gitea/workflows/code-review.yml index 8374aaba8..5fbf25f6f 100644 --- a/.gitea/workflows/code-review.yml +++ b/.gitea/workflows/code-review.yml @@ -48,6 +48,7 @@ jobs: GITEA_TOKEN: ${{ secrets.REVIEW_GITEA_TOKEN }} REPO_NAME: ${{ gitea.repository }} PR_NUMBER: ${{ gitea.event.pull_request.number }} + PR_HEAD_SHA: ${{ gitea.event.pull_request.head.sha }} # LLM 提供商: coze (扣子原生Bot) / openai (OpenAI兼容) LLM_PROVIDER: "coze" # 扣子模式配置(默认国内站 api.coze.cn) @@ -60,8 +61,9 @@ jobs: LLM_TIMEOUT: "120" run: | python3 scripts/ci_code_review.py - # 审查脚本异常不影响 CI 通过 - continue-on-error: true + # 注意:脚本退出码决定job状态 + # - 有阻塞级问题 → exit 1 → job失败 → 门禁拦截 + # - 无阻塞级问题/LLM异常 → exit 0 → 通过(fail-open) - name: Report CI trace if: always() diff --git "a/c -l)\\n if [ \"$PAGE_COUNT\" -lt 300 ]; then HAS_MORE=1; else HAS_MORE=0; fi|" "b/c -l)\\n if [ \"$PAGE_COUNT\" -lt 300 ]; then HAS_MORE=1; else HAS_MORE=0; fi|" new file mode 100644 index 000000000..f91e221e7 --- /dev/null +++ "b/c -l)\\n if [ \"$PAGE_COUNT\" -lt 300 ]; then HAS_MORE=1; else HAS_MORE=0; fi|" @@ -0,0 +1 @@ + PAGE_COUNT=$(echo "$PAGE_FILES" diff --git a/scripts/ci_code_review.py b/scripts/ci_code_review.py index 74ecb747b..1ec5e8330 100644 --- a/scripts/ci_code_review.py +++ b/scripts/ci_code_review.py @@ -10,6 +10,7 @@ import argparse import json import logging import os +import re import sys from typing import Optional, Tuple @@ -183,6 +184,37 @@ 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 +448,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 +473,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 +499,46 @@ 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,14 +756,20 @@ 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. 解析阻塞级问题,用退出码决定 job 状态 + # 有阻塞级问题 → exit 1 → job失败 → Gitea自动打failure status → 门禁拦截 + # 无阻塞级问题 → exit 0 → job成功 → Gitea自动打success status + # LLM调用失败等异常 → exit 0 → fail-open,不阻塞正常开发 + has_blocker, blocker_count = parse_blocker_result(review_result) - logger.info("代码审查完成") - sys.exit(0) + if has_blocker: + logger.error(f"检测到 {blocker_count} 个阻塞级问题,审查不通过") + logger.info("代码审查完成(失败)") + sys.exit(1) + else: + logger.info("无阻塞级问题,审查通过") + logger.info("代码审查完成(通过)") + sys.exit(0) except Exception as e: logger.exception(f"审查脚本发生未预期的异常: {e}")