From 2b4cc9a5bfb5e6581e33ce84752a5c8fc36309a3 Mon Sep 17 00:00:00 2001 From: randypanding <66171646+randypanding@users.noreply.github.com> Date: Thu, 20 Aug 2026 13:59:58 +0800 Subject: [PATCH 1/8] =?UTF-8?q?governance:=20auto-fix=20=E4=B8=8A=E9=99=90?= =?UTF-8?q?=E6=89=A7=E6=B3=95=20+=20=E6=88=90=E6=9C=AC=E7=86=94=E6=96=AD?= =?UTF-8?q?=EF=BC=88P2-8=EF=BC=8CADR-0040=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- governance/policy/automation-limits.yaml | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 governance/policy/automation-limits.yaml diff --git a/governance/policy/automation-limits.yaml b/governance/policy/automation-limits.yaml new file mode 100644 index 0000000..6b039aa --- /dev/null +++ b/governance/policy/automation-limits.yaml @@ -0,0 +1,48 @@ +version: 1 +# ============================================================================ +# automation-limits.yaml —— 无人值守自动化生存护栏(auto-fix 上限 + 额度/成本熔断) +# 真源: 本文件(阈值声明);执行: governance/auto-fix-limit.sh + governance/cost-check.sh +# (.github/workflows/auto-fix-limit.yml / cost-check.yml 周期调用) +# 决策: ADR-0040(P2-8,.github issue #93 / 计划 #81) +# 注: 熔断标志(org 变量 AUTO_MERGE_DISABLED)是运行态标志,刻意不纳入 +# expected-state.json——置位/复位是运行动作而非期望状态,纳入会造成 +# "人工复位后 drift-check 反向改回去"的对账冲突(ADR-0040 后果节)。 +# ============================================================================ + +auto_fix: + # 修复循环上限:PR 的 gate 失败次数达到 max_attempts → 撤 auto-merge + 关 PR + 开 issue + max_attempts: 3 + # 计数对象:required check 名(BP-2,全组织统一 gate) + check_name: gate + # 默认作用域:agent 身份(AG-1 App slug)创建的 open PR;其他 PR 打 opt_in_label 纳入同路径 + # (脚本按 slug 宽容匹配 REST "[bot]" 与 GraphQL "app/" 两种作者登录名形态) + author: cloudbrid-agent + opt_in_label: auto-fix-loop + # 视为一次失败尝试的 check run 结论(取每 commit 最新一次 gate run) + # fail-closed 方向:多计早关优于少计烧额度;非头 commit 上的未完成(null)亦计 + failed_conclusions: [failure, timed_out, startup_failure, cancelled] + # 超限处置标记(打在目标仓 PR 上 / 说明 issue 打在 .github 仓) + exhausted_label: auto-fix-limit-exhausted + issue_label: auto-fix-limit + +cost: + # Actions 分钟月预算(声明值,非账单——全仓公开计费净额 $0,此处防的是失控烧穿速率; + # 当月用量 = /orgs/{org}/settings/billing/usage 按 product=actions & unitType=Minutes 汇总) + actions_minutes: + quota_per_month: 20000 + warn_pct: 80 # ≥80% 开告警 issue(同日去重,不硬停) + hardstop_pct: 100 # ≥100% 置熔断 + 撤全部 open PR auto-merge + P0 issue + # LLM token 月预算(数据源 pending:llm-gateway usage 端点就绪后接入,ADR-0040 决策 6; + # pending 期间仅声明 + 注入通道(LLM_TOKENS_USED)可测,不触发真实告警) + llm_tokens: + quota_per_month: 200000000 + warn_pct: 80 + hardstop_pct: 100 + data_source: pending + +circuit_breaker: + # 熔断标志:org Actions 变量(visibility=all,全仓 workflow/agent 可读) + variable: AUTO_MERGE_DISABLED + # 人工复位:owner PATCH org 变量为 false(或 DELETE)+ 在 P0 issue 留复位评论(留痕); + # cost-check 确认"变量已复位且用量 <100%"后自动关闭 P0 issue + reset_by: randypanding From 65633310400ae1b7cb0292b2b48f26ec96e734a7 Mon Sep 17 00:00:00 2001 From: randypanding <66171646+randypanding@users.noreply.github.com> Date: Thu, 20 Aug 2026 14:00:01 +0800 Subject: [PATCH 2/8] =?UTF-8?q?governance:=20auto-fix=20=E4=B8=8A=E9=99=90?= =?UTF-8?q?=E6=89=A7=E6=B3=95=20+=20=E6=88=90=E6=9C=AC=E7=86=94=E6=96=AD?= =?UTF-8?q?=EF=BC=88P2-8=EF=BC=8CADR-0040=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- governance/auto-fix-limit.sh | 219 +++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 governance/auto-fix-limit.sh diff --git a/governance/auto-fix-limit.sh b/governance/auto-fix-limit.sh new file mode 100644 index 0000000..75517d7 --- /dev/null +++ b/governance/auto-fix-limit.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash +# auto-fix-limit.sh —— agent 修复循环上限执法(ADR-0040,P2-8 .github#93) +# +# 扫描受管仓 open PR:统计每个 PR 各 commit 上 gate check run 的最新失败结论数。 +# 计数真源 = Checks API(commit 元数据)——无内存态/状态文件,runner 崩溃或 workflow +# 重启后计数天然续接而非清零(工作卡 T3 持久性由构造保证)。 +# +# 达上限(policy/automation-limits.yaml auto_fix.max_attempts,默认 3): +# 撤 auto-merge → 关闭 PR → 打 exhausted 标签 → 在 .github 仓开说明 issue(含失败 run 链接)。 +# 带 exhausted 标签被 reopen 的 PR 由下一轮再次关闭(计数是历史事实,不因 reopen 归零)。 +# +# 熔断置位(org 变量 AUTO_MERGE_DISABLED,由 cost-check.sh 负责"置")时: +# 本脚本每轮撤所有 open PR 的 auto-merge——硬停的机器执法,新 enable 旁路窗口 ≤ 扫描周期。 +# +# 用法: GH_TOKEN= bash auto-fix-limit.sh +# 注入(测试/聚焦,T1): AUTOFIX_MAX_ATTEMPTS / AUTOFIX_CHECK_NAME / AUTOFIX_AUTHOR / +# AUTOFIX_REPOS(逗号表)/ AUTOFIX_ALL_PRS=1(全量 open PR)/ AUTOFIX_DRY_RUN=1(只报告不写) +# 退出码: 0=无超限 | 1=本轮关闭了超限 PR(运行变红=可见信号)| 2=基础设施故障(fail-closed) +set -uo pipefail + +ORG="${ORG:-Cloudbird-Software}" +DIR="$(cd "$(dirname "$0")" && pwd)" +GOV_REPO="$ORG/.github" +GH="${GH:-gh}" +DRY_RUN="${AUTOFIX_DRY_RUN:-0}" +EXHAUSTED_CLOSED=0 +INFRA=0 + +ok() { echo "OK $1"; } +act() { echo "ACT $1"; } +infra() { echo "INFRA $1" >&2; INFRA=$((INFRA+1)); } + +[[ -n "${GH_TOKEN:-}" ]] || { echo "FATAL: GH_TOKEN 未设置(需 org admin token)" >&2; exit 2; } + +# ---------- policy 读取(python 解析 YAML;逐行 KEY=value,无 eval;tr 去 CR 兼容 Windows python)---------- +POLICY_ENV=$(python3 - "$DIR/policy/automation-limits.yaml" <<'PYEOF' | tr -d '\r' +import sys, yaml +try: + c = yaml.safe_load(open(sys.argv[1], encoding="utf-8")) + a, k = c["auto_fix"], c["circuit_breaker"] + rows = [("AF_MAX_ATTEMPTS", a["max_attempts"]), ("AF_CHECK_NAME", a["check_name"]), + ("AF_AUTHOR", a["author"]), ("AF_OPT_IN_LABEL", a["opt_in_label"]), + ("AF_EXHAUSTED_LABEL", a["exhausted_label"]), ("AF_ISSUE_LABEL", a["issue_label"]), + ("AF_FAILED_CONCLUSIONS", ",".join(a["failed_conclusions"])), + ("CB_VARIABLE", k["variable"])] + for kk, vv in rows: + vv = str(vv) + assert "=" not in vv and "\n" not in vv, f"policy 值含非法字符: {kk}" + print(f"{kk}={vv}") +except Exception as e: + sys.exit(f"policy 解析失败: {e}") +PYEOF +) || { echo "FATAL: policy/automation-limits.yaml 解析失败" >&2; exit 2; } +while IFS='=' read -r key val; do declare "$key=$val"; done <<< "$POLICY_ENV" +for v in AF_MAX_ATTEMPTS AF_CHECK_NAME AF_AUTHOR AF_OPT_IN_LABEL AF_EXHAUSTED_LABEL AF_ISSUE_LABEL AF_FAILED_CONCLUSIONS CB_VARIABLE; do + [[ -n "${!v:-}" ]] || { echo "FATAL: policy 缺 $v" >&2; exit 2; } +done +# 环境注入优先(T1 负向注入通道) +MAX_ATTEMPTS="${AUTOFIX_MAX_ATTEMPTS:-$AF_MAX_ATTEMPTS}" +CHECK_NAME="${AUTOFIX_CHECK_NAME:-$AF_CHECK_NAME}" +AUTHOR="${AUTOFIX_AUTHOR:-$AF_AUTHOR}" + +# ---------- 受管仓清单:REPOS.yaml 全量 active;AUTOFIX_REPOS 逗号表覆盖 ---------- +if [[ -n "${AUTOFIX_REPOS:-}" ]]; then + REPOS="${AUTOFIX_REPOS//,/ }" +else + REPOS=$(python3 - "$DIR/REPOS.yaml" <<'PYEOF' | tr -d '\r' +import sys, yaml +try: + repos = yaml.safe_load(open(sys.argv[1], encoding="utf-8"))["repos"] + print(" ".join(r["name"] for r in repos if r.get("status") == "active")) +except Exception as e: + sys.exit(f"REPOS.yaml 解析失败: {e}") +PYEOF +) || { echo "FATAL: REPOS.yaml 解析失败" >&2; exit 2; } +fi +[[ -n "$REPOS" ]] || { echo "FATAL: 受管仓清单为空" >&2; exit 2; } + +# ---------- helpers ---------- +mutate() { # DRY_RUN 拦截一切写操作(本地验证不产生副作用) + if [[ "$DRY_RUN" == "1" ]]; then echo "DRY (skip) $*"; else "$@"; fi +} +label_ensure() { #