#!/usr/bin/env bash # CI 现查件——按需读远端 run 的终局态,只呈报、不落盘(2026-11-04 用户裁:回执等待件与本地缓存判定退役)。 # 用法: tools/ci_check.sh [<提交hash>](缺省=当前 HEAD) # 判据:Gitea commit status 只用来取本次任务的 run 链接,绿红一律认 run 自身的 status/conclusion—— # run 未 completed(排队/在跑/尝试级中间态)与零触发批(纯闸件/管线批不建任务)一律呈报「未落定」,不写红。 # 调用面:开工前读态(红未修不开新批)、合回后想知道结果时手动查——合回流程不等待、不唤醒,呈报即回合止。 # 授权:GITEA_TOKEN 环境变量优先,否则读 git 凭据库(host=git.kuaimeo.com)。 # 退出码: 0=绿 1=红 2=未落定/零触发/查不到 3=参数或令牌错误 set -u COMMON="$(git -C "$(dirname "$0")" rev-parse --path-format=absolute --git-common-dir 2>/dev/null)" \ || { echo "不在 git 仓库内"; exit 3; } REPO="$(dirname "$COMMON")" BASE="${1:-}" [ -n "$BASE" ] || BASE="$(git -C "$REPO" rev-parse HEAD)" BASE="$(git -C "$REPO" rev-parse --verify "${BASE}^{commit}")" || { echo "提交不存在: $BASE"; exit 3; } _origin="$(git -C "$REPO" remote get-url origin 2>/dev/null || true)" REPO_SLUG="$(printf '%s' "$_origin" | grep -oE '[^/:]+/[^/]+\.git$' | sed 's/\.git$//')" [ -n "$REPO_SLUG" ] || { echo "无法从 origin 解析 owner/repo: $_origin"; exit 3; } TOKEN="${GITEA_TOKEN:-}" if [ -z "$TOKEN" ]; then TOKEN="$(printf 'protocol=https\nhost=git.kuaimeo.com\n\n' | git credential fill 2>/dev/null | sed -n 's/^password=//p' | head -1)" fi [ -n "$TOKEN" ] || { echo "取不到 Gitea 令牌(设 GITEA_TOKEN 或先 git credential approve 存储)"; exit 3; } API="https://git.kuaimeo.com/api/v1/repos/$REPO_SLUG" WEB="https://git.kuaimeo.com/$REPO_SLUG" export PYTHONIOENCODING=utf-8 python -I - "$API" "$WEB" "$BASE" "$TOKEN" <<'PYEOF' import json, re, sys, urllib.request, urllib.error api, web, base, token = sys.argv[1:5] H = {"Authorization": "token " + token} def get(url, t=20): req = urllib.request.Request(url, headers=H) return json.load(urllib.request.urlopen(req, timeout=t)) def short(sha): return sha[:8] def undetermined(msg): print("CI现查: 未落定 | commit %s | %s" % (short(base), msg)) sys.exit(2) try: s = get(api + "/commits/%s/status" % base) except urllib.error.HTTPError as e: undetermined("查 commit status 失败 HTTP %s" % e.code) except Exception as e: undetermined("查 commit status 失败:%s" % e) statuses = s.get("statuses") or [] target = next((x.get("target_url", "") for x in statuses if x.get("target_url")), "") if not statuses: undetermined("远端无该提交的任务——零触发批(纯闸件/管线批不建任务),或推送未触发 CI") m = re.search(r"/actions/runs/(\d+)", target or "") if not m: undetermined("commit status 无 run 链接(state=%s),查 %s/actions" % (s.get("state"), web)) run_id = m.group(1) try: run = get(api + "/actions/runs/%s" % run_id) except Exception as e: undetermined("查 run %s 失败:%s" % (run_id, e)) rstatus, rconclusion = run.get("status"), run.get("conclusion") if rstatus != "completed": undetermined("run %s status=%s(runner 需开机在线;队列与日志见 %s/actions)" % (run_id, rstatus, web)) if rconclusion == "success": print("CI现查: 绿 | commit %s | run %s conclusion=success" % (short(base), run_id)) sys.exit(0) red_jobs = [] try: jobs = get(api + "/actions/runs/%s/jobs" % run_id) for j in jobs.get("jobs", []): if j.get("conclusion") in ("success", "skipped", None): continue bad_steps = [x.get("name") for x in (j.get("steps") or []) if x.get("conclusion") not in ("success", "skipped")] red_jobs.append((j.get("name"), bad_steps)) except Exception as e: red_jobs.append(("" % e, [])) print("CI现查: 红 | commit %s | run %s conclusion=%s" % (short(base), run_id, rconclusion)) for name, steps in red_jobs: print(" 红 job:%s" % name) for st in steps: print(" 失败步:%s" % st) print(" 日志:%s" % (target or "%s/actions/runs/%s" % (web, run_id))) sys.exit(1) PYEOF