From 09e9e04cce2f0ec0ab10b993143c5b890f806ac1 Mon Sep 17 00:00:00 2001 From: randypanding <66171646+randypanding@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:45:52 +0800 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=C2=A712=20=E6=B4=BB=E4=BD=93?= =?UTF-8?q?=E9=AA=8C=E8=AF=81=E9=98=B2=E8=B5=B7=E9=A3=9E=E7=AA=97=E5=8F=A3?= =?UTF-8?q?=E8=AF=AF=E6=8A=A5=20+=20liveness=20=E6=8C=87=E7=BA=B9=E5=BD=92?= =?UTF-8?q?=E4=B8=80=E5=8C=96=EF=BC=88P1-4=EF=BC=8CADR-0034=20=E4=BF=AE?= =?UTF-8?q?=E8=AE=A2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/governance-drift.yml | 4 +- governance/drift-check.sh | 110 +++++++++++-------------- 2 files changed, 53 insertions(+), 61 deletions(-) diff --git a/.github/workflows/governance-drift.yml b/.github/workflows/governance-drift.yml index b0b0026..7be22ed 100644 --- a/.github/workflows/governance-drift.yml +++ b/.github/workflows/governance-drift.yml @@ -73,7 +73,9 @@ jobs: # 指纹使去重失效):只取 DRIFT 行、归一化时长秒数(回填时限=NNNs→s)、 # 排序去重后哈希——语义集合不变则指纹跨日恒定,漂移新增/消除/内容实质 # 变化才产生新指纹。 - FP=$(grep '^DRIFT' drift-report.txt | sed -E 's/回填时限=[0-9]+s/回填时限=s/g' | sort -u | sha256sum | cut -d' ' -f1) + # §13 liveness 行含逐秒增长的年龄字段(updated NNNNs 前 / 已 NNNNs / 创建 NNNNs), + # 不归一化则同一卡死/活体缺失每小时产生新指纹、去重失效(RB-B2 同款) + FP=$(grep '^DRIFT' drift-report.txt | sed -E 's/回填时限=[0-9]+s/回填时限=s/g; s/updated [0-9]+s 前/updated s 前/g; s/已 [0-9]+s/已 s/g; s/创建 [0-9]+s/创建 s/g' | sort -u | sha256sum | cut -d' ' -f1) BODY="自动化检测发现以下漂移(运行 #${{ github.run_id }}): $(cat drift-report.txt) diff --git a/governance/drift-check.sh b/governance/drift-check.sh index 59d527b..9dc5dea 100644 --- a/governance/drift-check.sh +++ b/governance/drift-check.sh @@ -439,78 +439,68 @@ else [[ $REQ_MISSING -eq 0 ]] && ok "CI-Workflows 必需大版本指针存在($CW_REQUIRED_POINTERS)" fi -# ---------- 12. required check 活体存在性(P1-4,ADR-0034)---------- +# ---------- 12. required check 活体存在性(P1-4,ADR-0034;in-flight 防误报修订)---------- # 文本对账 ≠ 生效验证:ruleset JSON 完全正确的同时,required check 字符串精确 # 匹配可能实际为空(job 改名 / workflow 重构)→ "零 required check" → PR 裸奔。 -# 每个受管仓最近活动的 PR head(无 PR 活动时退化为默认分支 HEAD)上,必须存在 -# 每个 required check 名(从 rulesets/*.json 派生——单一真源)的 check run 且 -# conclusion 非空。fail-closed:check-runs 查询失败即判漂移,不用部分结果。 +# 每个受管仓的候选 head(最近更新的至多 3 个已合并/打开 PR + 默认分支 HEAD)上, +# 必须存在每个 required check 名(从 rulesets/*.json 派生——单一真源)的 check run +# 且 conclusion 非空。修订(2026-08-20 实测误报 mutual:三连新开 PR 的 CI 起飞 +# 窗口内 gate 尚未报 conclusion,被误判缺失): +# - CI 未完结(任一 check run 状态非 completed)的 head 不构成任何证据—— +# 既不算"缺 gate",也不算"有 gate"(防起飞窗口误报); +# - 默认分支 HEAD 纳入候选(stacked/连开 PR 场景下 PR head 可能同时 in-flight); +# - 关闭未合并的 PR head 不采样(废弃实验分支的残缺 CI 不构成活体证据); +# - 全部候选 in-flight 时显式 SKIP(非漂移非通过,下轮复核),查询失败仍 fail-closed。 REQ_CHECKS=$(jq -rs '[.[].rules[]? | select(.type == "required_status_checks") | .parameters.required_status_checks[].context] | unique | .[]' "$DIR"/rulesets/*.json) [[ -n "$REQ_CHECKS" ]] || { echo "FATAL: rulesets 未声明任何 required check——§12 活体验证失去判据"; exit 2; } epoch_of() { date -u -d "$1" +%s; } # ISO8601 → epoch(runner GNU date) for r in $REPOS; do jq -e --arg r "$r" '($r as $x | . | index($x)) != null' <<<"$EXCLUDES" >/dev/null && continue - LIVE_MISS=0; QUERY_FAIL=0 - # 第一判据(强):open PR 是活体漏洞本体——head 上已产生其他 check run(工作流 - # 已在跑)但缺任一 required check 名 = 改名/重构实锤,裸奔窗口开启,立即报。 - # 零 check run 的启动延迟窗口不在此报(§13(c) 以 liveness 阈值兜底),防误报。 - OPEN_PRS12=$(api "https://api.github.com/repos/$ORG/$r/pulls?state=open&per_page=30") - if jq -e 'type == "array"' <<<"$OPEN_PRS12" >/dev/null 2>&1; then - while IFS=$'\t' read -r pnum osha; do - [[ -n "$pnum" ]] || continue - CRS=$(api "https://api.github.com/repos/$ORG/$r/commits/$osha/check-runs?per_page=100") + # 候选 head:最近更新的至多 3 个已合并/打开 PR head sha + 默认分支 HEAD + PRS_RECENT=$(api "https://api.github.com/repos/$ORG/$r/pulls?state=all&sort=updated&direction=desc&per_page=20") + if jq -e 'type == "array"' <<<"$PRS_RECENT" >/dev/null 2>&1; then + HEADS=$(jq -r '[.[] | select(.state == "open" or .merged_at != null) | .head.sha][0:3] | .[]' <<<"$PRS_RECENT") + DBR=$(api "https://api.github.com/repos/$ORG/$r" | jq -r '.default_branch // "main"') + MSHA=$(api "https://api.github.com/repos/$ORG/$r/git/ref/heads/$DBR" | jq -r '.object.sha // empty') + [[ -n "$MSHA" ]] && HEADS="$HEADS"$' +'"$MSHA" + else + drift "repo '$r' PR 清单拉取失败,required check 活体验证无法执行(fail-closed)" + continue + fi + LIVE_MISS=0; QUERY_FAIL=0; ALL_INFLIGHT=1 + for ctx in $REQ_CHECKS; do + FOUND=0 + while IFS= read -r sha; do + [[ -n "$sha" ]] || continue + CRS=$(api "https://api.github.com/repos/$ORG/$r/commits/$sha/check-runs?per_page=100") if ! jq -e 'type == "object" and has("check_runs")' <<<"$CRS" >/dev/null 2>&1; then - drift "repo '$r' PR#$pnum check-runs 查询失败,required check 活体无法验证(fail-closed)" QUERY_FAIL=1; continue fi - N_ANY=$(jq '.check_runs | length' <<<"$CRS") - [[ "$N_ANY" -gt 0 ]] || continue # 启动延迟窗口,交 §13(c) - for ctx in $REQ_CHECKS; do - jq -e --arg c "$ctx" '[.check_runs[] | select(.name == $c)] | length > 0' <<<"$CRS" >/dev/null 2>&1 \ - || { drift "repo '$r' PR#$pnum required check '$ctx' 活体缺失:open PR 的工作流已产出其他 check run 但无 '$ctx'——job 改名/workflow 重构,裸奔窗口开启(ADR-0034 §12)"; LIVE_MISS=1; } - done - done < <(jq -r '.[] | [(.number|tostring), .head.sha] | @tsv' <<<"$OPEN_PRS12") - else - drift "repo '$r' open PR 清单拉取失败,required check 活体验证无法执行(fail-closed)" - QUERY_FAIL=1 - fi - # 第二判据(退化):无 open PR 时——最近活动的至多 3 个 PR head(再退化为默认 - # 分支 HEAD)上必须存在每个 required check 名且 conclusion 非空(改名合入 main - # 后连续 PR 均缺失的兜底检出)。 - if [[ $(jq 'length' <<<"$OPEN_PRS12") -eq 0 && $QUERY_FAIL -eq 0 ]]; then - PRS_RECENT=$(api "https://api.github.com/repos/$ORG/$r/pulls?state=all&sort=updated&direction=desc&per_page=20") - if jq -e 'type == "array"' <<<"$PRS_RECENT" >/dev/null 2>&1; then - HEADS=$(jq -r '[.[] | .head.sha][0:3][]' <<<"$PRS_RECENT") - [[ -n "$HEADS" ]] || HEADS=$(api "https://api.github.com/repos/$ORG/$r/git/ref/heads/main" | jq -r '.object.sha // empty') - else - drift "repo '$r' PR 清单拉取失败,required check 活体验证无法执行(fail-closed)" - QUERY_FAIL=1 - fi - if [[ -n "${HEADS:-}" && $QUERY_FAIL -eq 0 ]]; then - for ctx in $REQ_CHECKS; do - FOUND=0 - while IFS= read -r sha; do - [[ -n "$sha" ]] || continue - CRS=$(api "https://api.github.com/repos/$ORG/$r/commits/$sha/check-runs?per_page=100") - if ! jq -e 'type == "object" and has("check_runs")' <<<"$CRS" >/dev/null 2>&1; then - QUERY_FAIL=1; continue - fi - if jq -e --arg c "$ctx" '[.check_runs[] | select(.name == $c and .conclusion != null)] | length > 0' <<<"$CRS" >/dev/null 2>&1; then - FOUND=1; break - fi - done <<<"$HEADS" - if [[ $QUERY_FAIL -eq 1 && $FOUND -eq 0 ]]; then - drift "repo '$r' check-runs 查询失败,required check '$ctx' 活体无法验证(fail-closed)" - elif [[ $FOUND -ne 1 ]]; then - drift "repo '$r' required check '$ctx' 活体缺失:ruleset 文本正确但最近 PR head / main HEAD 均无该 check run——job 改名或 workflow 重构?(ADR-0034 §12 退化判据)" - LIVE_MISS=1 - fi - done + TOTAL=$(jq -r '.total_count // 0' <<<"$CRS") + [[ "$TOTAL" -eq 0 ]] && continue + # CI 未完结的 head:不构成证据(防起飞窗口误报——gate job 在依赖图末端, + # 新开 PR 的前几分钟 conclusion 必为 null,此时判"缺失"全是误报) + jq -e '[.check_runs[] | select(.status != "completed")] | length > 0' <<<"$CRS" >/dev/null 2>&1 && continue + ALL_INFLIGHT=0 + if jq -e --arg c "$ctx" '[.check_runs[] | select(.name == $c and .conclusion != null)] | length > 0' <<<"$CRS" >/dev/null 2>&1; then + FOUND=1; break + fi + done <<<"$HEADS" + if [[ $FOUND -ne 1 ]]; then + if [[ $QUERY_FAIL -eq 1 ]]; then + drift "repo '$r' check-runs 查询失败,required check '$ctx' 活体无法验证(fail-closed)" + LIVE_MISS=1 + elif [[ $ALL_INFLIGHT -eq 1 ]]; then + echo "SKIP required-check-live '$r'(候选 head(近 3 PR + main HEAD)全部 CI in-flight,本轮无法判定——非漂移,下轮复核)" + else + drift "repo '$r' required check '$ctx' 活体缺失:已完结 CI 的候选 head(近 3 PR + main HEAD)均无该 check run——job 改名或 workflow 重构?裸奔窗口已开启(ADR-0034 §12)" + LIVE_MISS=1 + fi fi - fi - [[ $LIVE_MISS -eq 0 && $QUERY_FAIL -eq 0 ]] && ok "required-check-live '$r'(open PR 无裸奔;${REQ_CHECKS//$'\n'/ } 活体齐备)" - unset HEADS + done + [[ $LIVE_MISS -eq 0 && $ALL_INFLIGHT -eq 0 ]] && ok "required-check-live '$r'(required check 在已完结候选 head 上齐备)" done # ---------- 13. PR liveness 侦测(P1-4,ADR-0034)---------- From 36be96c474ca12d3538385acff99b2223e3be0c9 Mon Sep 17 00:00:00 2001 From: randypanding <66171646+randypanding@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:56:24 +0800 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=C2=A712=20=E8=AF=AD=E4=B9=89?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E2=80=94=E2=80=94main=20HEAD=20=E4=BB=85?= =?UTF-8?q?=E4=BD=9C=E6=97=A0=20PR=20=E6=B4=BB=E5=8A=A8=E9=80=80=E5=8C=96?= =?UTF-8?q?=E8=BD=BD=E4=BD=93=EF=BC=8C=E4=B8=8D=E4=BD=9C=E8=B1=81=E5=85=8D?= =?UTF-8?q?=E6=9D=A5=E6=BA=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- governance/drift-check.sh | 37 ++++++++++++++++++++----------------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/governance/drift-check.sh b/governance/drift-check.sh index 9dc5dea..28884d9 100644 --- a/governance/drift-check.sh +++ b/governance/drift-check.sh @@ -442,29 +442,32 @@ fi # ---------- 12. required check 活体存在性(P1-4,ADR-0034;in-flight 防误报修订)---------- # 文本对账 ≠ 生效验证:ruleset JSON 完全正确的同时,required check 字符串精确 # 匹配可能实际为空(job 改名 / workflow 重构)→ "零 required check" → PR 裸奔。 -# 每个受管仓的候选 head(最近更新的至多 3 个已合并/打开 PR + 默认分支 HEAD)上, -# 必须存在每个 required check 名(从 rulesets/*.json 派生——单一真源)的 check run -# 且 conclusion 非空。修订(2026-08-20 实测误报 mutual:三连新开 PR 的 CI 起飞 -# 窗口内 gate 尚未报 conclusion,被误判缺失): -# - CI 未完结(任一 check run 状态非 completed)的 head 不构成任何证据—— -# 既不算"缺 gate",也不算"有 gate"(防起飞窗口误报); -# - 默认分支 HEAD 纳入候选(stacked/连开 PR 场景下 PR head 可能同时 in-flight); -# - 关闭未合并的 PR head 不采样(废弃实验分支的残缺 CI 不构成活体证据); -# - 全部候选 in-flight 时显式 SKIP(非漂移非通过,下轮复核),查询失败仍 fail-closed。 +# 判据:每个受管仓最近更新的至多 3 个已合并/打开 PR 的 head 上,必须存在每个 +# required check 名(从 rulesets/*.json 派生——单一真源)的 check run 且 +# conclusion 非空;无 PR 活动时退化为默认分支 HEAD。修订(2026-08-20 实测误报 +# mutual:三连新开 PR 的 CI 起飞窗口内 gate 尚未报 conclusion,被误判缺失): +# - CI 未完结(任一 check run 非 completed)的 head 不构成任何证据——既不算 +# "缺 gate"也不算"有 gate"(防起飞窗口误报);全部候选 in-flight 时显式 +# SKIP(非漂移,下轮复核) +# - 关闭未合并的 PR 不采样(废弃实验分支的残缺 CI 不构成证据) +# - 已完结 CI 的 PR head 缺 gate 仍照报(PR 群体缺 required check = CI 结构性 +# 异常或改名注入——T1 的检测对象;main HEAD 仅在无 PR 活动时作退化载体, +# 不作为豁免来源)。查询失败维持 fail-closed。 REQ_CHECKS=$(jq -rs '[.[].rules[]? | select(.type == "required_status_checks") | .parameters.required_status_checks[].context] | unique | .[]' "$DIR"/rulesets/*.json) [[ -n "$REQ_CHECKS" ]] || { echo "FATAL: rulesets 未声明任何 required check——§12 活体验证失去判据"; exit 2; } epoch_of() { date -u -d "$1" +%s; } # ISO8601 → epoch(runner GNU date) for r in $REPOS; do jq -e --arg r "$r" '($r as $x | . | index($x)) != null' <<<"$EXCLUDES" >/dev/null && continue - # 候选 head:最近更新的至多 3 个已合并/打开 PR head sha + 默认分支 HEAD PRS_RECENT=$(api "https://api.github.com/repos/$ORG/$r/pulls?state=all&sort=updated&direction=desc&per_page=20") if jq -e 'type == "array"' <<<"$PRS_RECENT" >/dev/null 2>&1; then HEADS=$(jq -r '[.[] | select(.state == "open" or .merged_at != null) | .head.sha][0:3] | .[]' <<<"$PRS_RECENT") - DBR=$(api "https://api.github.com/repos/$ORG/$r" | jq -r '.default_branch // "main"') - MSHA=$(api "https://api.github.com/repos/$ORG/$r/git/ref/heads/$DBR" | jq -r '.object.sha // empty') - [[ -n "$MSHA" ]] && HEADS="$HEADS"$' -'"$MSHA" + if [[ -z "$HEADS" ]]; then + # 无(已合并/打开的)PR 活动 → 退化为默认分支 HEAD + DBR=$(api "https://api.github.com/repos/$ORG/$r" | jq -r '.default_branch // "main"') + HEADS=$(api "https://api.github.com/repos/$ORG/$r/git/ref/heads/$DBR" | jq -r '.object.sha // empty') + [[ -n "$HEADS" ]] || { drift "repo '$r' 无 PR 活动且默认分支 HEAD 不可读,活体验证无载体(fail-closed)"; continue; } + fi else drift "repo '$r' PR 清单拉取失败,required check 活体验证无法执行(fail-closed)" continue @@ -489,13 +492,13 @@ for r in $REPOS; do fi done <<<"$HEADS" if [[ $FOUND -ne 1 ]]; then - if [[ $QUERY_FAIL -eq 1 ]]; then + if [[ $QUERY_FAIL -eq 1 && $ALL_INFLIGHT -eq 1 ]]; then drift "repo '$r' check-runs 查询失败,required check '$ctx' 活体无法验证(fail-closed)" LIVE_MISS=1 elif [[ $ALL_INFLIGHT -eq 1 ]]; then - echo "SKIP required-check-live '$r'(候选 head(近 3 PR + main HEAD)全部 CI in-flight,本轮无法判定——非漂移,下轮复核)" + echo "SKIP required-check-live '$r'(候选 head 全部 CI in-flight,本轮无法判定——非漂移,下轮复核)" else - drift "repo '$r' required check '$ctx' 活体缺失:已完结 CI 的候选 head(近 3 PR + main HEAD)均无该 check run——job 改名或 workflow 重构?裸奔窗口已开启(ADR-0034 §12)" + drift "repo '$r' required check '$ctx' 活体缺失:已完结 CI 的候选 head 均无该 check run——job 改名或 workflow 重构?裸奔窗口已开启(ADR-0034 §12)" LIVE_MISS=1 fi fi