feat: hard merge gate by default (CI + directory + contract + coordinator + reviews)
- cloud/merge_gate_hard.py (new): pure hard gate; mode resolution is env-only (unset/invalid -> hard, fail-closed); five checks plus branch protection must be protected; unknown/unprotected block even with explicit confirm; no-evidence blocks (fail-closed); directory check reuses #209 check_directory_isolation (v0.3 rule); blocks codes + merge_gate_blocked audit detail are the track-3 pushback interface (no pushback state machine in this track). - cloud/server.py: POST /tasks/{id}/merge uses hard mode by default; record/enforce/off only when explicitly set via env (preserving #209 semantics); body merge_gate_mode is ignored server-side so callers cannot downgrade the gate; every gate block writes a merge_gate_blocked audit before returning 409, status unchanged; hard-mode body type errors return 400. - docs/merge-gate-branch-protection.md (new): protection payload (PR + strict status checks + no force-push + no deletion + enforce_admins), empty required_checks = unconfigured, Pro/public prerequisites, 403 causes; states protection is NOT claimed enabled. - SKILL.md (gate-mechanism wording only): four checks -> five checks; body + coordinator + client appendix gate paragraphs updated to default hard, merge_gate_blocked audit, payload doc reference. - tests: 31 new hard-gate tests (pure + endpoint + skill text); #209 endpoint tests now set record env explicitly (their intent); test_server_merge 200-path calls now carry full hard evidence.
This commit is contained in:
1 parent
d553f9549c
commit
61e08a1a8c
7 files changed
+1048
-39
No files matched your search
@@ -0,0 +1,129 @@
|
||||
# 分支保护与合并硬门禁:GitHub 侧操作说明
|
||||
|
||||
状态声明(先读):本仓库当前是否已开启主分支保护,**本文件不声称**。
|
||||
保护只能由仓库管理员在 GitHub 侧开通,本平台代码(cloud/merge_gate.py、
|
||||
cloud/merge_gate_hard.py)只能在调用方明确声明保护状态时做门禁,
|
||||
绝不代替 GitHub 开通、绝不声称“已保护”。服务端硬门禁(默认 hard)
|
||||
要求 `branch_protection == "protected"` 才会放行;`unknown` 与
|
||||
`unprotected` 一律 409。
|
||||
|
||||
## 1. 分支保护 payload(对默认分支,如 main)
|
||||
|
||||
GitHub REST:`PUT /repos/{owner}/{repo}/branches/{branch}/protection`
|
||||
|
||||
```json
|
||||
{
|
||||
"required_status_checks": {
|
||||
"strict": true,
|
||||
"checks": [
|
||||
{"context": "ci"}
|
||||
]
|
||||
},
|
||||
"enforce_admins": true,
|
||||
"required_pull_request_reviews": {
|
||||
"required_approving_review_count": 1,
|
||||
"dismiss_stale_reviews": true,
|
||||
"require_code_owner_reviews": false
|
||||
},
|
||||
"restrictions": null,
|
||||
"allow_force_pushes": false,
|
||||
"allow_deletions": false,
|
||||
"required_linear_history": false,
|
||||
"allow_fork_syncing": false,
|
||||
"block_creations": false
|
||||
}
|
||||
```
|
||||
|
||||
逐项对应任务要求:
|
||||
|
||||
- PR:`required_pull_request_reviews`——必须经 PR 评审才能合并,
|
||||
本平台技能要求“没有 PR 号不算完成”,与此一致。
|
||||
- strict status checks:`required_status_checks.strict = true`——
|
||||
PR 分支必须与目标分支最新状态同步后检查才算通过,防“旧绿灯合新代码”。
|
||||
- no force-push:`allow_force_pushes = false`(默认分支绝不 force-push,
|
||||
与技能“绝不对默认分支 force-push”一致)。
|
||||
- no deletion:`allow_deletions = false`。
|
||||
- enforce_admins:`enforce_admins = true`——管理员同样受保护规则约束,
|
||||
不能绕过(注意:仓库所有者/组织所有者在部分场景下仍可通过
|
||||
设置页调整规则本身,enforce_admins 约束的是“绕过规则合并”,
|
||||
不是“不能改规则”)。
|
||||
|
||||
API 命名注意(新旧两套,别抄错):
|
||||
|
||||
- 较新的 API 用 `required_status_checks.checks[]`,每项含 `context`
|
||||
与可选 `app_id`;较旧文档/客户端用 `contexts[]`(字符串数组)。
|
||||
两者语义相同:列出必须通过的检查名。调用前以实际 API 返回为准,
|
||||
不要凭记忆混用。
|
||||
- `allow_force_pushes` / `allow_deletions` 在新 API 里是顶层布尔值;
|
||||
旧版曾用 `allow_force_pushes: {enabled: ...}` 对象形,同样以实际
|
||||
API 为准。
|
||||
|
||||
## 2. 空 required_checks = 未配置(硬门禁的同一口径)
|
||||
|
||||
- GitHub 侧:`required_status_checks.checks`(或 `contexts`)为空数组,
|
||||
即使 `strict: true`,也等于“没有必须通过的检查”——任何 CI 状态
|
||||
都不会被保护规则拦住。不要把“开了保护”理解成“CI 已被强制”。
|
||||
- 服务端硬门禁侧:`required_checks` 为 `[]` 时判
|
||||
`ci_required_checks_unconfigured`,409;`required_checks` 缺失时判
|
||||
`ci_evidence_missing`,同样 409。两者都是 fail-closed,只是
|
||||
“显式空配置”与“没给证据”的区分,便于第 3 轨推回包定位问题。
|
||||
- 开通保护后必须做的核对:保护设置页(或 GET 同一 protection 端点)
|
||||
返回的 checks 列表非空,且与仓库真实 CI 检查名逐一对应;
|
||||
改名/新增 CI 检查后,保护里的检查名必须同步更新,否则
|
||||
旧检查名永远等不到、PR 永远合并不了,或新检查根本不在强制列表里。
|
||||
|
||||
## 3. Pro / 公开的前提(三选一,用户侧决定,代码不替选)
|
||||
|
||||
GitHub 对私有仓库的分支保护规则有版本限制(以 GitHub 当前定价页为准,
|
||||
本文件不复述具体价格):
|
||||
|
||||
1. **升级到 Pro / Team / Enterprise**:私有仓库可用完整的
|
||||
required_pull_request_reviews 与 required_status_checks。
|
||||
2. **公开仓库**:公开仓库免费可用分支保护。但公开是不可逆影响
|
||||
可见性的决定:公开前必须确认仓库里没有密钥、内部设计、私有
|
||||
数据与历史提交里的敏感内容;**本轨绝不擅自公开仓库**。
|
||||
3. **保持现状 + 纪律 + 记风险**:不开保护,靠硬门禁的
|
||||
`branch_protection_unprotected` 阻断 + 审计记录 + 人工流程约束。
|
||||
这不是“等同于保护”,只是三者中唯一不花钱、不改可见性的选项;
|
||||
选择它意味着接受“保护规则本身不存在,门禁只拦本平台的合并端点,
|
||||
直接推默认分支仍可由有写权限者做到”这一残留风险。
|
||||
|
||||
在用户做出三选一决定之前,硬门禁默认 hard 会把未声明/未保护的
|
||||
合并全部拦下(409),这是符合 fail-closed 的预期行为,不是故障。
|
||||
|
||||
## 4. 常见 403 原因(按排查顺序)
|
||||
|
||||
保护端点返回 403 时,先区分是“版本限制”还是“权限不足”:
|
||||
|
||||
- **版本限制**:私有仓库免费版调用含 required_pull_request_reviews /
|
||||
required_status_checks 的保护设置,GitHub 以 403 拒绝(文案大意为
|
||||
需要升级)。这是第 3 节三选一要解决的前提,不是 token 写错。
|
||||
- **调用方不是管理员**:只有仓库管理员(Admin 权限)能改分支保护;
|
||||
Maintainer/Write 权限调该端点会 403。检查调用身份的仓库角色。
|
||||
- **GitHub App 权限不足或未安装**:App 需要 Repository administration
|
||||
(写分支保护)权限,且必须安装在目标仓库上;权限是安装时授予的,
|
||||
之后在 App 设置里加权限不会自动生效,必须由安装方重新接受新权限。
|
||||
App token 的权限缩小、过期或被吊销同样表现为 403/401。
|
||||
- **OAuth token 权限不足**:个人 token 需含 `repo` 作用域(私有仓)
|
||||
才能写保护;细粒度 token 需含对应的仓库管理权限。
|
||||
- **组织/企业策略**:组织规则集(rulesets)或企业策略可能禁止
|
||||
修改、或强制覆盖仓库级保护,导致写入被拒或写入后不生效;
|
||||
此时以组织规则集的实际生效状态为准,不要只看仓库设置页。
|
||||
|
||||
排查时只读核对:`GET /repos/{owner}/{repo}/branches/{branch}/protection`
|
||||
返回 404 表示该分支未设保护(不是“请求错了”),200 则返回当前规则;
|
||||
不要用“试一次合并看拦不拦”来反推保护状态。
|
||||
|
||||
## 5. 与服务端硬门禁的对应关系
|
||||
|
||||
| GitHub 侧状态 | 服务端硬门禁(默认 hard) |
|
||||
| --- | --- |
|
||||
| 保护已开,checks 非空且全绿 | 调用方声明 `protected` + CI 证据全绿时放行(其余四项也须过) |
|
||||
| 保护已开,但 checks 为空 | 调用方 `required_checks: []` → `ci_required_checks_unconfigured`,409 |
|
||||
| 保护未开 | 调用方声明 `unprotected` → `branch_protection_unprotected`,409(确认无效) |
|
||||
| 保护状态未声明 | `branch_protection_unknown`,409(unknown 不等于 protected) |
|
||||
|
||||
服务端门禁不向 GitHub 求证:调用方声明 `protected` 但 GitHub 实际
|
||||
未开时,服务端无法从声明本身发现谎报——这正是“保护必须由管理员
|
||||
在 GitHub 侧开通并用 GET 核对”的原因,也是第 3 轨审计要覆盖的
|
||||
核对项(审计结论与 GitHub 实际状态不一致即为偏差)。
|
||||
Reference in new issue
Block a user