Files
xiaoxia-saas/scripts/ci_notify.py
T
xiaoxia 545ff0fab8
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 24s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (web-cache, infra/docker/web.Dockerfile, xiaoxia-saas-web, web, Web, 30) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m17s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m24s
CI/CD Pipeline / PR Build API Image (Backend) (pull_request) Successful in 1m35s
AI Code Review / AI Code Review (pull_request) Failing after 2m12s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 2m22s
CI/CD Pipeline / PR Build Worker Image (Backend) (pull_request) Failing after 2m23s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m55s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m30s
CI/CD Pipeline / AI Code Review (pull_request) Failing after 3m56s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 5m59s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m42s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 8s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 7s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 8s
fix(ci): restore execute permission for ci_notify.py and healthcheck script
2026-07-29 09:19:16 +08:00

209 lines
7.7 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
统一CI通知脚本 - 发送飞书卡片通知
支持三种模式: start / success / failure
包含: PR链接、耗时、失败阶段、分支、提交者、Run链接、Runner信息
用法:
NOTIFY_MODE=start JOB_NAME="xxx" python3 scripts/ci_notify.py
NOTIFY_MODE=success JOB_NAME="xxx" JOB_DURATION="2m30s" python3 scripts/ci_notify.py
NOTIFY_MODE=failure JOB_NAME="xxx" FAILED_STEP="xxx" JOB_DURATION="2m30s" python3 scripts/ci_notify.py
环境变量:
CI_NOTIFY_WEBHOOK - 飞书webhook地址 (必填)
NOTIFY_MODE - 通知模式: start / success / failure (必填)
JOB_NAME - Job名称 (必填)
JOB_DURATION - 耗时,如"2m30s" (成功/失败时建议传)
FAILED_STEP - 失败的步骤名 (失败时建议传)
GITHUB_REF_NAME - 分支名
GITHUB_SHA - commit SHA
GITHUB_ACTOR - 提交者
GITHUB_RUN_ID - Run ID
GITHUB_REPOSITORY - 仓库路径
GITHUB_EVENT_NAME - 事件类型 (pull_request / push / ...)
GITHUB_PR_NUMBER - PR编号 (PR事件时)
GITHUB_PR_TITLE - PR标题 (PR事件时)
RUNNER_NAME - Runner名称 (可选,自动获取)
设计原则:
1. 通知失败永远不阻断CI主流程(返回exit code 0
2. 标题包含"CI通知"/"CI告警"关键词,适配飞书webhook关键词校验
3. 卡片信息尽量丰富,方便快速定位问题
"""
import json
import os
import sys
import urllib.request
def get_env(name, default=""):
"""读取环境变量"""
return os.environ.get(name, default)
def format_duration(seconds_str):
"""将秒数格式化为易读形式"""
try:
seconds = int(float(seconds_str))
mins = seconds // 60
secs = seconds % 60
if mins > 0:
return f"{mins}m{secs}s"
return f"{secs}s"
except (ValueError, TypeError):
return seconds_str or "未知"
def classify_job(job_name):
"""根据Job名称判断所属阶段"""
name = job_name.lower()
if any(k in name for k in ["validate", "lint", "unit test", "integration test"]):
return "门禁检查"
if any(k in name for k in ["build", "image"]):
return "镜像构建"
if any(k in name for k in ["deploy", "staging", "production"]):
return "部署发布"
if any(k in name for k in ["e2e", "test", "smoke"]):
return "测试验证"
return "其他"
def main() -> int:
webhook = get_env("CI_NOTIFY_WEBHOOK")
if not webhook:
print("未配置 CI_NOTIFY_WEBHOOK,跳过通知")
print("如需启用,请在仓库 Settings -> Secrets and variables -> Actions 中添加 CI_NOTIFY_WEBHOOK")
return 0
mode = get_env("NOTIFY_MODE", "failure").lower()
job_name = get_env("JOB_NAME", "Unknown Job")
duration = get_env("JOB_DURATION")
if not duration:
duration_sec = get_env("JOB_DURATION_SECONDS")
duration = format_duration(duration_sec) if duration_sec else "计算中..."
failed_step = get_env("FAILED_STEP", "")
branch = get_env("GITHUB_REF_NAME", "unknown")
commit = get_env("GITHUB_SHA", "unknown")[:8]
actor = get_env("GITHUB_ACTOR", "unknown")
run_id = get_env("GITHUB_RUN_ID", "unknown")
repo = get_env("GITHUB_REPOSITORY", "unknown")
event_name = get_env("GITHUB_EVENT_NAME", "")
pr_number = get_env("GITHUB_PR_NUMBER", "")
pr_title = get_env("GITHUB_PR_TITLE", "")
runner_name = get_env("RUNNER_NAME", "")
run_url = f"https://git.xiaoxiajianji.com/{repo}/actions/runs/{run_id}"
job_stage = classify_job(job_name)
# 根据模式设置标题、状态、颜色
# 注意:标题中必须包含飞书webhook配置的关键词,否则会报"Key Words Not Found"
# 这里加入"CI通知"/"CI告警"关键词提高命中率
if mode == "start":
title = f"🔄 CI通知:{job_name} 开始构建"
status = "blue"
button_text = "查看进度"
button_type = "primary"
elif mode == "success":
title = f"✅ CI通知:{job_name} 构建成功"
status = "green"
button_text = "查看详情"
button_type = "primary"
else: # failure
title = f"❌ CI告警:{job_name} 构建失败"
status = "red"
button_text = "查看失败日志"
button_type = "danger"
# 构建卡片内容 - 左侧标签+右侧值的结构化展示
fields = []
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**阶段**\n{job_stage}"}})
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**任务**\n{job_name}"}})
if mode != "start":
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**耗时**\n{duration}"}})
else:
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": "**状态**\n进行中"}})
if runner_name:
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**Runner**\n{runner_name}"}})
if mode == "failure" and failed_step:
fields.append({"is_short": False, "text": {"tag": "lark_md", "content": f"**失败步骤**\n{failed_step}"}})
# PR/分支信息
if event_name == "pull_request" and pr_number:
pr_url = f"https://git.xiaoxiajianji.com/{repo}/pulls/{pr_number}"
pr_display = f"#{pr_number}"
if pr_title:
pr_display += f" {pr_title[:30]}"
fields.append({"is_short": False, "text": {"tag": "lark_md", "content": f"**PR**\n[{pr_display}]({pr_url})"}})
elif event_name == "push":
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**分支**\n{branch}"}})
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**提交**\n`{commit}`"}})
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**提交者**\n{actor}"}})
fields.append({"is_short": True, "text": {"tag": "lark_md", "content": f"**Run ID**\n{run_id}"}})
payload = {
"msg_type": "interactive",
"card": {
"header": {
"title": {
"tag": "plain_text",
"content": title,
},
"status": status,
},
"elements": [
{
"tag": "div",
"fields": fields,
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {"tag": "plain_text", "content": button_text},
"url": run_url,
"type": button_type,
}
],
},
],
},
}
data = json.dumps(payload).encode("utf-8")
req = urllib.request.Request(
webhook,
data=data,
headers={"Content-Type": "application/json"},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
resp_body = resp.read().decode("utf-8")
# 飞书返回code=0表示成功
try:
result = json.loads(resp_body)
if result.get("code", 0) != 0:
print(f"通知发送告警: 飞书返回错误 - {result.get('msg', resp_body)}", file=sys.stderr)
print(f"通知已发送 ({mode}) - 飞书返回非0,但不阻断CI流程")
else:
print(f"通知已发送 ({mode})")
except json.JSONDecodeError:
print(f"通知已发送 ({mode})")
except Exception as e:
print(f"通知发送告警: {e}", file=sys.stderr)
# 通知无论成功失败都不阻断CI主流程,统一返回0
return 0
if __name__ == "__main__":
sys.exit(main())