diff --git a/scripts/ci/ci_dashboard.py b/scripts/ci/ci_dashboard.py
index 09b85cc3b..3c7c1f6f8 100644
--- a/scripts/ci/ci_dashboard.py
+++ b/scripts/ci/ci_dashboard.py
@@ -11,6 +11,7 @@ CI 可观测性看板 - 从 Gitea Actions API 拉取数据并生成 Markdown/HTM
GITEA_REPO 仓库 (默认 xiaoxia/xiaoxia-saas)
GITEA_TOKEN API Token (优先) 或 GITEA_USERNAME + GITEA_PASSWORD
"""
+
import argparse
import base64
import json
@@ -29,6 +30,7 @@ DEFAULT_REPO = "xiaoxia/xiaoxia-saas"
DEFAULT_DAYS = 7
PAGE_LIMIT = 50 # 每页数量,最大50
+
# ── API 封装 ─────────────────────────────────────────
class GiteaActions:
def __init__(self, base_url, repo, token=None, username=None, password=None):
@@ -310,8 +312,20 @@ def analyze_runs(runs):
# 基础设施关键词(与 ci_health_check.py 保持一致的分类逻辑)
infra_job_keywords = ["checkout", "build", "deploy", "cleanup", "setup", "cache", "install", "docker"]
business_job_keywords = [
- "unit test", "pytest", "vitest", "jest", "lint", "eslint", "prettier",
- "integration", "e2e", "validate", "code quality", "mypy", "ruff", "flake8",
+ "unit test",
+ "pytest",
+ "vitest",
+ "jest",
+ "lint",
+ "eslint",
+ "prettier",
+ "integration",
+ "e2e",
+ "validate",
+ "code quality",
+ "mypy",
+ "ruff",
+ "flake8",
]
for r in runs:
@@ -581,12 +595,14 @@ def generate_html(stats, start_date, end_date, repo):
for name, s in stats["job_success_stats"].items():
if s["total"] >= 3: # 至少有3次才统计
rate = (s["success"] / s["total"] * 100) if s["total"] > 0 else 0
- job_stats_list.append({
- "name": name,
- "rate": round(rate, 1),
- "total": s["total"],
- "success": s["success"],
- })
+ job_stats_list.append(
+ {
+ "name": name,
+ "rate": round(rate, 1),
+ "total": s["total"],
+ "success": s["success"],
+ }
+ )
job_stats_list.sort(key=lambda x: x["rate"])
job_stats_list = job_stats_list[:15] # 取成功率最低的15个
job_names = [j["name"] for j in job_stats_list]
@@ -601,277 +617,294 @@ def generate_html(stats, start_date, end_date, repo):
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
# 序列化数据为 JSON(供 JS 使用)
- data_json = json.dumps({
- "daily_dates": daily_dates,
- "daily_success_rates": daily_success_rates,
- "daily_run_counts": daily_run_counts,
- "wf_names": wf_names,
- "wf_avg_durations": wf_avg_durations,
- "failure_pie_data": failure_pie_data,
- "job_names": job_names,
- "job_rates": job_rates,
- }, ensure_ascii=False)
+ data_json = json.dumps(
+ {
+ "daily_dates": daily_dates,
+ "daily_success_rates": daily_success_rates,
+ "daily_run_counts": daily_run_counts,
+ "wf_names": wf_names,
+ "wf_avg_durations": wf_avg_durations,
+ "failure_pie_data": failure_pie_data,
+ "job_names": job_names,
+ "job_rates": job_rates,
+ },
+ ensure_ascii=False,
+ )
# HTML 模板(注意:不使用 f-string,避免与 CSS/JS 的大括号冲突)
html_parts = []
- html_parts.append('')
+ html_parts.append("")
html_parts.append('')
- html_parts.append('
')
+ html_parts.append("")
html_parts.append(' ')
html_parts.append(' ')
- html_parts.append(f' CI 健康度看板 - {repo}')
+ html_parts.append(f" CI 健康度看板 - {repo}")
html_parts.append(' ')
- html_parts.append(' ')
- html_parts.append('')
- html_parts.append('')
+ html_parts.append(" background: #f0f2f5;")
+ html_parts.append(" color: #333;")
+ html_parts.append(" padding: 20px;")
+ html_parts.append(" }")
+ html_parts.append(" .container { max-width: 1400px; margin: 0 auto; }")
+ html_parts.append(" .header {")
+ html_parts.append(" background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);")
+ html_parts.append(" color: white;")
+ html_parts.append(" padding: 24px 32px;")
+ html_parts.append(" border-radius: 12px;")
+ html_parts.append(" margin-bottom: 20px;")
+ html_parts.append(" }")
+ html_parts.append(" .header h1 { font-size: 24px; margin-bottom: 8px; }")
+ html_parts.append(" .header .subtitle { font-size: 14px; opacity: 0.9; }")
+ html_parts.append(" .header .meta { font-size: 12px; opacity: 0.8; margin-top: 8px; }")
+ html_parts.append(" .metrics-row {")
+ html_parts.append(" display: grid;")
+ html_parts.append(" grid-template-columns: repeat(4, 1fr);")
+ html_parts.append(" gap: 16px;")
+ html_parts.append(" margin-bottom: 20px;")
+ html_parts.append(" }")
+ html_parts.append(" .metric-card {")
+ html_parts.append(" background: white;")
+ html_parts.append(" border-radius: 12px;")
+ html_parts.append(" padding: 20px;")
+ html_parts.append(" box-shadow: 0 2px 8px rgba(0,0,0,0.06);")
+ html_parts.append(" transition: transform 0.2s;")
+ html_parts.append(" }")
+ html_parts.append(
+ " .metric-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); }"
+ )
+ html_parts.append(" .metric-card .label { font-size: 13px; color: #8c8c8c; margin-bottom: 8px; }")
+ html_parts.append(" .metric-card .value { font-size: 28px; font-weight: 600; }")
+ html_parts.append(" .metric-card .unit { font-size: 14px; color: #8c8c8c; margin-left: 4px; }")
+ html_parts.append(" .metric-card.success .value { color: #52c41a; }")
+ html_parts.append(" .metric-card.warning .value { color: #faad14; }")
+ html_parts.append(" .metric-card.danger .value { color: #ff4d4f; }")
+ html_parts.append(" .metric-card.info .value { color: #1890ff; }")
+ html_parts.append(" .charts-grid {")
+ html_parts.append(" display: grid;")
+ html_parts.append(" grid-template-columns: 1fr 1fr;")
+ html_parts.append(" gap: 16px;")
+ html_parts.append(" margin-bottom: 20px;")
+ html_parts.append(" }")
+ html_parts.append(" .chart-card {")
+ html_parts.append(" background: white;")
+ html_parts.append(" border-radius: 12px;")
+ html_parts.append(" padding: 20px;")
+ html_parts.append(" box-shadow: 0 2px 8px rgba(0,0,0,0.06);")
+ html_parts.append(" }")
+ html_parts.append(" .chart-card.full-width { grid-column: 1 / -1; }")
+ html_parts.append(" .chart-card h3 {")
+ html_parts.append(" font-size: 16px;")
+ html_parts.append(" margin-bottom: 12px;")
+ html_parts.append(" color: #262626;")
+ html_parts.append(" font-weight: 600;")
+ html_parts.append(" }")
+ html_parts.append(" .chart-container { width: 100%; height: 320px; }")
+ html_parts.append(" .chart-container.tall { height: 400px; }")
+ html_parts.append(" .footer {")
+ html_parts.append(" text-align: center;")
+ html_parts.append(" color: #8c8c8c;")
+ html_parts.append(" font-size: 12px;")
+ html_parts.append(" padding: 16px 0;")
+ html_parts.append(" }")
+ html_parts.append(" @media (max-width: 900px) {")
+ html_parts.append(" .metrics-row { grid-template-columns: repeat(2, 1fr); }")
+ html_parts.append(" .charts-grid { grid-template-columns: 1fr; }")
+ html_parts.append(" }")
+ html_parts.append(" @media (max-width: 600px) {")
+ html_parts.append(" .metrics-row { grid-template-columns: 1fr; }")
+ html_parts.append(" body { padding: 12px; }")
+ html_parts.append(" }")
+ html_parts.append(" ")
+ html_parts.append("")
+ html_parts.append("")
html_parts.append(' ')
html_parts.append(' ')
+ html_parts.append("
")
html_parts.append(' ')
html_parts.append('
')
html_parts.append('
总成功率
')
html_parts.append(f'
{success_rate}%
')
- html_parts.append('
')
+ html_parts.append("
")
html_parts.append(' ')
html_parts.append('
总 Run 数
')
html_parts.append(f'
{total_runs}次
')
- html_parts.append('
')
+ html_parts.append(" ")
html_parts.append(' ')
html_parts.append('
平均耗时
')
html_parts.append(f'
{avg_dur_min}分钟
')
- html_parts.append('
')
+ html_parts.append(" ")
html_parts.append(' ')
html_parts.append('
基础设施故障率
')
html_parts.append(f'
{infra_fail_rate}%
')
- html_parts.append('
')
- html_parts.append(' ')
+ html_parts.append(" ")
+ html_parts.append(" ")
html_parts.append(' ')
html_parts.append('
')
- html_parts.append('
📈 CI 成功率趋势
')
+ html_parts.append("
📈 CI 成功率趋势
")
html_parts.append('
')
- html_parts.append('
')
- html_parts.append('
')
+ html_parts.append(" ")
+ html_parts.append(" ")
html_parts.append(' ')
html_parts.append('
')
- html_parts.append('
⏱️ 各 Workflow 平均耗时
')
+ html_parts.append("
⏱️ 各 Workflow 平均耗时
")
html_parts.append('
')
- html_parts.append('
')
+ html_parts.append("
")
html_parts.append(' ')
- html_parts.append('
❌ 失败原因分布
')
+ html_parts.append("
❌ 失败原因分布
")
html_parts.append('
')
- html_parts.append('
')
- html_parts.append(' ')
+ html_parts.append(" ")
+ html_parts.append(" ")
html_parts.append(' ')
html_parts.append('
')
- html_parts.append('
📋 各 Job 成功率排行(最低 15 名)
')
+ html_parts.append("
📋 各 Job 成功率排行(最低 15 名)
")
html_parts.append('
')
- html_parts.append('
')
- html_parts.append('
')
+ html_parts.append(" ")
+ html_parts.append(" ")
html_parts.append(' ')
html_parts.append('
')
- html_parts.append('
📊 每日 Run 数量趋势
')
+ html_parts.append("
📊 每日 Run 数量趋势
")
html_parts.append('
')
- html_parts.append('
')
- html_parts.append('
')
+ html_parts.append(" ")
+ html_parts.append(" ")
html_parts.append(' ')
- html_parts.append(' ')
- html_parts.append(' ')
- html_parts.append('')
- html_parts.append('')
+ html_parts.append(" })();")
+ html_parts.append(" ")
+ html_parts.append("")
+ html_parts.append("