-
Notifications
You must be signed in to change notification settings - Fork 0
feat(drill): 缺席 fail-closed 演练脚本(W4-C4 .github#223,ADR-0069) #246
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| #!/usr/bin/env bash | ||
| # failclose-test.sh —— 缺席 fail-closed 演练(宪法 §6 / ADR-0069 决策 6 / .github#223 AC-3) | ||
| # | ||
| # 回归 #180 先例(管家缺席 → dead-man trip → 自动合并停):模拟"审计包/心跳 | ||
| # 缺席超时",断言 AUTO_MERGE_DISABLED 置位路径真的会触发。三层: | ||
| # 1) 谓词单测(无网络):心跳新鲜→不 trip / 缺席超时→trip——注入时间戳断言, | ||
| # 与 butler-heartbeat-watch 同一判据(age > deadman_stale_hours); | ||
| # 2) 真源检查(只读):当前真实心跳新鲜度 + 阈值(butler.yaml),只报告不动状态; | ||
| # 3) 置位路径实演(FAILCLOSE_DRY_RUN=0 时): | ||
| # PATCH org 变量 AUTO_MERGE_DISABLED=true → GET 读回断言 → **立即复位** → | ||
| # GET 读回断言 false,输出置位/复位时戳与窗口时长(记录进演练台账)。 | ||
| # 复位用 trap 保证异常退出也尝试——真置位不复位=打断线上自动合并(不可留)。 | ||
| # 端点勘误与 deadman-trip.sh 一致:POST 须打集合端点 /actions/variables。 | ||
| # 凭据不足时默认 FAILCLOSE_DRY_RUN=1:只输出"将要置位"的判定与精确 API 调用 | ||
| # 作为证据,不动真变量(不打断线上自动合并)。 | ||
| # 用法: [FAILCLOSE_DRY_RUN=1|0] [FAILCLOSE_STALE_HOURS=N] bash failclose-test.sh | ||
| # 退出码: 0=演练通过(含 dry-run 断言通过)| 1=断言失败 | 2=infra(API 不可达等) | ||
| set -uo pipefail | ||
|
|
||
| ORG="${DRILL_ORG:-Cloudbird-Software}" | ||
| CB=AUTO_MERGE_DISABLED | ||
| DRY_RUN="${FAILCLOSE_DRY_RUN:-1}" | ||
| THRESH_H="${FAILCLOSE_STALE_HOURS:-}" | ||
| # python3 解析(CI 恒有 python3;本地 python3 可能是商店 stub——实测可用性而非 command -v) | ||
| pick_py() { | ||
| local c | ||
| for c in "${PYTHON:-}" python3 python py -3; do | ||
| [[ -n "$c" ]] || continue | ||
| "$c" -c 'import sys, yaml; print("ok")' >/dev/null 2>&1 || continue | ||
| echo "$c"; return 0 | ||
| done | ||
| return 1 | ||
| } | ||
| PY3="$(pick_py)" || { echo "::error::无可用 python(含 pyyaml)——阈值真源不可读" >&2; exit 2; } | ||
| DIR="$(cd "$(dirname "$0")/.." && pwd)" | ||
| NOW() { date -u +%FT%TZ; } | ||
| INFRA=0 | ||
| SET_AT=""; RESET_AT="" | ||
|
|
||
| ok() { echo "OK $1"; } | ||
| act() { echo "ACT $1"; } | ||
| infra(){ echo "INFRA $1" >&2; INFRA=$((INFRA+1)); } | ||
| audit(){ echo "AUDIT | butler=failclose-drill | run_id=${GITHUB_RUN_ID:-local} | ts=$(NOW) | outcome=$1 | actions=$2"; } | ||
|
|
||
| # ---------- 1) 谓词单测(无网络;注入时间戳——BUTLER_DRY_RUN 类 env 注入模式) ---------- | ||
| # 谓词与 butler-heartbeat-watch 完全同式: AGE_S > THRESH_H*3600 → trip | ||
| trip_if_stale() { # $1=last_success_epoch $2=threshold_hours → echo trip|no-trip | ||
| local age=$(( $(date -u +%s) - $1 )) | ||
| if (( age > $2 * 3600 )); then echo "trip"; else echo "no-trip"; fi | ||
| } | ||
| predicate_selftest() { | ||
| local pass=0 fail=0 | ||
| # 新鲜心跳(30 分钟前)→ 不 trip | ||
| r=$(trip_if_stale "$(( $(date -u +%s) - 1800 ))" "${THRESH_H:-3}") | ||
| [[ "$r" == "no-trip" ]] && { ok "谓词: 心跳 30min 前且阈值 ${THRESH_H:-3}h → no-trip"; pass=$((pass+1)); } \ | ||
| || { echo "::error::谓词失败: 新鲜心跳被判 $r"; fail=$((fail+1)); } | ||
| # 缺席超时(365 天前)→ trip(模拟审计包/心跳缺席超时) | ||
| r=$(trip_if_stale "$(( $(date -u +%s) - 31536000 ))" "${THRESH_H:-3}") | ||
| [[ "$r" == "trip" ]] && { ok "谓词: 心跳缺席 365d > 阈值 ${THRESH_H:-3}h → trip(缺席即停判定在)"; pass=$((pass+1)); } \ | ||
| || { echo "::error::谓词失败: 缺席超时被判 $r——fail-closed 判定缺失"; fail=$((fail+1)); } | ||
| # 边界: 恰好等于阈值 → 不 trip(严格大于才停,与 heartbeat-watch 一致) | ||
| r=$(trip_if_stale "$(( $(date -u +%s) - $(( ${THRESH_H:-3} * 3600 )) ))" "${THRESH_H:-3}") | ||
| [[ "$r" == "no-trip" ]] && { ok "谓词: 恰在阈值=边界(严格>才 trip)"; pass=$((pass+1)); } \ | ||
| || { echo "::error::谓词失败: 边界语义漂移"; fail=$((fail+1)); } | ||
|
Comment on lines
+61
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 2. Flaky threshold boundary test predicate_selftest() computes the boundary case using one "now" timestamp but trip_if_stale() recomputes "now" internally, so a 1-second tick can turn an equality case into "age > threshold" and intermittently fail the drill. This makes quarterly evidence runs non-deterministic and can produce false failures. Agent Prompt
Comment on lines
+61
to
+64
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win [严重级别:中] 边界用例存在跨秒竞态,会偶发变红。 L62 在调用方取一次 🐛 建议修复-trip_if_stale() { # $1=last_success_epoch $2=threshold_hours → echo trip|no-trip
- local age=$(( $(date -u +%s) - $1 ))
+trip_if_stale() { # $1=last_success_epoch $2=threshold_hours [$3=now_epoch] → echo trip|no-trip
+ local now="${3:-$(date -u +%s)}"
+ local age=$(( now - $1 ))
if (( age > $2 * 3600 )); then echo "trip"; else echo "no-trip"; fi
}
@@
- r=$(trip_if_stale "$(( $(date -u +%s) - $(( ${THRESH_H:-3} * 3600 )) ))" "${THRESH_H:-3}")
+ local now; now=$(date -u +%s)
+ r=$(trip_if_stale "$(( now - ${THRESH_H:-3} * 3600 ))" "${THRESH_H:-3}" "$now")🧰 Tools🪛 Shellcheck (0.11.0)[info] 63-63: Note that A && B || C is not if-then-else. C may run when A is true. (SC2015) 🤖 Prompt for AI Agents |
||
| echo "PREDICATE pass=$pass fail=$fail" | ||
| [[ $fail -eq 0 ]] | ||
| } | ||
|
|
||
| # ---------- 2) 真源检查(只读) ---------- | ||
| threshold_from_butler() { | ||
| if [[ -z "$THRESH_H" ]]; then | ||
| # 经 stdin 喂文件(本地 Windows python 解析不了 MSYS 路径;CI Linux 同样适用) | ||
| THRESH_H=$("$PY3" -c 'import sys, yaml; print(yaml.safe_load(sys.stdin)["thresholds"]["deadman_stale_hours"])' \ | ||
| < "$DIR/policy/butler.yaml" | tr -d '\r') || { | ||
| echo "::error::butler.yaml thresholds.deadman_stale_hours 读取失败" >&2; return 2; } | ||
| fi | ||
| [[ "$THRESH_H" =~ ^[0-9]+([.][0-9]+)?$ ]] || { echo "::error::阈值非数值: $THRESH_H" >&2; return 2; } | ||
|
|
||
| } | ||
| probe_live_heartbeat() { # 只读:当前真实心跳新鲜度(不动任何状态) | ||
|
Comment on lines
+76
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 3. Decimal threshold breaks arithmetic threshold_from_butler() accepts decimal hours (regex allows floats), but trip_if_stale() uses Bash integer arithmetic with $2 * 3600, which errors or misbehaves for values like 0.5. This can cause incorrect trip/no-trip results when FAILCLOSE_STALE_HOURS is set to a decimal. Agent Prompt
|
||
| if [[ -z "${GH_TOKEN:-}" ]]; then | ||
| ok "真实心跳: 无 GH_TOKEN,跳过只读探针(谓词单测已覆盖判定;真置位模式另行强制凭据)" | ||
| return 0 | ||
| fi | ||
| local last age | ||
| last=$(gh api "repos/$ORG/.github/actions/workflows/butler-heartbeat.yml/runs?status=success&per_page=1" \ | ||
| --jq '.workflow_runs[0].updated_at // empty' 2>/dev/null) || { infra "heartbeat runs 查询失败"; return 0; } | ||
| if [[ -z "$last" ]]; then | ||
| ok "真实心跳: 无成功 run(若长期如此,watch 层会 trip——本演练不动状态)" | ||
| else | ||
| age=$(( ( $(date -u +%s) - $(date -u -d "$last" +%s) ) / 60 )) | ||
| ok "真实心跳: 最近成功 ${age}min 前(阈值 ${THRESH_H}h)——只读探针,无动作" | ||
| fi | ||
| } | ||
|
|
||
| # ---------- 3) 置位路径实演 ---------- | ||
| var_get() { gh api "orgs/$ORG/actions/variables/$CB" --jq .value 2>/dev/null || echo "ABSENT"; } | ||
|
|
||
| var_set() { # $1=true|false —— 与 deadman-trip.sh 同端点(POST 打集合端点) | ||
| if ! gh api -X PATCH "orgs/$ORG/actions/variables/$CB" -f name="$CB" -f value="$1" >/dev/null 2>&1; then | ||
| gh api -X POST "orgs/$ORG/actions/variables" -f name="$CB" -f value="$1" -f visibility=all >/dev/null 2>&1 | ||
| fi | ||
| } | ||
|
Comment on lines
+97
to
+101
|
||
| reset_trap() { # 异常退出也复位(真置位不可留——宪法 §6 停机须人工确认后人工复位, | ||
| # 但**演练**置位必须在演练内复位并留时戳;此 trap 只兜异常,正常路径下方显式复位) | ||
| if [[ -n "$SET_AT" && -z "$RESET_AT" ]]; then | ||
| var_set false && infra "异常退出兜底复位已执行(详见台账)" | ||
| fi | ||
|
Comment on lines
+104
to
+106
Comment on lines
+104
to
+106
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 4. Trap reset can fail silently reset_trap() only logs/marks infra when the fallback reset succeeds, but does not emit an error, manual recovery command, or force a non-zero outcome if the reset attempt fails. If the script crashes after setting AUTO_MERGE_DISABLED=true and the reset fails (network/permissions), the org-level automerge breaker may remain enabled without a clear loud signal. Agent Prompt
|
||
| } | ||
|
Comment on lines
+102
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win [严重级别:高] L125 在读回校验(L127)之前设置 修复方向:只在读回校验通过后记录 🛡️ 建议修复 var_set() { # $1=true|false —— 与 deadman-trip.sh 同端点(POST 打集合端点)
if ! gh api -X PATCH "orgs/$ORG/actions/variables/$CB" -f name="$CB" -f value="$1" >/dev/null 2>&1; then
- gh api -X POST "orgs/$ORG/actions/variables" -f name="$CB" -f value="$1" -f visibility=all >/dev/null 2>&1
+ gh api -X POST "orgs/$ORG/actions/variables" -f name="$CB" -f value="$1" -f visibility=all >/dev/null 2>&1 || return 1
fi
}
@@
act "立即复位: $CB=false($(NOW))"
- var_set false
- RESET_AT=$(NOW)
+ local reset_ts; reset_ts=$(NOW)
+ var_set false || infra "复位 API 调用失败(下方读回将判定)"
v=$(var_get)
[[ "$v" == "false" ]] || { echo "::error::复位后读回=$v(期望 false)——必须人工立即复位: gh api -X PATCH orgs/$ORG/actions/variables/$CB -f name=$CB -f value=false" >&2; audit real-fail '{"breaker":"reset-readback-mismatch"}'; return 1; }
+ RESET_AT="$reset_ts" # 仅在读回=false 后记账,异常路径仍由 trap 兜底Also applies to: 120-130 🧰 Tools🪛 Shellcheck (0.11.0)[info] 102-107: This function is never invoked. Check usage (or ignored if invoked indirectly). (SC2329) 🤖 Prompt for AI Agents |
||
| drill_set_reset() { | ||
| if [[ "$DRY_RUN" != "0" ]]; then | ||
| ok "DRY-RUN 演练: 缺席超时谓词已判 trip,**将要执行** 置位: gh api -X PATCH orgs/$ORG/actions/variables/$CB -f name=$CB -f value=true" | ||
| ok "DRY-RUN 证据: 判定输出(上方谓词行)即 fail-closed 触发链的置位前一刻证据;未动真实变量" | ||
| audit dry-run-pass '{"breaker":"would-set","variable":"'"$CB"'"}' | ||
| return 0 | ||
| fi | ||
| [[ -n "${GH_TOKEN:-}" ]] || { echo "::error::真置位需 GH_TOKEN(org admin 写变量)" >&2; return 2; } | ||
| trap reset_trap EXIT | ||
| act "实演置位: $CB=true($(NOW))" | ||
| SET_AT=$(NOW) | ||
| var_set true | ||
| local v; v=$(var_get) | ||
| [[ "$v" == "true" ]] || { echo "::error::置位后读回=$v(期望 true)——置位路径失效"; audit real-fail '{"breaker":"set-readback-mismatch"}'; return 1; } | ||
| ok "置位路径验证: 读回=true(缺席即停路径触发,#180 先例回归通过)" | ||
| act "立即复位: $CB=false($(NOW))" | ||
| var_set false | ||
| RESET_AT=$(NOW) | ||
| v=$(var_get) | ||
| [[ "$v" == "false" ]] || { echo "::error::复位后读回=$v(期望 false)——必须人工立即复位: gh api -X PATCH orgs/$ORG/actions/variables/$CB -f name=$CB -f value=false" >&2; audit real-fail '{"breaker":"reset-readback-mismatch"}'; return 1; } | ||
| ok "复位验证: 读回=false(复位时戳 $RESET_AT,置位窗口已闭合,线上自动合并不受影响)" | ||
| audit real-pass '{"breaker":"set-and-reset","set_at":"'"$SET_AT"'","reset_at":"'"$RESET_AT"'"}' | ||
| trap - EXIT | ||
| } | ||
|
|
||
| main() { | ||
| audit running '{"phase":"start","dry_run":"'"$DRY_RUN"'"}' | ||
| threshold_from_butler || exit 2 # 阈值先验证后使用(注入值/真源值同一条路径) | ||
| predicate_selftest || { audit predicate-fail '{"phase":"predicate"}'; exit 1; } | ||
| probe_live_heartbeat | ||
| drill_set_reset; rc=$? | ||
| if [[ $INFRA -gt 0 ]]; then audit infra-fail "{\"infra_failures\":$INFRA}"; exit 2; fi | ||
| exit $rc | ||
| } | ||
| main | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| #!/usr/bin/env bash | ||
| # test-failclose.sh —— 缺席 fail-closed 演练 dry-run 断言自测(W4-C4 AC-3) | ||
| # 只跑 dry-run 路径(不动 org 真变量 AUTO_MERGE_DISABLED——真置位路径由季度 | ||
| # 演练/显式 FAILCLOSE_DRY_RUN=0 承担,本测试永不触发)。 | ||
| set -uo pipefail | ||
| HERE="$(cd "$(dirname "$0")" && pwd)" | ||
| ROOT="$(cd "$HERE/.." && pwd)" | ||
| PASS=0; FAIL=0 | ||
| TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT | ||
|
|
||
| run_fc() { env -u GH_TOKEN FAILCLOSE_STALE_HOURS=3 FAILCLOSE_DRY_RUN=1 bash "$ROOT/failclose-test.sh"; } | ||
|
|
||
| echo "== 1) dry-run 全链通过(谓词表 + 将要置位证据,不动真变量)" | ||
| OUT=$(run_fc | tee "$TMP/fc.log"); RC=${PIPESTATUS[0]} | ||
|
|
||
| [[ $RC -eq 0 ]] && grep -q "缺席超时.*trip\|→ trip" "$TMP/fc.log" \ | ||
| && { PASS=$((PASS+1)); echo "ok 谓词: 缺席超时→trip 判定在"; } \ | ||
| || { FAIL=$((FAIL+1)); echo "FAIL 谓词/退出码 rc=$RC"; } | ||
| grep -q "心跳 30min 前.*no-trip\|新鲜.*no-trip" "$TMP/fc.log" \ | ||
| && { PASS=$((PASS+1)); echo "ok 谓词: 新鲜心跳不误杀"; } \ | ||
| || { FAIL=$((FAIL+1)); echo "FAIL 新鲜心跳谓词"; } | ||
| grep -q "将要执行.*AUTO_MERGE_DISABLED\|将要执行" "$TMP/fc.log" \ | ||
| && { PASS=$((PASS+1)); echo "ok dry-run 输出'将要置位'判定证据(凭据不足时的替代证据路径)"; } \ | ||
| || { FAIL=$((FAIL+1)); echo "FAIL dry-run 证据行缺失"; } | ||
| grep -q "dry-run-pass" "$TMP/fc.log" \ | ||
| && { PASS=$((PASS+1)); echo "ok AUDIT dry-run-pass 留痕"; } \ | ||
| || { FAIL=$((FAIL+1)); echo "FAIL dry-run 审计行缺失"; } | ||
|
|
||
| echo "== 2) fail-closed 语义(真置位拒绝无凭据执行,不半演)" | ||
| OUT=$(env -u GH_TOKEN FAILCLOSE_DRY_RUN=0 bash "$ROOT/failclose-test.sh" 2>&1); RC=$? | ||
| if [[ $RC -eq 2 ]] && grep -q "真置位需 GH_TOKEN" <<<"$OUT"; then | ||
| PASS=$((PASS+1)); echo "ok 真置位模式缺凭据→infra 拒绝(exit2)" | ||
| else FAIL=$((FAIL+1)); echo "FAIL 真置位无凭据 rc=$RC"; fi | ||
|
|
||
| echo "== 3) 阈值非法值被拒(真源防护)" | ||
| OUT=$(env -u GH_TOKEN FAILCLOSE_STALE_HOURS=abc FAILCLOSE_DRY_RUN=1 bash "$ROOT/failclose-test.sh" 2>&1); RC=$? | ||
| [[ $RC -eq 2 ]] && { PASS=$((PASS+1)); echo "ok 非数值阈值→exit2"; } \ | ||
| || { FAIL=$((FAIL+1)); echo "FAIL 非法阈值 rc=$RC"; } | ||
|
|
||
| echo "failclose 自测: pass=$PASS fail=$FAIL" | ||
| [[ $FAIL -eq 0 ]] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| #!/usr/bin/env bash | ||
| # test-history.sh —— 台账 append-only + 红率/难度趋势聚合自测(W4-C4 AC-4) | ||
| set -uo pipefail | ||
| HERE="$(cd "$(dirname "$0")" && pwd)" | ||
| ROOT="$(cd "$HERE/.." && pwd)" | ||
| source "$(cd "$(dirname "$0")" && pwd)/lib.sh" | ||
| PYTHON="$(pick_py)" || { echo "::error::无可用 python(含 pyyaml)"; exit 2; } | ||
| PASS=0; FAIL=0 | ||
| TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT | ||
| H="$TMP/history.jsonl" | ||
| D="$ROOT/drill.py" | ||
|
|
||
| rec() { printf '%s' "$1"; } | ||
|
|
||
| echo "== 1) record 追加 + append-only 破坏被拒" | ||
| "$PYTHON" "$D" record --history "$H" --json '{"ts":"2026-08-22T04:23:11Z","kind":"seed-drill","run_id":"r1","sample_id":"hygiene-gitleaks-aws-key","difficulty":"easy","gate":"org-hygiene","target_repo":".github","branch":"drill/seed-20260822","head_sha":"aaa","surface":"draft-pr","verdict":"red"}' >/dev/null | ||
| [[ $(wc -l < "$H") -eq 1 ]] && { PASS=$((PASS+1)); echo "ok 首条追加"; } || { FAIL=$((FAIL+1)); echo "FAIL 首条追加"; } | ||
| "$PYTHON" "$D" record --history "$H" --json '{"ts":"2026-08-22T05:00:00Z","kind":"seed-drill","run_id":"r2","sample_id":"gate-yaml-parse-corrupt","difficulty":"medium","gate":"gate","target_repo":".github","verdict":"red"}' >/dev/null | ||
| [[ $(wc -l < "$H") -eq 2 ]] && { PASS=$((PASS+1)); echo "ok 顺序追加"; } || { FAIL=$((FAIL+1)); echo "FAIL 顺序追加"; } | ||
| "$PYTHON" "$D" record --history "$H" --json '{"ts":"2026-08-21T00:00:00Z","kind":"seed-drill","run_id":"r3"}' >/dev/null 2>&1 | ||
| [[ $? -ne 0 ]] && { PASS=$((PASS+1)); echo "ok 时间戳回拨被拒(append-only)"; } || { FAIL=$((FAIL+1)); echo "FAIL 回拨被放行"; } | ||
| "$PYTHON" "$D" record --history "$H" --json '{"ts":"2026-08-22T06:00:00Z","kind":"seed-drill","run_id":"r1"}' >/dev/null 2>&1 | ||
| [[ $? -ne 0 ]] && { PASS=$((PASS+1)); echo "ok 同 run 重复记录被拒"; } || { FAIL=$((FAIL+1)); echo "FAIL 重复 run 被放行"; } | ||
| "$PYTHON" "$D" record --history "$H" --json 'not-json' >/dev/null 2>&1 | ||
| [[ $? -ne 0 ]] && { PASS=$((PASS+1)); echo "ok 畸形 JSON 被拒"; } || { FAIL=$((FAIL+1)); echo "FAIL 畸形被放行"; } | ||
|
Comment on lines
+20
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win 让负向用例同时验证拒绝和无写入。
此外, Also applies to: 50-52 🧰 Tools🪛 Shellcheck (0.11.0)[style] 21-21: Check exit code directly with e.g. 'if ! mycmd;', not indirectly with $?. (SC2181) [info] 21-21: Note that A && B || C is not if-then-else. C may run when A is true. (SC2015) [style] 23-23: Check exit code directly with e.g. 'if ! mycmd;', not indirectly with $?. (SC2181) [info] 23-23: Note that A && B || C is not if-then-else. C may run when A is true. (SC2015) [style] 25-25: Check exit code directly with e.g. 'if ! mycmd;', not indirectly with $?. (SC2181) [info] 25-25: Note that A && B || C is not if-then-else. C may run when A is true. (SC2015) 🤖 Prompt for AI Agents |
||
| PRE=$(head -1 "$H") | ||
| "$PYTHON" "$D" record --history "$H" --json '{"ts":"2026-08-22T07:00:00Z","kind":"seed-drill","run_id":"r4","difficulty":"hard","gate":"gate","verdict":"red"}' >/dev/null | ||
| [[ "$(head -1 "$H")" == "$PRE" ]] && { PASS=$((PASS+1)); echo "ok 追加不改写既有行"; } || { FAIL=$((FAIL+1)); echo "FAIL 既有行被改写"; } | ||
|
|
||
| echo "== 2) redrate 聚合(红率 + 难度趋势 + 零分母诚实口径 + 告警出口)" | ||
| OUT=$("$PYTHON" "$D" redrate --history "$H") | ||
| echo "$OUT" | "$PYTHON" -c ' | ||
| import json, sys | ||
| d = json.load(sys.stdin) | ||
| assert d["seed_drills"] == 3, d | ||
| assert d["verdicts"] == {"red": 3}, d | ||
| assert d["red_rate"] == 1.0, d | ||
| day = d["difficulty_trend_by_day"]["2026-08-22"] | ||
| assert day.get("easy") == 1 and day.get("medium") == 1 and day.get("hard") == 1, day | ||
| ' && { PASS=$((PASS+1)); echo "ok 红=3/3 rate=1.0 且难度分布可见"; } \ | ||
| || { FAIL=$((FAIL+1)); echo "FAIL 聚合口径: $OUT"; } | ||
| printf '%s\n' '{"ts":"2026-08-29T04:23:11Z","kind":"seed-drill","run_id":"r5","difficulty":"easy","gate":"org-hygiene","verdict":"green"}' >> "$H" | ||
| OUT=$("$PYTHON" "$D" redrate --history "$H") | ||
| echo "$OUT" | "$PYTHON" -c ' | ||
| import json, sys | ||
| d = json.load(sys.stdin) | ||
| assert d["red_rate"] == 0.75, d # 3红/4可判定(no-surface 不入分母) | ||
| ' && { PASS=$((PASS+1)); echo "ok green 入账拉低红率(0.75)"; } \ | ||
|
Comment on lines
+42
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- test-history.sh ---'
sed -n '1,120p' governance/drill/tests/test-history.sh
printf '%s\n' '--- drill.py aggregation and related definitions ---'
sed -n '180,285p' governance/drill/drill.py
printf '%s\n' '--- references to no-surface, red_rate, and surface ---'
rg -n -C 3 'no-surface|red_rate|surface' governance/drill governance standards .github 2>/dev/null || trueRepository: Cloudbird-Software/.github Length of output: 20348 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- contract references ---'
rg -n -i -C 4 'no[- ]surface|no-surface|可判定|ADR-0069|red.?rate|红率' . --glob '!governance/drill/history.jsonl' || true
printf '%s\n' '--- standalone aggregation probe ---'
python3 - <<'PY'
rows = [
{"kind": "seed-drill", "verdict": "red", "surface": "draft-pr"},
{"kind": "seed-drill", "verdict": "red"},
{"kind": "seed-drill", "verdict": "red"},
{"kind": "seed-drill", "verdict": "green"},
]
verdicts = {}
for row in rows:
verdict = row.get("verdict", "?")
verdicts[verdict] = verdicts.get(verdict, 0) + 1
red = verdicts.get("red", 0)
green = verdicts.get("green", 0)
denom = red + green
rate = round(red / denom, 4) if denom else None
assert len([row for row in rows if row.get("kind") == "seed-drill"]) == 4
assert verdicts == {"red": 3, "green": 1}
assert rate == 0.75
print({"verdicts": verdicts, "denom": denom, "red_rate": rate,
"missing_surface_included": True})
PYRepository: Cloudbird-Software/.github Length of output: 31879 区分
🧰 Tools🪛 Shellcheck (0.11.0)[info] 48-48: Note that A && B || C is not if-then-else. C may run when A is true. (SC2015) 🤖 Prompt for AI Agents |
||
| || { FAIL=$((FAIL+1)); echo "FAIL 红率计算: $OUT"; } | ||
| "$PYTHON" "$D" redrate --history "$H" --fail-unhealthy >/dev/null 2>&1 | ||
| [[ $? -ne 0 ]] && { PASS=$((PASS+1)); echo "ok 红率<100% 触发告警退出(AC-4 告警出口)"; } \ | ||
| || { FAIL=$((FAIL+1)); echo "FAIL 告警出口未触发"; } | ||
| H2="$TMP/empty.jsonl"; : > "$H2" | ||
| OUT=$("$PYTHON" "$D" redrate --history "$H2") | ||
| echo "$OUT" | grep -q '"red_rate": null' && { PASS=$((PASS+1)); echo "ok 零分母→null(不除零不出假 100%)"; } \ | ||
| || { FAIL=$((FAIL+1)); echo "FAIL 零分母口径: $OUT"; } | ||
|
|
||
| echo "台账自测: pass=$PASS fail=$FAIL" | ||
| [[ $FAIL -eq 0 ]] | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: Cloudbird-Software/.github
Length of output: 342
🏁 Script executed:
Repository: Cloudbird-Software/.github
Length of output: 10814
🏁 Script executed:
Repository: Cloudbird-Software/.github
Length of output: 6516
统一阈值的小数处理
governance/policy/butler.yaml当前值为整数3,但FAILCLOSE_STALE_HOURS和STALE_HOURS_OVERRIDE的正则仍接受1.5。小数进入 Bash 算术表达式后会产生语法错误,trip_if_stale返回no-trip,边界测试也会失败。请改用浮点比较,或将正则收紧为^[0-9]+$并更新错误信息。🤖 Prompt for AI Agents