- 缘由(实证): ci_watch.sh 以「commit status 离开 pending」作终局判据——run 37 开跑 36 秒即写 verdict=red,该 run 三分半后 conclusion=success(现查 API),同 commit 先红后绿;远端时代 16 条 红回执所引 run 现查 15 条真红 1 条假红。另两条稳定假红源: 超时档(零触发批远端不建任务——现查90abdd4a的 statuses=null,只要挂了轮询必等满 3000s 写红;runner 排队同形)与多路轮询重复覆写 (fb27dc2f三条红挤在 15 秒内)。修判据不如撤等待: 判据移到开工前一次现查,竞态与超时两类假红 结构性消失。 - tools/ci_check.sh(新): 单次读 run 自身 status/conclusion,未 completed 与零触发一律呈报未落定 不写红;红态列失败 job 与失败步;只呈报不落盘。退出码 0绿/1红/2未落定/3参数与令牌。 孤立验证四形态: fb27dc2f→红(run 51 conclusion=failure) / 90abdd4a→未落定(零触发) / f1840fbc→未落定(run 55 queued) / 39acdcaa→绿(run 48 success)。 - tools/hooks/post-merge: 只推送不再挂后台轮询;push_failed 判定不再落 ci-status.json,改大声呈报。 - 删 tools/ci_watch.sh 与 .zcode/hooks/ci-status-banner.sh(不入 git 面,主区直改)并从 .zcode/config.json 注销——该横幅在当前 harness 只挂 worktree-guard 的配置下本就不投递(本会话 ci-status 为 red 全程零横幅)。 - 口径同步: AGENTS.md 运行与验证首条、process-conventions §1 第3条(回执等待纪律→合回零等待纪律)与 第4条执法分层②、§8 第8条、test-conventions §3 与 §8.7 审计证据行(中/低档证据由落盘回执改为 可复核的现查读数=commit+run号+conclusion)、run_all_gates.sh 头注与 .gates gitignore 模板句、 迭代技能两镜像三处(入 git 面外另记 .zcode/.qoder)。 - 待办清单条目「ci_watch 中间态误写终局红回执」整条删除(以退役方式了结,非按原判据修复)。 - 在案真红一条随本批呈报未处理: run 51(迭代 51 合回fb27dc2f) 失败步=导入缓存刷新+跑闸, 归因待取 job 日志——红未修不开新批的门按用户发令特批越过。
101 lines
4.2 KiB
Bash
Executable File
101 lines
4.2 KiB
Bash
Executable File
#!/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(("<job明细获取失败:%s>" % 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
|