Files
xiaoxia-saas/scripts/ci/chatops/ci_trigger.py
T
CI Bot 55347f962c
CI Build & Deploy Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 15s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 16s
AI Code Review / AI Code Review (pull_request) Failing after 23s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 49s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Successful in 1m55s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 2m52s
Auto Approve CI PRs / Auto Approve on CI Green (pull_request) Successful in 3m17s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m31s
Auto Merge CI PRs / Auto Merge on CI Green + Approved (pull_request) Successful in 3m56s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 20s
feat(ci): P2-2 ChatOps集成 - 飞书机器人对接Gitea Actions
新增 scripts/ci/chatops/ 目录,Python实现,纯工具链扩展不动业务代码。

3个核心模块:
- ci_query.py    - 查询CI状态(分支/PR/失败详情)
- ci_trigger.py  - 触发重跑(rerun失败job/整个workflow/取消)
- feishu_notify.py - CI事件飞书通知(失败/恢复/E2E摘要)

配套基础设施:
- config.py      - 配置化,所有token/webhook走环境变量
- gitea_client.py - Gitea API统一封装(urllib实现,无第三方依赖)
- webhook_server.py - FastAPI webhook接收服务(可选依赖)

通知规则(最高频使用场景):
- main/develop分支CI失败 → 飞书告警卡片(含失败摘要+一键重跑)
- 分支从失败恢复 → 绿色恢复通知(含故障时长)
- E2E测试失败 → 单独摘要通知
- 连续失败不重复告警(状态缓存去重)

飞书交互命令(第一版已实现解析+执行):
- /ci status [branch]   - 查询分支CI状态
- /ci rerun <run-id>    - 重跑失败jobs
- /ci rerun latest [branch] - 重跑最近一次失败的run
- /ci help              - 帮助

black+ruff全绿,与现有ci脚本风格一致。
2026-07-18 18:38:11 +08:00

170 lines
5.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""
CI 触发模块 - 重新运行失败 job、重跑整个 workflow、取消 run
支持操作:
- rerun_failed: 重跑失败的 jobs
- rerun_all: 重跑整个 workflow
- cancel: 取消运行中的 run
用法:
python3 scripts/ci/chatops/ci_trigger.py --run-id 456 --action rerun_failed
python3 scripts/ci/chatops/ci_trigger.py --run-id 456 --action rerun_all
python3 scripts/ci/chatops/ci_trigger.py --run-id 456 --action cancel
设计:
- 与飞书机器人 /ci rerun 命令对接
- 操作前自动校验 run 状态,避免无效操作
"""
import argparse
import sys
from . import config
from .gitea_client import GiteaClient
class CITrigger:
"""CI 操作触发器"""
def __init__(self, gitea_client=None):
self.gitea = gitea_client or GiteaClient()
# ── 触发操作 ─────────────────────────────────────
def rerun_failed(self, run_id):
"""重跑失败的 jobs
Returns:
dict: {success, message, new_run_id?}
"""
run = self.gitea.get_run(run_id)
if not run:
return {"success": False, "message": f"Run {run_id} 不存在"}
status = run.get("status", "")
if status != "completed":
return {
"success": False,
"message": f"Run {run_id} 当前状态为 {status},仅 completed 状态才能重跑",
}
result = self.gitea.rerun_failed_jobs(run_id)
if result is None:
return {"success": False, "message": "重跑请求失败"}
# Gitea rerun 后返回的 run id 通常不变(复用原 run)
return {
"success": True,
"message": f"已触发重跑失败 jobs: Run #{run_id}",
"run_id": run_id,
"run_url": f"{config.GITEA_URL}/{config.GITEA_REPO}/actions/runs/{run_id}",
}
def rerun_all(self, run_id):
"""重跑整个 workflow run
Returns:
dict: {success, message, run_id, run_url}
"""
run = self.gitea.get_run(run_id)
if not run:
return {"success": False, "message": f"Run {run_id} 不存在"}
status = run.get("status", "")
if status == "running" or status == "pending":
return {
"success": False,
"message": f"Run {run_id} 正在运行中,无需重跑",
}
result = self.gitea.rerun_run(run_id)
if result is None:
return {"success": False, "message": "重跑请求失败"}
return {
"success": True,
"message": f"已触发完整重跑: Run #{run_id}",
"run_id": run_id,
"run_url": f"{config.GITEA_URL}/{config.GITEA_REPO}/actions/runs/{run_id}",
}
def cancel_run(self, run_id):
"""取消运行中的 run
Returns:
dict: {success, message}
"""
run = self.gitea.get_run(run_id)
if not run:
return {"success": False, "message": f"Run {run_id} 不存在"}
status = run.get("status", "")
if status == "completed":
return {
"success": False,
"message": f"Run {run_id} 已完成,无需取消",
}
result = self.gitea.cancel_run(run_id)
if result is None:
return {"success": False, "message": "取消请求失败"}
return {
"success": True,
"message": f"已取消 Run #{run_id}",
"run_id": run_id,
}
def rerun_latest_failed(self, branch="develop", workflow_id=None):
"""重跑指定分支最近一次失败的 run
用于快速恢复场景,不需要先查 run_id
"""
runs, _ = self.gitea.list_runs(branch=branch, workflow_id=workflow_id, status="failure", limit=5)
if not runs:
return {"success": False, "message": f"{branch} 分支没有失败的 run"}
latest = runs[0]
run_id = latest.get("id")
return self.rerun_failed(run_id)
# ── CLI 入口 ──────────────────────────────────────────
def main():
parser = argparse.ArgumentParser(description="CI 触发操作")
parser.add_argument("--run-id", required=True, help="Workflow Run ID")
parser.add_argument(
"--action",
required=True,
choices=["rerun_failed", "rerun_all", "cancel"],
help="操作类型",
)
args = parser.parse_args()
trigger = CITrigger()
if args.action == "rerun_failed":
result = trigger.rerun_failed(args.run_id)
elif args.action == "rerun_all":
result = trigger.rerun_all(args.run_id)
elif args.action == "cancel":
result = trigger.cancel_run(args.run_id)
else:
print(f"未知操作: {args.action}")
return 1
status = "✅" if result["success"] else "❌"
print(f"{status} {result['message']}")
if result.get("run_url"):
print(f" {result['run_url']}")
return 0 if result["success"] else 1
if __name__ == "__main__":
sys.exit(main())