10 KiB
接入既有工程:让现在的工程启用这套规范
一句话:接入 = 前置体检 → 加指针 → 开保护 → 反向自检,一条命令走完。 为什么要有"前置体检"这一关,见 §2 —— 跳过它会把 PR 永久锁死。
⚠️ 工具不替人合并、也不替人审批(2026-10 改):它建完 PR 就把状态如实打给你,然后停下。 批准之后才跑
--merge-pr <n> --approve-sha <完整40位SHA>,而它会先核对: ① 获批的就是头提交那个完整 SHA(短 sha 不算批准依据);② 明确策略要求的每一个 status context 都是success(失败 / 取消 / 从未运行 / 状态拿不到 / 等超时 / 头提交变了 —— 任一情形都拒绝); ③ PR 的目标分支就是默认分支、状态是open、head 就是本仓(跨仓/合错分支都不做)。--self-test(11 分支)+--self-test-chain(假 Gitea 跑真实merge_pr(),13 分支)逐条证明。"要求哪些检查"只能来自明确策略:
--require-context "build / build (push)", 或仓库里的.gitea/required-contexts.txt(一行一个)。两个都没有 / 文件是空的 ⇒ 拒绝合并 —— 不接受"体检时观察到哪些状态"来代替:combined state只报"报过状态的都成功", 一个无关检查成功就能骗过它,于是没跑过必需检查的 PR 会被合进去。
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—— 等守门脚本做完,再给自己开检查