From e346bb288f74cc3730f17ec093cf496b1fefb1b7 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 15:50:34 +0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20AI=E4=BB=A3=E7=A0=81=E5=AE=A1?= =?UTF-8?q?=E6=9F=A5=E6=B7=BB=E5=8A=A0commit=20status=E8=BE=93=E5=87=BA?= =?UTF-8?q?=E5=92=8C=E9=98=BB=E5=A1=9E=E7=BA=A7=E9=97=AE=E9=A2=98=E5=88=A4?= =?UTF-8?q?=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci_code_review.py | 147 ++++++++++++++++++++++++++++++++++---- 1 file changed, 135 insertions(+), 12 deletions(-) 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) -- 2.54.0 From 55805b067de1037f62692897c100c5482d081381 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 15:51:07 +0800 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20code-review=E6=B7=BB=E5=8A=A0PR=5FH?= =?UTF-8?q?EAD=5FSHA=E7=94=A8=E4=BA=8E=E6=89=93commit=20status?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/code-review.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitea/workflows/code-review.yml b/.gitea/workflows/code-review.yml index 8374aaba8..dc5d214df 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) -- 2.54.0 From 6289eb9564db9afd8f086195c162dd6c4b5f7f23 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 16:01:06 +0800 Subject: [PATCH 3/8] =?UTF-8?q?fix:=20AI=E5=AE=A1=E6=9F=A5=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E9=80=80=E5=87=BA=E7=A0=81=E6=8E=A7=E5=88=B6job?= =?UTF-8?q?=E7=8A=B6=E6=80=81=EF=BC=8C=E6=9B=B4=E5=8F=AF=E9=9D=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci_code_review.py | 54 ++++++++------------------------------- 1 file changed, 11 insertions(+), 43 deletions(-) diff --git a/scripts/ci_code_review.py b/scripts/ci_code_review.py index ffecbf512..8b7007c71 100644 --- a/scripts/ci_code_review.py +++ b/scripts/ci_code_review.py @@ -754,52 +754,20 @@ def main(): logger.error("评论发布失败") sys.exit(1) - # 10. 解析阻塞级问题并打 commit status + # 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) - - # 获取 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") + logger.error(f"检测到 {blocker_count} 个阻塞级问题,审查不通过") + logger.info("代码审查完成(失败)") + sys.exit(1) 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) + logger.info("无阻塞级问题,审查通过") + logger.info("代码审查完成(通过)") + sys.exit(0) except Exception as e: logger.exception(f"审查脚本发生未预期的异常: {e}") -- 2.54.0 From 50b179c3cc50af8abd64736f27a122b182b06ca8 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 16:01:07 +0800 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=E5=8E=BB=E6=8E=89continue-on-error?= =?UTF-8?q?=EF=BC=8CAI=E5=AE=A1=E6=9F=A5=E7=BB=93=E6=9E=9C=E7=9B=B4?= =?UTF-8?q?=E6=8E=A5=E5=BD=B1=E5=93=8Djob=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/code-review.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/code-review.yml b/.gitea/workflows/code-review.yml index dc5d214df..5fbf25f6f 100644 --- a/.gitea/workflows/code-review.yml +++ b/.gitea/workflows/code-review.yml @@ -61,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() -- 2.54.0 From e8e1ff8ff259b53e19ba47e02f54ed0b456e081b Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 18:08:41 +0800 Subject: [PATCH 5/8] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=E7=BC=BA=E5=A4=B1?= =?UTF-8?q?=E7=9A=84import=20re=EF=BC=8C=E4=BF=AE=E5=A4=8Dparse=5Fblocker?= =?UTF-8?q?=5Fresult=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci_code_review.py | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/ci_code_review.py b/scripts/ci_code_review.py index 8b7007c71..ceb5bbc89 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 -- 2.54.0 From 78afa85374ca053e9930fd4869479d0df4f959c9 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 18:47:59 +0800 Subject: [PATCH 6/8] =?UTF-8?q?style:=20black=20+=20isort=20=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci_code_review.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/ci_code_review.py b/scripts/ci_code_review.py index ceb5bbc89..1ec5e8330 100644 --- a/scripts/ci_code_review.py +++ b/scripts/ci_code_review.py @@ -184,7 +184,9 @@ class GiteaClient: return False return True - def create_commit_status(self, sha: str, state: str, context: str, description: str = "", target_url: str = "") -> bool: + 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 @@ -502,7 +504,6 @@ 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]: """ 从审查结果中解析是否存在阻塞级问题。 -- 2.54.0 From 8fdd7e267d7df00b5d54f8c2f69ad9655e5b4503 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 21:12:02 +0800 Subject: [PATCH 7/8] chore: trigger full CI re-run after runner label fix [skip ci-format-check] -- 2.54.0 From 5eff3a92cbb5466801410152def4da9a0c32b9fd Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Mon, 27 Jul 2026 21:22:33 +0800 Subject: [PATCH 8/8] chore: re-trigger CI after runner fix [skip ci-format-check] -- 2.54.0