Files
DKChan b9c2e86d13 工程+文档(CI 现查件堵假绿): 绿须坐实跑闸步真跑过,零触发批呈报「未验」
- 实证: run 55(零触发批 f1840fbc)的 job 步级读数=检出与域分流 success、导入缓存刷新与跑闸两步
  skipped,而 run conclusion 仍为 success;原 ci_check.sh 只看 conclusion,把「一条闸都没跑」
  读成绿。假绿比假红危险——它放行的是「红未修不开新批」这道开工门。
- tools/ci_check.sh: 绿分支改先取 jobs 再判——跑闸步全 skipped → 呈报「未验」退出码 4;job 列表
  为空/取不到步级读数/工作流里查不到跑闸步 → 呈报「不可判」退出码 2,一律禁当绿证;红态读数
  在 conclusion 与步级矛盾时显式报矛盾行。退出码表加 4=未验。
- 孤立验证五形态(真实远端读数): 3de8955f/run54 全步真跑→绿 exit0;f1840fbc/run55 跑闸步 skipped
  →未验 exit4;fb27dc2f/run51→红 exit1;5d7579e7/run56 in_progress→未落定 exit2;3c1e3ddf 未推
  提交→无 status→未落定 exit2。
- 口径同步: AGENTS.md 运行与验证首条判据句、process-conventions §1 第 3 条加「绿须坐实」段、
  test-conventions §8.7 中/低档证据句加「未验与不可判不作证据」+回补途径、迭代技能两镜像现查句
  (不入 git 面)。
- 闸: --docs 验收通过零新增红(跑批树内容=本提交内容)。
2026-11-04 06:29:18 +08:00

119 lines
5.4 KiB
Bash
Executable File
Raw Permalink 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 bash
# CI 现查件——按需读远端 run 的终局态,只呈报、不落盘(2026-11-04 用户裁:回执等待件与本地缓存判定退役)。
# 用法: tools/ci_check.sh [<提交hash>](缺省=当前 HEAD)
# 判据:Gitea commit status 只用来取本次任务的 run 链接,绿红一律认 run 自身的 status/conclusion,
# 且绿必须坐实「跑闸步真跑过」——零触发批的 run 照样建、照样 success,但跑闸步是 skipped,
# 一条闸都没跑,读成绿就是假绿(2026-11-04 run 55 实证)。run 未 completed(排队/在跑/尝试级中间态)呈报未落定。
# 调用面:开工前读态(红未修不开新批)、合回后想知道结果时手动查——合回流程不等待、不唤醒,呈报即回合止。
# 授权:GITEA_TOKEN 环境变量优先,否则读 git 凭据库(host=git.kuaimeo.com)。
# 退出码: 0=绿(跑闸步真跑过) 1=红 2=未落定/不可判/查不到 3=参数或令牌错误 4=未验(零触发批,跑闸步全跳过)
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)
def unknown(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("远端无该提交的 status——该批没推上去(或推的不是 master);零触发批有 run,走 run 侧读数")
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))
try:
jobs = get(api + "/actions/runs/%s/jobs" % run_id).get("jobs", []) or []
except Exception as e:
unknown("查 run %s 的 job 明细失败:%s——取不到步级读数就不下绿" % (run_id, e))
gate_steps = [x.get("conclusion") for j in jobs for x in (j.get("steps") or []) if "跑闸" in (x.get("name") or "")]
if rconclusion == "success":
if not jobs:
unknown("run %s conclusion=success 但 job 列表为空,无法确认闸是否真跑" % run_id)
if not gate_steps:
unknown("run %s 里没有「跑闸」步——工作流结构已改,本件的步级判据需随动(查 %s/actions/runs/%s)" % (run_id, web, run_id))
if all(c == "skipped" for c in gate_steps):
print("CI现查: 未验 | commit %s | run %s conclusion=success 但跑闸步全为 skipped——零触发批,远端一条闸都没跑" % (short(base), run_id))
sys.exit(4)
print("CI现查: 绿 | commit %s | run %s conclusion=success,跑闸步 %d 条真跑过(闸况明细查 job 日志)" % (short(base), run_id, len(gate_steps)))
sys.exit(0)
red_jobs = []
for j in 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))
if not red_jobs:
red_jobs.append(("<job 全为成功或跳过——conclusion=%s 与步级读数矛盾,取日志核对>" % rconclusion, []))
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