diff --git a/.github/workflows/governance-drift.yml b/.github/workflows/governance-drift.yml new file mode 100644 index 0000000..42ec019 --- /dev/null +++ b/.github/workflows/governance-drift.yml @@ -0,0 +1,48 @@ +name: governance-drift +on: + schedule: + - cron: "0 3 * * 1" # 每周一 03:00 UTC + workflow_dispatch: + +permissions: {} + +jobs: + drift-check: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - name: 检测治理漂移 + env: + GH_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} + run: | + if [[ -z "$GH_TOKEN" ]]; then + echo "::error::缺 org secret GOVERNANCE_TOKEN(需 org admin token)。设置: 组织 Settings → Secrets and variables → Actions → New organization secret" >&2 + exit 2 + fi + bash governance/drift-check.sh | tee drift-report.txt + - name: 发现漂移则开 issue(幂等) + if: failure() + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + TITLE="治理漂移检测:组织配置与 governance/ 落盘不一致" + EXISTING=$(gh issue list --repo "$REPO" --state open --search "in:title 治理漂移" --json number --jq 'length') + BODY="自动化检测发现以下漂移(运行 #${{ github.run_id }}): + + $(cat drift-report.txt) + + 修复方式:本地跑 \`bash governance/apply.sh\`,或手动改回后跑 \`bash governance/drift-check.sh\` 验证。 + + @randypanding" + if [[ "$EXISTING" != "0" ]]; then + NUM=$(gh issue list --repo "$REPO" --state open --search "in:title 治理漂移" --json number --jq '.[0].number') + gh issue comment "$NUM" --repo "$REPO" --body "$BODY" + else + gh issue create --repo "$REPO" --title "$TITLE" --body "$BODY" + fi diff --git a/governance/apply.sh b/governance/apply.sh new file mode 100755 index 0000000..50f4348 --- /dev/null +++ b/governance/apply.sh @@ -0,0 +1,72 @@ +#!/usr/bin/env bash +# apply.sh —— 把 governance/ 落盘状态幂等应用到组织(写操作) +# +# 什么时候跑: +# - drift-check 报漂移后,修复用 +# - 修改 governance/ 文件后,发布用 +# 变更通过 PR 进入 governance/(main-protection 排除了本仓库,可直接合并), +# 合并后本地跑一次本脚本 = "基础设施即代码"的 apply。 +# +# 用法: GH_TOKEN= bash apply.sh +set -euo pipefail + +ORG="${ORG:-Cloudbird-Software}" +DIR="$(cd "$(dirname "$0")" && pwd)" +EXPECTED="$DIR/expected-state.json" + +api() { curl -sS -H "Authorization: Bearer ${GH_TOKEN:?需要 org admin GH_TOKEN}" \ + -H "Accept: application/vnd.github+json" "$@"; } + +echo "==> 1/5 Rulesets(存在则更新,不存在则创建)" +EXISTING=$(api "https://api.github.com/orgs/$ORG/rulesets?per_page=100") +for f in "$DIR"/rulesets/*.json; do + name=$(jq -r .name "$f") + rid=$(jq -r --arg n "$name" '.[] | select(.name == $n) | .id' <<<"$EXISTING" | head -1) + if [[ -n "$rid" && "$rid" != "null" ]]; then + code=$(api -o /dev/null -w '%{http_code}' -X PUT \ + "https://api.github.com/orgs/$ORG/rulesets/$rid" -d @"$f") + echo " $name: 更新 (HTTP $code)" + else + resp=$(api -X POST "https://api.github.com/orgs/$ORG/rulesets" -d @"$f") + code_ok=$(jq -r 'if .id then "created id=" + (.id|tostring) else .message end' <<<"$resp") + echo " $name: $code_ok" + fi +done + +echo "==> 2/5 Actions 允许策略 + 白名单" +api -o /dev/null -X PUT "https://api.github.com/orgs/$ORG/actions/permissions" \ + -d '{"enabled": true, "allowed_actions": "selected"}' +api -o /dev/null -X PUT "https://api.github.com/orgs/$ORG/actions/permissions/selected-actions" \ + -d "$(jq -c '.actions_policy | {github_owned_allowed, verified_allowed, patterns_allowed}' "$EXPECTED")" +echo " done" + +echo "==> 3/5 默认 workflow 权限 = 只读" +api -o /dev/null -X PUT "https://api.github.com/orgs/$ORG/actions/permissions/workflow" \ + -d "$(jq -c '{default_workflow_permissions: .actions_policy.default_workflow_permissions, can_approve_pull_request_reviews: .actions_policy.default_workflow_permissions_can_approve}' "$EXPECTED")" +echo " done" + +echo "==> 4/5 Code Security 默认应用到新仓库" +CS=$(api "https://api.github.com/orgs/$ORG/code-security/configurations") +CSID=$(jq -r --arg n "$(jq -r .code_security.configuration_name "$EXPECTED")" \ + '.[] | select(.name == $n) | .id' <<<"$CS" | head -1) +if [[ -n "$CSID" && "$CSID" != "null" ]]; then + code=$(api -o /dev/null -w '%{http_code}' -X PUT \ + "https://api.github.com/orgs/$ORG/code-security/configurations/$CSID/defaults" \ + -d "$(jq -c '{default_for_new_repos: .code_security.default_for_new_repos}' "$EXPECTED")") + echo " config#$CSID 设为新仓默认 (HTTP $code)" +else + echo " 跳过:配置不存在,请先在网页创建 'GitHub recommended'" +fi + +echo "==> 5/5 仓库基线(squash-only / 删分支)" +EXCLUDES=$(jq -c '.repo_baseline.exclude_repos // []' "$EXPECTED") +REPOS=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100" | jq -r '.[].name') +for r in $REPOS; do + jq -e --arg r "$r" 'index($r) != null' <<<"$EXCLUDES" >/dev/null && { echo " $r: 跳过(exclude)"; continue; } + api -o /dev/null -X PATCH "https://api.github.com/repos/$ORG/$r" \ + -d '{"allow_squash_merge": true, "allow_merge_commit": false, "allow_rebase_merge": false, "delete_branch_on_merge": true}' + echo " $r: 基线已应用" +done + +echo +echo "完成。验证: bash $DIR/drift-check.sh" diff --git a/governance/drift-check.sh b/governance/drift-check.sh new file mode 100755 index 0000000..ab22816 --- /dev/null +++ b/governance/drift-check.sh @@ -0,0 +1,121 @@ +#!/usr/bin/env bash +# drift-check.sh —— 治理飘移检测(只读) +# +# 对比 governance/expected-state.json + rulesets/*.json 与线上实际配置。 +# 输出 OK/DRIFT 逐项结果;发现任何漂移 exit 1。 +# +# 用法: GH_TOKEN= bash drift-check.sh +# CI 中由 .github/workflows/governance-drift.yml 调用,凭据来自 secret GOVERNANCE_TOKEN。 +set -uo pipefail + +ORG="${ORG:-Cloudbird-Software}" +DIR="$(cd "$(dirname "$0")" && pwd)" +EXPECTED="$DIR/expected-state.json" +DRIFTS=0 + +ok() { echo "OK $1"; } +drift(){ echo "DRIFT $1"; DRIFTS=$((DRIFTS+1)); } + +api() { curl -sS -H "Authorization: Bearer ${GH_TOKEN:?需要 org admin GH_TOKEN}" \ + -H "Accept: application/vnd.github+json" "$@"; } + +[[ -n "${GH_TOKEN:-}" ]] || { echo "FATAL: GH_TOKEN 未设置"; exit 2; } + +# ---------- 1. Rulesets:存在性 / enforcement / 核心规则 ---------- +ACTUAL_RULESETS=$(api "https://api.github.com/orgs/$ORG/rulesets?per_page=100") +for f in "$DIR"/rulesets/*.json; do + name=$(jq -r .name "$f") + want_enf=$(jq -r .enforcement "$f") + want_rules=$(jq -c '[.rules[].type] | sort' "$f") + row=$(jq -c --arg n "$name" '.[] | select(.name == $n)' <<<"$ACTUAL_RULESETS") + if [[ -z "$row" || "$row" == "null" ]]; then + drift "ruleset '$name' 不存在(定义文件 $f)"; continue + fi + got_enf=$(jq -r .enforcement <<<"$row") + [[ "$got_enf" == "$want_enf" ]] || drift "ruleset '$name' enforcement=$got_enf 期望=$want_enf" + # 线上完整定义(含 id)对比规则集 + rid=$(jq -r .id <<<"$row") + detail=$(api "https://api.github.com/orgs/$ORG/rulesets/$rid") + got_rules=$(jq -c '[.rules[].type] | sort' <<<"$detail") + [[ "$got_rules" == "$want_rules" ]] || drift "ruleset '$name' 规则集变动: $got_rules 期望=$want_rules" + # 精确 diff:把线上定义裁剪成与落盘文件同构后比较(剔除 API 自动填充的空字段) + got_norm=$(jq -S 'del(.rules[].parameters.required_reviewers? | select(. == [])) | {name,target,enforcement,conditions,bypass_actors,rules}' <<<"$detail") + want_norm=$(jq -S '{name,target,enforcement,conditions,bypass_actors,rules}' "$f") + [[ "$got_norm" == "$want_norm" ]] \ + || drift "ruleset '$name' 定义与落盘不一致: $(diff <(echo "$want_norm") <(echo "$got_norm") | head -10)" + ok "ruleset '$name'" +done +EXTRA=$(jq -c --argjson known "$(jq -s '[.[].name]' "$DIR"/rulesets/*.json)" \ + '[.[].name] - $known' <<<"$ACTUAL_RULESETS") +[[ "$EXTRA" == "[]" ]] || drift "线上存在未落盘的 ruleset: $EXTRA(落盘或删除)" + +# ---------- 2. Actions 策略 ---------- +AP=$(api "https://api.github.com/orgs/$ORG/actions/permissions") +[[ "$(jq -r .allowed_actions <<<"$AP")" == "$(jq -r .actions_policy.allowed_actions "$EXPECTED")" ]] \ + || drift "allowed_actions=$(jq -r .allowed_actions <<<"$AP")" + +SA=$(api "https://api.github.com/orgs/$ORG/actions/permissions/selected-actions") +[[ "$(jq -c '{github_owned_allowed, verified_allowed, patterns_allowed}' <<<"$SA")" == \ + "$(jq -c '.actions_policy | {github_owned_allowed, verified_allowed, patterns_allowed}' "$EXPECTED")" ]] \ + || drift "allowed actions 白名单不一致: $(jq -c .patterns_allowed <<<"$SA")" + +WF=$(api "https://api.github.com/orgs/$ORG/actions/permissions/workflow") +[[ "$(jq -r .default_workflow_permissions <<<"$WF")" == "$(jq -r .actions_policy.default_workflow_permissions "$EXPECTED")" ]] \ + || drift "default_workflow_permissions=$(jq -r .default_workflow_permissions <<<"$WF")" +ok "actions 策略(含白名单)" + +# ---------- 3. Code Security 默认配置 ---------- +CS=$(api "https://api.github.com/orgs/$ORG/code-security/configurations") +want_cs=$(jq -r .code_security.configuration_name "$EXPECTED") +csrow=$(jq -c --arg n "$want_cs" '.[] | select(.name == $n)' <<<"$CS") +if [[ -z "$csrow" || "$csrow" == "null" ]]; then + drift "code security 配置 '$want_cs' 不存在" +else + # 注意:default_for_new_repos 是只写 API(PUT .../defaults),GitHub 不提供读取端点, + # 无法只读验证。由 apply.sh 的幂等 PUT 保证,drift-check 只验证配置本体存在。 + ok "code security '$want_cs'(default_for_new_repos 由 apply 保证,API 无读取端点)" +fi + +# ---------- 4. 仓库基线(squash-only / 删分支)---------- +EXCLUDES=$(jq -c '.repo_baseline.exclude_repos // []' "$EXPECTED") +REPOS=$(api "https://api.github.com/orgs/$ORG/repos?per_page=100" | jq -r '.[].name') +for r in $REPOS; do + jq -e --arg r "$r" '($r as $x | . | index($x)) != null' <<<"$EXCLUDES" >/dev/null && continue + RR=$(api "https://api.github.com/repos/$ORG/$r") + bad="" + [[ "$(jq -r .allow_squash_merge <<<"$RR")" == "true" ]] || bad="$bad squash-off" + [[ "$(jq -r .allow_merge_commit <<<"$RR")" == "false" ]] || bad="$bad merge-commit-on" + [[ "$(jq -r .allow_rebase_merge <<<"$RR")" == "false" ]] || bad="$bad rebase-on" + [[ "$(jq -r .delete_branch_on_merge <<<"$RR")" == "true" ]] || bad="$bad keep-branch" + [[ -z "$bad" ]] && ok "repo baseline '$r'" || drift "repo '$r' 基线异常:$bad" +done + +# ---------- 5. 必需 org secrets(只查存在性,值不可读)---------- +SECRETS=$(api "https://api.github.com/orgs/$ORG/actions/secrets") +for s in $(jq -r '.org_secrets_required[]' "$EXPECTED"); do + jq -e --arg s "$s" '.secrets[].name == $s' <<<"$SECRETS" >/dev/null \ + && ok "org secret '$s'" || drift "org secret '$s' 缺失" +done + +# ---------- 6. GitHub App 权限形状 ---------- +APP=$(api "https://api.github.com/apps/$(jq -r .github_app.name "$EXPECTED")") +if [[ "$(jq -r .id <<<"$APP" 2>/dev/null)" == "$(jq -r .github_app.id "$EXPECTED")" ]]; then + for p in $(jq -r '.github_app.permissions | to_entries[] | "\(.key)=\(.value)"' "$EXPECTED"); do + k=${p%%=*}; v=${p##*=} + [[ "$(jq -r .permissions.$k <<<"$APP")" == "$v" ]] || drift "App 权限 $k 变动" + done + for p in $(jq -r '.github_app.must_not_have[]' "$EXPECTED"); do + jq -e --arg p "$p" '.permissions | has($p)' <<<"$APP" >/dev/null \ + && drift "App 出现禁用权限 '$p'(应立即在 App 设置页移除)" + done + ok "github app '$(jq -r .github_app.name "$EXPECTED")'" +else + drift "github app '$(jq -r .github_app.name "$EXPECTED")' 不存在或 id 不符" +fi + +echo "----------------------------------------" +if [[ $DRIFTS -gt 0 ]]; then + echo "结果: $DRIFTS 项漂移。修复: bash governance/apply.sh 或手动改回" + exit 1 +fi +echo "结果: 无漂移,组织配置与 governance/ 落盘一致" diff --git a/governance/expected-state.json b/governance/expected-state.json new file mode 100644 index 0000000..7451d0f --- /dev/null +++ b/governance/expected-state.json @@ -0,0 +1,40 @@ +{ + "org": "Cloudbird-Software", + "comment": "组织治理期望状态。drift-check.sh 据此检测漂移;apply.sh 据此幂等修复。ruleset 完整定义在 rulesets/ 目录。", + "actions_policy": { + "allowed_actions": "selected", + "github_owned_allowed": true, + "verified_allowed": true, + "patterns_allowed": [ + "zizmorcore/*", + "astral-sh/*", + "dependabot/fetch-metadata", + "softprops/action-gh-release", + "docker/*", + "Cloudbird-Software/*" + ], + "default_workflow_permissions": "read", + "default_workflow_permissions_can_approve": false + }, + "code_security": { + "configuration_name": "GitHub recommended", + "default_for_new_repos": "all" + }, + "repo_baseline": { + "squash_only": true, + "delete_branch_on_merge": true, + "exclude_repos": ["AI_Web_School"] + }, + "org_secrets_required": ["AGENT_APP_SECRET"], + "github_app": { + "name": "cloudbrid-agent", + "id": 4632704, + "permissions": { + "contents": "write", + "issues": "write", + "metadata": "read", + "pull_requests": "write" + }, + "must_not_have": ["workflows", "administration"] + } +} diff --git a/governance/language-policy.md b/governance/language-policy.md new file mode 100644 index 0000000..7e94adf --- /dev/null +++ b/governance/language-policy.md @@ -0,0 +1,39 @@ +# 组织语言生态政策(agent 按需读取) + +> 本文件是组织级政策,不放在任何仓库的 AGENTS.md 里(惜上下文)。 +> agent 在以下场景必须读它:新建项目、新增模块选型、引入新依赖、写架构文档。 +> 违规 = gate 拦截或 PR 打回。最后更新: 2026-08。 + +## 分层选型 + +| 层 | 允许 | 禁止 | 为什么(选型理由) | +|---|---|---|---| +| 主力应用/服务 | Go(服务/CLI 优先);受约束的 TypeScript(需要前端同构时) | Rust/Java/C++/新语言 | 训练数据海量、写法单一、编译反馈快——LLM 产出可靠 | +| LLM/Prompt 层 | BAML + Python | 裸 prompt 字符串拼接 | BAML 是"深接口"的教科书实现:prompt 即类型化契约 | +| 数据层 | SQL + 类型生成(如 sqlc / kysely) | 重 ORM(Prisma/Hibernate 类) | SQL 是史上最成功的深接口 DSL;重 ORM 挡住 agent 对生成的控制 | +| 配置/基建 | 声明式(Terraform / Compose) | 脚本化基建(bash 造 infra) | 无逻辑 = AI 不会写错 | +| 验证层 | property-based test(fast-check/proptest)+ schema 契约 + dependency-cruiser 依赖规则 | 只写 happy-path 单测 | 这是组织的真正护城河 | + +## 每层的硬性要求 + +1. **Go**:`gofmt` 零 diff;错误必须显式处理(`errcheck` 进 gate);模块入口 `cmd/`,包间禁止循环依赖。 +2. **TypeScript**:`strict: true`;禁止 `any`(eslint 进 gate);跨模块只 import 入口 `index.ts`(depcruise 检查)。 +3. **BAML**:prompt 改动必须跑 golden test(输入→输出快照);禁止在 TS/Go 里内嵌 prompt 字符串。 +4. **SQL**:迁移文件只增不改(up + down);查询经类型生成器,禁止手写拼接。 +5. **Terraform/Compose**:`plan`/`config -q` 进 CI;禁止 `local` 值参与资源命名。 +6. **验证层**:对外接口必须有 property-based test;`.dependency-cruiser.cjs` 的 TODO 边界规则在模块落地当周补全。 + +## 新仓库初始化(agent 必须遵循) + +``` +gh repo create Cloudbird-Software/ --template Cloudbird-Software/template-service --public --clone +cd && bash <(curl -sS https://raw.githubusercontent.com/Cloudbird-Software/.github/main/scripts/new-repo-init.sh) +``` + +然后第一个 PR:按本层表选型填 `.dependency-cruiser.cjs` 的 TODO 规则 + 建模块 AGENTS.md。 +语言一旦选定,中途换语言 = 重新立项,不是重构。 + +## 违规处理 + +- gate 拦住的(lint/depcruise/类型)→ agent 自行修复,不许绕过 +- gate 拦不住的(选型违规、引入 ORM)→ PR 打回,理由引用本文件对应行 diff --git a/governance/rulesets/codeql-gate.json b/governance/rulesets/codeql-gate.json new file mode 100644 index 0000000..a512095 --- /dev/null +++ b/governance/rulesets/codeql-gate.json @@ -0,0 +1,22 @@ +{ + "name": "codeql-gate", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] }, + "repository_name": { "include": ["~ALL"], "exclude": ["AI_Web_School", ".github"] } + }, + "bypass_actors": [ + { "actor_id": null, "actor_type": "OrganizationAdmin", "bypass_mode": "always" } + ], + "rules": [ + { + "type": "code_scanning", + "parameters": { + "code_scanning_tools": [ + { "tool": "CodeQL", "security_alerts_threshold": "medium_or_higher", "alerts_threshold": "errors" } + ] + } + } + ] +} diff --git a/governance/rulesets/main-protection.json b/governance/rulesets/main-protection.json new file mode 100644 index 0000000..319a692 --- /dev/null +++ b/governance/rulesets/main-protection.json @@ -0,0 +1,36 @@ +{ + "name": "main-protection", + "target": "branch", + "enforcement": "active", + "conditions": { + "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] }, + "repository_name": { "include": ["~ALL"], "exclude": ["AI_Web_School", ".github"] } + }, + "bypass_actors": [ + { "actor_id": null, "actor_type": "OrganizationAdmin", "bypass_mode": "always" } + ], + "rules": [ + { "type": "deletion" }, + { "type": "non_fast_forward" }, + { "type": "required_linear_history" }, + { + "type": "pull_request", + "parameters": { + "required_approving_review_count": 0, + "dismiss_stale_reviews_on_push": true, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_review_thread_resolution": true, + "allowed_merge_methods": ["squash"] + } + }, + { + "type": "required_status_checks", + "parameters": { + "strict_required_status_checks_policy": false, + "do_not_enforce_on_create": true, + "required_status_checks": [{ "context": "gate" }] + } + } + ] +} diff --git a/governance/rulesets/release-tags.json b/governance/rulesets/release-tags.json new file mode 100644 index 0000000..d53bc8e --- /dev/null +++ b/governance/rulesets/release-tags.json @@ -0,0 +1,13 @@ +{ + "name": "release-tags", + "target": "tag", + "enforcement": "active", + "conditions": { + "ref_name": { "include": ["refs/tags/v*"], "exclude": [] }, + "repository_name": { "include": ["~ALL"], "exclude": ["AI_Web_School"] } + }, + "bypass_actors": [ + { "actor_id": null, "actor_type": "OrganizationAdmin", "bypass_mode": "always" } + ], + "rules": [{ "type": "deletion" }, { "type": "non_fast_forward" }, { "type": "update" }] +}