6eac0b2cf2
Worker Base Image Build / Build Worker Base Images (worker-base-builder-cache, infra/docker/worker-base-builder.Dockerfile, worker-base-builder, builder) (push) Failing after 1m54s
Worker Base Image Build / Build Worker Base Images (worker-base-runtime-cache, infra/docker/worker-base-runtime.Dockerfile, worker-base-runtime, runtime) (push) Failing after 1m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 1m29s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m4s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m2s
CI/CD Pipeline / Unit Tests (push) Successful in 3m38s
CI/CD Pipeline / Integration Tests (push) Successful in 2m0s
CI/CD Pipeline / Frontend Lint (push) Successful in 28s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 44s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Failing after 2m16s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 8m14s
CI/CD Pipeline / Build Staging Worker Image (push) Failing after 2m0s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Has been skipped
同步内容: 1. CI流水线配置(ci-pipeline.yml)与develop对齐 2. PR构建脚本docker_build_only.sh增加buildx→docker build回退 3. pre-build步骤worker基础镜像构建增加buildx回退 4. 单元测试脚本全量覆盖率改为仅报告不阻塞 5. diff-cover依赖加入requirements-dev.txt 6. worker base builder/runtime Dockerfile同步 7. test_config_oss.py clear=False→clear=True修复OSS污染 8. Frontend Lint增加prettier依赖
297 lines
10 KiB
Python
Executable File
297 lines
10 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
CI 状态查询模块 - 查询 run 列表、某分支/某 PR 的 CI 状态、失败详情
|
||
|
||
支持查询类型:
|
||
- list_runs: 列出最近的 workflow runs
|
||
- branch_status: 某分支最新 CI 状态
|
||
- pr_status: 某 PR 的 CI 状态
|
||
- failure_detail: 某次 run 的失败详情
|
||
|
||
用法:
|
||
python3 scripts/ci/chatops/ci_query.py --branch develop
|
||
python3 scripts/ci/chatops/ci_query.py --pr 123
|
||
python3 scripts/ci/chatops/ci_query.py --run-id 456 --detail
|
||
|
||
设计:
|
||
- 与飞书机器人 /ci status 命令对接
|
||
- 返回结构化数据,上层负责格式化输出
|
||
"""
|
||
|
||
import argparse
|
||
import sys
|
||
|
||
from . import config
|
||
from .gitea_client import GiteaClient
|
||
|
||
|
||
class CIQuery:
|
||
"""CI 状态查询器"""
|
||
|
||
def __init__(self, gitea_client=None):
|
||
self.gitea = gitea_client or GiteaClient()
|
||
|
||
# ── 查询方法 ──────────────────────────────────────
|
||
|
||
def get_branch_status(self, branch, limit=5):
|
||
"""获取指定分支最新的 CI 状态
|
||
|
||
Returns:
|
||
dict: {branch, latest_run, recent_runs, overall_status}
|
||
"""
|
||
runs, total = self.gitea.list_runs(branch=branch, limit=limit)
|
||
if not runs:
|
||
return {
|
||
"branch": branch,
|
||
"latest_run": None,
|
||
"recent_runs": [],
|
||
"overall_status": "no_runs",
|
||
"total_count": total,
|
||
}
|
||
|
||
latest = runs[0]
|
||
overall = self._derive_overall_status(runs)
|
||
|
||
return {
|
||
"branch": branch,
|
||
"latest_run": latest,
|
||
"recent_runs": runs,
|
||
"overall_status": overall,
|
||
"total_count": total,
|
||
}
|
||
|
||
def get_pr_status(self, pr_number):
|
||
"""获取指定 PR 的 CI 状态
|
||
|
||
Returns:
|
||
dict: {pr_number, pr_title, runs, overall_status}
|
||
"""
|
||
pr = self.gitea.get_pr(pr_number)
|
||
if not pr:
|
||
return {
|
||
"pr_number": pr_number,
|
||
"pr_title": "未知",
|
||
"runs": [],
|
||
"overall_status": "pr_not_found",
|
||
}
|
||
|
||
pr_title = pr.get("title", "")
|
||
runs = self.gitea.get_pr_ci_runs(pr_number, limit=10)
|
||
overall = self._derive_overall_status(runs) if runs else "no_runs"
|
||
|
||
return {
|
||
"pr_number": pr_number,
|
||
"pr_title": pr_title,
|
||
"runs": runs,
|
||
"overall_status": overall,
|
||
"head_sha": pr.get("head", {}).get("sha", ""),
|
||
}
|
||
|
||
def get_failure_detail(self, run_id):
|
||
"""获取某次 run 的失败详情
|
||
|
||
Returns:
|
||
dict: {run_info, failed_jobs, summary}
|
||
"""
|
||
run = self.gitea.get_run(run_id)
|
||
if not run:
|
||
return {"run_info": None, "failed_jobs": [], "summary": "Run not found"}
|
||
|
||
failed_jobs = self.gitea.get_failed_jobs_summary(run_id, max_lines_per_job=30)
|
||
|
||
summary_parts = []
|
||
for job in failed_jobs:
|
||
step = f"(步骤: {job['failed_step']})" if job["failed_step"] else ""
|
||
summary_parts.append(f"• {job['name']}{step}")
|
||
|
||
summary = "\n".join(summary_parts) if summary_parts else "无失败 job(可能还在运行中)"
|
||
|
||
return {
|
||
"run_info": run,
|
||
"failed_jobs": failed_jobs,
|
||
"summary": summary,
|
||
"total_jobs": len(self.gitea.get_run_jobs(run_id)),
|
||
}
|
||
|
||
def list_recent_runs(self, status=None, branch=None, limit=10):
|
||
"""列出最近的 runs"""
|
||
runs, total = self.gitea.list_runs(status=status, branch=branch, limit=limit)
|
||
return {"runs": runs, "total_count": total}
|
||
|
||
# ── 辅助方法 ────────────────────────────────────
|
||
|
||
@staticmethod
|
||
def _derive_overall_status(runs):
|
||
"""根据最近 runs 推导整体状态
|
||
|
||
Returns:
|
||
success: 最近一次成功
|
||
failing: 最近一次失败(连续失败)
|
||
flaky: 有失败有成功(最近一次失败
|
||
running: 有正在运行的
|
||
unknown: 未知
|
||
"""
|
||
if not runs:
|
||
return "no_runs"
|
||
|
||
# 检查是否有运行中的
|
||
running = [r for r in runs if r.get("status") != "completed"]
|
||
if running:
|
||
return "running"
|
||
|
||
# 看最近一次
|
||
latest = runs[0]
|
||
latest_conclusion = latest.get("conclusion", "unknown")
|
||
|
||
if latest_conclusion == "success":
|
||
return "success"
|
||
|
||
if latest_conclusion == "failure":
|
||
# 检查是否连续失败
|
||
consecutive_failures = 0
|
||
for r in runs:
|
||
if r.get("conclusion") == "failure":
|
||
consecutive_failures += 1
|
||
else:
|
||
break
|
||
|
||
# 看之前有没有成功
|
||
has_success = any(r.get("conclusion") == "success" for r in runs)
|
||
|
||
if has_success:
|
||
return "flaky"
|
||
return "failing"
|
||
|
||
return "unknown"
|
||
|
||
# ── 格式化输出 ────────────────────────────────────
|
||
|
||
@staticmethod
|
||
def format_branch_status(status_data):
|
||
"""格式化分支状态为人类可读文本"""
|
||
branch = status_data["branch"]
|
||
latest = status_data["latest_run"]
|
||
overall = status_data["overall_status"]
|
||
|
||
status_emoji = {
|
||
"success": "✅",
|
||
"failing": "🔴",
|
||
"flaky": "🟡",
|
||
"running": "🔄",
|
||
"no_runs": "⚪",
|
||
"unknown": "❓",
|
||
}.get(overall, "❓")
|
||
|
||
lines = [f"**CI 状态:{branch} 分支**", f"整体状态: {status_emoji} {overall}"]
|
||
|
||
if latest:
|
||
name = latest.get("name", "Unknown")
|
||
conclusion = latest.get("conclusion", latest.get("status", "unknown"))
|
||
run_id = latest.get("id", "")
|
||
created = latest.get("created_at", "")[:16].replace("T", " ")
|
||
run_url = f"{config.GITEA_URL}/{config.GITEA_REPO}/actions/runs/{run_id}"
|
||
lines.append(f"最新: [{name} #{run_id}]({run_url}) - {conclusion} ({created})")
|
||
|
||
recent = status_data["recent_runs"]
|
||
if len(recent) > 1:
|
||
lines.append(f"\n最近 {len(recent)} 次:")
|
||
for r in recent[:5]:
|
||
c = r.get("conclusion", r.get("status", "?"))
|
||
emoji = {"success": "✅", "failure": "❌", "skipped": "⏭️"}.get(c, "🔄")
|
||
lines.append(f" {emoji} #{r.get('id', '?')} {r.get('name', '?')[:30]} - {c}")
|
||
|
||
return "\n".join(lines)
|
||
|
||
@staticmethod
|
||
def format_pr_status(status_data):
|
||
"""格式化 PR 状态为人类可读文本"""
|
||
pr_num = status_data["pr_number"]
|
||
pr_title = status_data["pr_title"]
|
||
overall = status_data["overall_status"]
|
||
|
||
status_emoji = {
|
||
"success": "✅",
|
||
"failing": "🔴",
|
||
"flaky": "🟡",
|
||
"running": "🔄",
|
||
"no_runs": "⚪",
|
||
"pr_not_found": "❓",
|
||
"unknown": "❓",
|
||
}.get(overall, "❓")
|
||
|
||
pr_url = f"{config.GITEA_URL}/{config.GITEA_REPO}/pulls/{pr_num}"
|
||
lines = [
|
||
f"**CI 状态:PR #{pr_num}**",
|
||
f"标题: [{pr_title}]({pr_url})",
|
||
f"状态: {status_emoji} {overall}",
|
||
]
|
||
|
||
runs = status_data["runs"]
|
||
if runs:
|
||
lines.append(f"\nCI Runs ({len(runs)}):")
|
||
for r in runs[:5]:
|
||
c = r.get("conclusion", r.get("status", "?"))
|
||
emoji = {"success": "✅", "failure": "❌", "skipped": "⏭️"}.get(c, "🔄")
|
||
run_id = r.get("id", "?")
|
||
run_url = f"{config.GITEA_URL}/{config.GITEA_REPO}/actions/runs/{run_id}"
|
||
lines.append(f" {emoji} [{r.get('name', '?')[:30]} #{run_id}]({run_url}) - {c}")
|
||
|
||
return "\n".join(lines)
|
||
|
||
|
||
# ── CLI 入口 ──────────────────────────────────────────
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description="CI 状态查询")
|
||
parser.add_argument("--branch", help="查询指定分支的 CI 状态")
|
||
parser.add_argument("--pr", type=int, help="查询指定 PR 的 CI 状态")
|
||
parser.add_argument("--run-id", help="查询指定 run 的详情")
|
||
parser.add_argument("--detail", action="store_true", help="显示失败详情")
|
||
parser.add_argument("--limit", type=int, default=5, help="返回数量限制")
|
||
parser.add_argument("--status", help="按状态过滤: success/failure/running")
|
||
|
||
args = parser.parse_args()
|
||
|
||
query = CIQuery()
|
||
|
||
if args.run_id:
|
||
if args.detail:
|
||
result = query.get_failure_detail(args.run_id)
|
||
print(f"Run #{args.run_id} 失败详情:")
|
||
print(result["summary"])
|
||
if result["failed_jobs"]:
|
||
print("\n详细日志尾部:")
|
||
for job in result["failed_jobs"]:
|
||
print(f"\n--- {job['name']} ---")
|
||
print(job["log_tail"][:500] if job["log_tail"] else "无日志")
|
||
else:
|
||
run = query.gitea.get_run(args.run_id)
|
||
if run:
|
||
print(f"Run #{args.run_id}: {run.get('name')} - {run.get('conclusion', run.get('status'))}")
|
||
print(f"分支: {run.get('head_branch', '?')}")
|
||
print(f"触发: {run.get('event', '?')}")
|
||
else:
|
||
print(f"Run {args.run_id} 不存在")
|
||
elif args.pr:
|
||
result = query.get_pr_status(args.pr)
|
||
print(CIQuery.format_pr_status(result))
|
||
elif args.branch:
|
||
result = query.get_branch_status(args.branch, limit=args.limit)
|
||
print(CIQuery.format_branch_status(result))
|
||
elif args.status:
|
||
result = query.list_recent_runs(status=args.status, limit=args.limit)
|
||
for r in result["runs"]:
|
||
print(
|
||
f"#{r.get('id')} {r.get('name')[:40]} - {r.get('conclusion', r.get('status'))} ({r.get('head_branch', '?')})"
|
||
)
|
||
else:
|
||
parser.print_help()
|
||
return 1
|
||
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
sys.exit(main())
|