按草案逐条落到正本,并把三处已知错误的口径改回: ① 测试语义:删掉"单测跑的是构建产物,不是源码"这条被过度推广的说法。 只有"跑产物"的套件(端到端 / 契约 / 产物自检)才必须排在构建之后; go test 自己编译测试程序、pytest 直读源码、vitest 默认也跑源码。 写错这条的代价是给每个套件加假依赖、拖慢门禁并制造假故障。T8 同步改。 ② CI 与 CD 分离:触发表删掉"推默认分支 → 自动部署测试环境",作业划分表删掉 deploy:test 行, §4.6 标注为"项目自己的 CD、不属于通用 CI",速查表同步。 ③ 门禁入口:统一为 make verify(名字可不同,但 AGENTS/Runbook/CI 必须指同一个), §3.1、§8 清单、§9 速查表一致。 ④ 新增 §4.8「必需检查清单与结果口径」:10 项必需检查的判据表 + 三条结果口径 (失败即停但汇总照做、not_run/skipped 不算过、汇总故障不能报绿), 以及预览/通知作为旁路的规则。 ⑤ 接入文档:修掉"直推也拦得住"这句错口径,并写明接入器不再自动合并、 --merge-pr 会先核对状态;试点记录里那一步标注为"当时的自动合并行为已改"。 ⑥ 03/04/05 里的同源表述一并同步(生成物 jobs 与假成功占位的判据)。 验证:本批 18 处精确替换在脚本里逐处断言"恰好命中 1 次"(DRY-RUN 先过一遍再应用); 替换后全仓 grep 复查,旧口径只在"说明它已被改掉"的上下文里出现。
9.3 KiB
接入既有工程:让现在的工程启用这套规范
一句话:接入 = 前置体检 → 加指针 → 开保护 → 反向自检,一条命令走完。 为什么要有"前置体检"这一关,见 §2 —— 跳过它会把 PR 永久锁死。
⚠️ 工具不替人合并、也不替人审批(2026-10 改):它建完 PR 就把状态如实打给你,然后停下。 批准之后才跑
--merge-pr <n>,而它会先核对检查状态:失败 / 取消 / 从未运行 / 状态拿不到 / 等超时 / 头提交变了 —— 任一情形都拒绝请求合并(--self-test逐条证明)。
1. 一条命令
# ① 只体检 + 打印计划(**不动任何东西**)—— 先看,再决定
python3 ~/eng-standards/scripts/adopt-existing.py <owner/repo>
# ② 计划无误再执行
python3 ~/eng-standards/scripts/adopt-existing.py <owner/repo> --apply
# 需要"谁都不能直推"时才开分支保护 —— **默认不开**(正本 §1.3:原稿口径是直推 main)
python3 ~/eng-standards/scripts/adopt-existing.py <owner/repo> --apply --with-protection
工具只依赖 Python 标准库 + git;凭据运行时从 ~/.git-credentials 读,永不打印。
幂等:重复跑不会重复建 PR、不会改 main。
它做的事:
| 步 | 动作 | 怎么落地 |
|---|---|---|
| 1 | 体检 | 读默认分支、工作流触发方式、runner(仓库级/组织级)、最新提交的 status context、已有保护 |
| 2 | 加指针 | 新起分支 → 写 AGENTS.md → 提交(过本机闸门)→ PR → 有界等 CI → 停下(工具不替你合并) |
| 3 | 开保护(仅 --with-protection 时) |
POST/PATCH /branch_protections:禁直推 + 必须 PR;状态检查按第 1 步判定。默认不做 —— 原稿口径是直推 main(正本 §1.3) |
| 4 | 给自检命令 | 打印"直推必须被拒"的验证命令(这步工具不代做,必须你跑) |
⚠️ main 一旦被保护,所有改动都得走 PR —— 包括工具自己加 AGENTS.md 这一步。
2. 前置体检为什么是必须的(本环境最容易踩的坑)
"必须状态检查通过"开错了,症状是:PR 打开后永远等一个不会出现的检查,永远合不了。 判据只有一条:该仓的工作流,在 PR 的头提交上会不会产生 status?
| 工作流触发 | PR 头提交有没有状态 | 能否开强制状态检查 |
|---|---|---|
on: push: branches: "**"(推任意分支都跑) |
有 —— 分支推送产生 xxx (push) |
✅ 可以 |
on: push: branches: [main],无 pull_request |
没有 —— PR 完全不触发 | ⛔ 开了就锁死 |
on: push: branches: [main] + pull_request |
有,但 context 是 xxx (pull_request) |
⛔ 与 (push) 不匹配;要开得按 (pull_request) 人工确认 |
工具按这张表自动判定,判定不通过就只开"禁直推 + 必须 PR",不碰状态检查。
实测证据(本机 7 个仓):
| 仓 | 工作流触发 | 判定 |
|---|---|---|
Weknora/knowledge-governance |
push: "**" |
✅ 可开 —— context build / build (push) |
Weknora/dobotai-platform |
push: "**" |
✅ 可开 —— 同上 |
aiball/software-pc |
push: [main],无 pull_request |
⛔ 不可开 |
aiball/protocol-docs |
push: [main] + pull_request |
⛔ 上下文不匹配 |
Weknora/knowledge-adapter-service |
默认分支上没有工作流 | ⛔ 无 CI 可强制 |
Weknora/Enterprise-Knowledge-Base |
本仓无工作流(现有 status 来自仓外) | ⛔ 不可开 |
zheromiku/eng-standards-main |
无工作流 | ⛔ 不可开 |
要做的是先补 CI,再开检查 —— 不是先开检查(那会锁死)。所以接入顺序是:
工作流能覆盖任意分支 → 再开强制状态检查。工作流只覆盖 main 的仓(aiball 两个),
第一步是把它改成 on: push: branches: "**" 或补一个 pull_request 触发。
3. 试点记录:Weknora/knowledge-governance(已跑通)
⚠️ 口径已更新:下表里的"分支保护"作法后来按原稿口径关掉了(正本 §1.3:默认不开、直推
main)。 记录保留有两个用处:① 万一将来真要开保护,它证明"PR + 强制状态检查"是能跑通的; ② §2 那条体检判据是真的有用。其余部分(加AGENTS.md指针)不受影响。
执行前该仓状态:无 AGENTS.md、无分支保护;build.yml 是 push: "**";仓库级 runner runner-weknora-kg-01。
| 步 | 实测结果 |
|---|---|
| 分支 + 提交 | docs/agents-pointer,提交通过本机提交闸门(~/.githooks/commit-msg) |
| 推送触发 CI | 头提交 d025fc9adfe2 → context build / build (push) → combined=success |
| PR | PR #1,mergeable=True(关键结论:强制状态检查认 PR —— 因为推分支就产生了 (push) 状态) |
| 合并 | 当时由工具自动合并:HTTP 200(空 body,属正常,不重试)→ 读回 state=closed merged=true merge_commit_sha=32b0ec58a612。该行为已改:现在工具不自动合并(见上) |
| 结果 | AGENTS.md 在 main;分支已自动删除 |
| 反向自检 | 直推 main 被服务端拒绝:Not allowed to push to protected branch main / pre-receive hook declined |
| 幂等 | 再跑一次 --apply:AGENTS.md 跳过、保护值不变、main 仍是 32b0ec58a612 |
4. 不想用工具?手工四步
# ① 体检(判据同 §2)
python3 ~/.hermes/scripts/gitea_api.py GET "/api/v1/repos/<ns>/<repo>/actions/tasks?limit=1"
git clone <repo> /tmp/t && cd /tmp/t
python3 -c "import re;print([l for l in open('.gitea/workflows/build.yml') if re.match(r'^\s*(on|push|pull_request|branches):',l)])"
docker ps --format '{{.Names}}' | grep runner # 本仓有没有 runner
# ② 加指针:走分支 + PR,不要直推 main
git switch -c docs/agents-pointer
# AGENTS.md 内容取自 ~/eng-standards/templates/README.md 里的 ```markdown 骨架块,
# 把 <PROJECT> / <ns>/<repo> 替换掉,并如实写下该仓的 CI / runner / 保护现状
git commit -F - <<'EOF'
docs(agents): 加 AGENTS.md 操作手册与规范指针
指向系统级规范(zheromiku/eng-standards-main)。
EOF
git push -u origin HEAD # 然后网页开 PR → 等 CI → 合并
# ③ 开保护:网页 Settings → Branches;或 API(字段见 swagger CreateBranchProtectionOption)
# enable_push=false、enable_force_push=false、required_approvals=0、
# block_on_outdated_branch=true、dismiss_stale_approvals=true、
# enable_status_check=<按 §2 判定>、status_check_contexts=<匹配的 context>
# ④ 反向自检(必须)
cd /tmp/t && git commit --allow-empty -m 'chore(probe): 不该进 main' && git push origin main
# 判据:Not allowed to push to protected branch
5. 接入后的验收判据(缺一即不算接入)
AGENTS.md(或.hermes.md)在默认分支上,且含指向zheromiku/eng-standards-main的路径。- (只在开了保护时)
GET /repos/{r}/branch_protections非空、enable_push=false,且直推默认分支被服务端拒绝。 ⚠️ 默认口径是直推main,所以正常情况下这里应当是"没有保护" —— 别把"没保护"当成缺陷。 - 推一个故意违规的提交 → 本机 hook 红、CI 的
gate红。 ⚠️ 注意口径:CI 的gate是推完再跑的,它拦不住那次推送本身(正本 §2 / §1.3)。 "直推也拦得住"是错的说法:要拦推送只有两条路 —— 本机 hook,或开分支保护(禁直推)。 gate 的价值是"把违规提交标红并挡住编译/部署",不是"阻止它进 main"。 - (只在开了保护时) PR 能正常合并 —— 证明强制状态检查没有把 PR 锁死。
- 会话里问"这个仓提交信息什么规范",代理能不靠你重复答出来(说明指针真的被注入了)。
6. 会遇到的拒绝与处理
| 现象 | 原因 | 处理 |
|---|---|---|
推 main 被拒 |
已开保护 —— 这是预期行为 | 走分支 + PR |
| PR 一直显示等待状态检查 | 该仓工作流没覆盖 PR(§2 表后两行) | 先改工作流触发,或临时关掉 enable_status_check |
PR 报 mergeable=false 但没有冲突 |
强制状态检查未满足 / 分支过期 | 看 GET /pulls/{n} 的 mergeable 与 head 上的 status |
| 合并接口返回空 body | Gitea 合并成功的正常返回 | 永不重试,改 GET /pulls/{n} 读回 merged=true |
run 一直 queued |
该仓没有归属自己的 runner | bash /srv/aiball/runner/deploy_repo_runner.sh <owner/repo> <name> |
| 工具说"没有凭据" | ~/.git-credentials 里主机是 URL 编码的 |
工具已按解码后比对;手工查时别按字面比 |
7. 扩到其余仓的顺序建议
先补 CI 再开检查,别反过来:
Weknora/knowledge-adapter-service—— 先把build.yml从ci/setup合到main(现在 main 上零校验)aiball/software-pc、aiball/protocol-docs—— 先把工作流触发改成覆盖 PR,再开强制检查Weknora/dobotai-platform—— 工作流已覆盖任意分支,可直接--applyWeknora/Enterprise-Knowledge-Base—— 需先接 CI + runner(分支也待收敛)zheromiku/eng-standards-main—— 等守门脚本做完,再给自己开检查