Files
xiaoxia-saas/scripts/ci/chatops/ci_query.py
T
xiaoxia 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
chore(ci): 同步main分支CI配置与scripts/ci脚本 - 与develop对齐
同步内容:
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依赖
2026-07-24 10:36:29 +08:00

297 lines
10 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 状态查询模块 - 查询 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())