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('

📊 CI 健康度看板

') + html_parts.append("

📊 CI 健康度看板

") html_parts.append(f'
仓库: {repo}
') html_parts.append(f'
统计周期: {start_date} ~ {end_date} | 生成时间: {now_str}
') - 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("") return "\n".join(html_parts)