From a18244b8a4e93e674bd46bf9c3dbc6db14633028 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Wed, 22 Jul 2026 07:41:06 +0800 Subject: [PATCH] =?UTF-8?q?ci:=20=E5=A2=9E=E5=BC=BACI=E5=A4=B1=E8=B4=A5?= =?UTF-8?q?=E9=80=9A=E7=9F=A5=EF=BC=8C=E8=87=AA=E5=8A=A8=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E5=8E=9F=E5=9B=A0=E5=B9=B6=E7=BB=99=E5=87=BA?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=BB=BA=E8=AE=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/ci_notify_failure.py | 155 ++++++++++++++++++++++++++++------- 1 file changed, 124 insertions(+), 31 deletions(-) diff --git a/scripts/ci_notify_failure.py b/scripts/ci_notify_failure.py index a8be3de03..d91770b8a 100755 --- a/scripts/ci_notify_failure.py +++ b/scripts/ci_notify_failure.py @@ -1,17 +1,53 @@ #!/usr/bin/env python3 -"""发送 CI 失败通知到飞书/项目群 webhook。""" +"""发送 CI 失败通知到飞书/项目群 webhook(增强版:带失败诊断)。 + +诊断功能:自动分析失败原因,给出分类和修复建议。 +""" import json import os import sys import urllib.request +import subprocess + + +def run_diagnosis() -> dict: + """运行失败诊断脚本,返回诊断结果""" + diag_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "ci/ci_failure_diagnosis.py") + if not os.path.exists(diag_script): + diag_script = "scripts/ci/ci_failure_diagnosis.py" + + result = {"category": "unknown", "category_cn": "未知", "severity": "medium", + "summary": "", "error_lines": [], "suggestions": [], "auto_fixable": False} + + try: + # 运行诊断脚本 + env = os.environ.copy() + env["DIAGNOSIS_OUTPUT"] = "/tmp/ci_diagnosis_result.json" + + proc = subprocess.run( + [sys.executable, diag_script], + capture_output=True, text=True, timeout=30, env=env + ) + + # 尝试读取结果文件 + output_file = "/tmp/ci_diagnosis_result.json" + if os.path.exists(output_file): + with open(output_file) as f: + result = json.load(f) + elif proc.stdout: + # 从stdout解析 + pass + except Exception as e: + print(f"诊断脚本执行失败: {e}", file=sys.stderr) + + return result def main() -> int: webhook = os.environ.get("CI_NOTIFY_WEBHOOK", "") if not webhook: print("未配置 CI_NOTIFY_WEBHOOK,跳过通知") - print("如需启用,请在仓库 Settings -> Secrets and variables -> Actions 中添加 CI_NOTIFY_WEBHOOK") return 0 failed_job = os.environ.get("FAILED_JOB", "Unknown Job") @@ -21,6 +57,88 @@ def main() -> int: run_id = os.environ.get("GITHUB_RUN_ID", "unknown") repo = os.environ.get("GITHUB_REPOSITORY", "unknown") run_url = f"https://git.xiaoxiajianji.com/{repo}/actions/runs/{run_id}" + pr_number = os.environ.get("PR_NUMBER", "") + + # 运行诊断 + diagnosis = run_diagnosis() + + # 构建卡片内容 + severity_color = {"high": "red", "medium": "orange", "low": "blue"} + card_status = severity_color.get(diagnosis.get("severity", "medium"), "red") + + # 标题 + title = f"❌ CI失败 - {diagnosis.get('category_cn', '未知')}" + + # 诊断部分 + diag_lines = [] + diag_lines.append(f"**任务**: {failed_job}") + diag_lines.append(f"**分类**: {diagnosis.get('category_cn', '未知')}") + if diagnosis.get("summary"): + diag_lines.append(f"**问题**: {diagnosis['summary']}") + + # 错误行 + error_lines = diagnosis.get("error_lines", []) + if error_lines: + diag_lines.append("") + diag_lines.append("**关键错误**:") + for err in error_lines[:3]: + if len(err) > 100: + err = err[:97] + "..." + diag_lines.append(f"`{err}`") + + # 修复建议 + suggestions = diagnosis.get("suggestions", []) + if suggestions: + diag_lines.append("") + diag_lines.append("**修复建议**:") + for i, s in enumerate(suggestions[:3], 1): + diag_lines.append(f"{i}. {s}") + + if diagnosis.get("auto_fixable"): + diag_lines.append("") + diag_lines.append("💡 *可自动修复的问题,试试Rerun*") + + # 基本信息 + info_lines = [ + f"**分支**: {branch}", + f"**提交**: `{commit}`", + f"**提交者**: {actor}", + ] + if pr_number: + info_lines.append(f"**PR**: #{pr_number}") + + elements = [ + { + "tag": "div", + "text": { + "tag": "lark_md", + "content": " +".join(diag_lines), + }, + }, + { + "tag": "hr", + }, + { + "tag": "div", + "text": { + "tag": "lark_md", + "content": " +".join(info_lines), + }, + }, + { + "tag": "action", + "actions": [ + { + "tag": "button", + "text": {"tag": "plain_text", "content": "查看失败日志"}, + "url": run_url, + "type": "danger", + }, + ], + }, + ] payload = { "msg_type": "interactive", @@ -28,36 +146,11 @@ def main() -> int: "header": { "title": { "tag": "plain_text", - "content": "❌ CI 构建失败", + "content": title, }, - "status": "red", + "status": card_status, }, - "elements": [ - { - "tag": "div", - "text": { - "tag": "lark_md", - "content": ( - f"**任务**: {failed_job}\n" - f"**分支**: {branch}\n" - f"**提交**: {commit}\n" - f"**提交者**: {actor}\n" - f"**Run ID**: {run_id}" - ), - }, - }, - { - "tag": "action", - "actions": [ - { - "tag": "button", - "text": {"tag": "plain_text", "content": "查看失败日志"}, - "url": run_url, - "type": "danger", - } - ], - }, - ], + "elements": elements, }, } @@ -71,7 +164,7 @@ def main() -> int: try: with urllib.request.urlopen(req, timeout=10) as resp: resp.read() - print("通知已发送") + print("通知已发送(带诊断信息)") except Exception as e: print(f"通知发送失败: {e}", file=sys.stderr) return 1