Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions .github/workflows/governance-drift.yml
Original file line number Diff line number Diff line change
@@ -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
72 changes: 72 additions & 0 deletions governance/apply.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# apply.sh —— 把 governance/ 落盘状态幂等应用到组织(写操作)
#
# 什么时候跑:
# - drift-check 报漂移后,修复用
# - 修改 governance/ 文件后,发布用
# 变更通过 PR 进入 governance/(main-protection 排除了本仓库,可直接合并),
# 合并后本地跑一次本脚本 = "基础设施即代码"的 apply。
#
# 用法: GH_TOKEN=<org admin 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"
121 changes: 121 additions & 0 deletions governance/drift-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
#!/usr/bin/env bash
# drift-check.sh —— 治理飘移检测(只读)
#
# 对比 governance/expected-state.json + rulesets/*.json 与线上实际配置。
# 输出 OK/DRIFT 逐项结果;发现任何漂移 exit 1。
#
# 用法: GH_TOKEN=<org admin 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/ 落盘一致"
40 changes: 40 additions & 0 deletions governance/expected-state.json
Original file line number Diff line number Diff line change
@@ -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"]
}
}
39 changes: 39 additions & 0 deletions governance/language-policy.md
Original file line number Diff line number Diff line change
@@ -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/<name> --template Cloudbird-Software/template-service --public --clone
cd <name> && bash <(curl -sS https://raw.githubusercontent.com/Cloudbird-Software/.github/main/scripts/new-repo-init.sh) <name>
```

然后第一个 PR:按本层表选型填 `.dependency-cruiser.cjs` 的 TODO 规则 + 建模块 AGENTS.md。
语言一旦选定,中途换语言 = 重新立项,不是重构。

## 违规处理

- gate 拦住的(lint/depcruise/类型)→ agent 自行修复,不许绕过
- gate 拦不住的(选型违规、引入 ORM)→ PR 打回,理由引用本文件对应行
22 changes: 22 additions & 0 deletions governance/rulesets/codeql-gate.json
Original file line number Diff line number Diff line change
@@ -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" }
]
}
}
]
}
36 changes: 36 additions & 0 deletions governance/rulesets/main-protection.json
Original file line number Diff line number Diff line change
@@ -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" }]
}
}
]
}
13 changes: 13 additions & 0 deletions governance/rulesets/release-tags.json
Original file line number Diff line number Diff line change
@@ -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" }]
}