🐛 fix(commit-message): enforce one rule owner across skill and CI

CI ran .gitea/ci/commit_message_lint.py, an independent reimplementation
that parsed the type/emoji mapping out of docs/common/commit_message.md.
Its scope pattern accepted fix(-a), fix(a-), fix(a--b) and fix(_), and it
had no subject length check, so the gate that actually blocks merges
enforced weaker rules than the Validation this skill reports.

The CI entry is now a wrapper that locates the skill validator and
delegates to it with no arguments; commit_policy.json becomes the only
rule source and explanatory docs stop being a machine policy input.

The validator also learns the workflow_run event, whose payload carries
neither a PR title nor a before/after range. An upstream pull_request now
validates <integration-branch>..head_sha instead of HEAD alone, taking
the branch name from COMMIT_LINT_MAIN_BRANCH, and degrades to the
upstream head commit with a WARN rather than guessing a base.

Alongside: --help now documents the no-argument CI mode it had always
supported silently, CI wiring detail moves to references/ci-wiring.md,
and the description gains negative boundaries.

test/test_commit_message_policy.py asserts policy/spec-table equality and
uses ast to assert the CI entry imports no regex and reads no file, so a
second implementation cannot reappear unnoticed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
csh
2026-08-20 15:21:37 +08:00
co-authored by Claude Fable 5
parent 651c1f68d2
commit 7408c532f0
6 changed files with 476 additions and 208 deletions
@@ -0,0 +1,73 @@
# CI 接线(CI Wiring
配置或排查 CI 上的提交信息校验时读本文件。交互式起草提交信息不需要它。
## 唯一实现
`scripts/validate_commit_message.py` 是提交信息规则的唯一实现,策略来自
`references/commit_policy.json`。CI 入口只负责定位并调用它:
```bash
python <skill-root>/scripts/validate_commit_message.py
```
无参数即进入 CI 输入校验模式。退出码原样透传:`0` 全部通过、`1` 存在不合规主题、
`2` 策略/参数/输入错误。
CI 入口不得自行实现校验,不得从 `docs/common/commit_message.md` 之类的说明性文档
解析 type/emoji 映射。说明性文档是给人读的;把它当机器策略来源会让 CI 与
`commit_policy.json` 各自演进,而 validator 报告的 `Validation` 就不再代表实际
拦合并的那道门。本仓库的 `.gitea/ci/commit_message_lint.py` 是薄 wrapper 范例,
其纪律由 `test/test_commit_message_policy.py` 机器保证。
## 事件覆盖范围
`validate_commit_message.py --help` 的当前输出为权威;下表是形状说明。
| 事件 | 取回的主题 |
| --------------- | ----------------------------------------------------------------- |
| `pull_request*` | `pull_request.title``base.sha..head.sha` 的完整提交范围 |
| `push` | `before..after` 完整范围;`before` 为零对象时按分支图计算新增提交 |
| `workflow_run` | 见下节 |
| 其它 / 本地 | 回退 `HEAD` 一条 |
已识别事件缺失或损坏 payload 时失败;shallow repository 一律失败。不能用可能截断的
payload `commits` 数组或计数字段证明范围完整。
## workflow_run 编排
`workflow_run` payload 不含 PR 标题,也不含 `before`/`after``workflow_run.head_sha`
是唯一锚点。因此:
- **上游为 `pull_request*`**:取 `<集成分支>..head_sha` 的完整范围。集成分支名读
`$COMMIT_LINT_MAIN_BRANCH`(默认 `main`),依次尝试
`refs/remotes/origin/<名字>``refs/heads/<名字>`
- **其它上游事件**:只校验 `head_sha` 一条。
- 集成分支无法解析、上游 `head_branch` 就是集成分支、或计算出的范围为空时,退化为
只校验 `head_sha` 并打印 `WARN`,**不猜测 base**。默认分支的 push 因此只校验被推
上去的那一条,这是可接受的:进入默认分支的路径由 PR 那道门守。
该架构下 PR 标题不参与校验,这是 payload 决定的,不是配置项。完整提交范围校验取代
它,且强度更高——标题一直只是历史的代理,范围校验管的是真正留在历史里的东西。若确实
需要校验 PR 标题,只能在直接响应 `pull_request` 的那个 workflow 里做。
## 可信编排
workflow 编排必须来自可信 base/default branch。两条路:
1. 直接触发用 `pull_request_target`
2. 先由 `pull_request` 准备输入,再由默认分支的 `workflow_run` 执行。
普通 `pull_request` 中来自 PR head 的 workflow 即使切到 base worktree,仍可被待审改动
删除或绕过。可信 workflow 必须从可信 base 运行 wrapper、validator 和 policy;只能读取
PR head 的 Git 对象,不能在持有 secret 的步骤 checkout 或执行 PR head 内容。
## 排查
| 现象 | 处理 |
| ----------------------------------------- | ------------------------------------------------------------- |
| `rc=2``ERROR: ...policy...` | 部署或配置问题,不是提交信息不合规;检查 policy 路径与 schema |
| `rc=2``ERROR: ...payload...` | 事件 payload 缺失或损坏;核对 `GITHUB_EVENT_PATH` |
| `rc=2``requires complete ... history` | checkout 是 shallow;改为完整 fetch |
| `checks: 1 subject(s)` 但期望是一个范围 | 读 stderr 的 `WARN`:集成分支未解析,或上游不是 PR |
| `validator: ... not found` | wrapper 的候选路径与实际部署布局不符 |