commitlint:让最终提交历史满足可执行契约
提交消息看起来只是 Git 对象里的一段文本,进入主线后却会被发布工具、Changelog、审计查询和故障回溯反复消费。update、fix stuff 之类消息不会破坏代码,却会让后续系统失去稳定输入。commitlint 的职责很窄:读取一条或一段提交消息,按解析器和规则给出确定的退出状态。它不理解代码是否兼容,也不替团队决定 SemVer。
先确认主线最后留下哪条消息
门禁应约束最终进入主分支的历史,而不是开发者曾经输入过、合并时又被丢弃的文本。Merge commit 会保留分支提交并新增合并消息;rebase merge 会重放每个提交;squash merge 通常只留下 squash 消息。采用 squash 的仓库如果只检查开发分支 commits,门禁即使全绿,最终主线消息仍可能不合格。
| 合并方式 | 主线真实输入 | 主要校验入口 |
|---|---|---|
| Merge commit | 分支提交与合并提交 | PR 提交区间,加平台生成消息策略 |
| Rebase merge | 重放后的每个提交 | PR 的 base/head 区间 |
| Squash merge | squash 后的一条提交 | PR 标题或最终 squash 消息 |
规则设计从这里开始。团队要先确定 type、scope、header 长度、breaking change、revert、merge 和机器人消息的契约,再选择 preset。默认配置是合理起点,不是业务事实。
parser 只负责把文本变成结构
一个常见输入如下:
feat(api): add cursor pagination
Return a stable cursor for the order query.
BREAKING CHANGE: remove the legacy pageNumber fieldparser 会把 header、type、scope、subject、body 和 footer 分开,规则随后检查这些字段。feat 并不自动证明向后兼容,fix 也不保证只能发 patch;commitlint 能证明格式和团队约定一致,不能从消息推断真实消费者影响。
规则数组包含严重级别、适用条件和值。[2, "always", 100] 表示错误级规则,要求 header 始终不超过 100 个字符;0 是关闭,1 只警告,2 才会让 CLI 非零退出。把关键规则设成 warning,流水线看似接入了工具,实际没有阻断能力。
锁定 commitlint 与配置加载方式
实验基线使用 commitlint 21.2.2,它要求 Node >=22.12.0。版本只用于复现实验;升级时同时检查 CLI、共享 preset、parser 和 Node 支持范围。
npm install --save-dev --save-exact \
@commitlint/cli@21.2.2 \
@commitlint/config-conventional@21.2.2
npx commitlint --version配置使用明确的模块格式。Node 项目模块类型不清晰时,commitlint.config.mjs 比一个含义随环境变化的 .js 文件更稳妥:
export default {
extends: ["@commitlint/config-conventional"],
defaultIgnores: true,
rules: {
"type-enum": [2, "always", ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "test"]],
"scope-enum": [2, "always", ["api", "web", "worker", "docs", "deps"]],
"header-max-length": [2, "always", 100],
"subject-empty": [2, "never"],
},
ignores: [(message) => message.startsWith("chore(bot): sync ")],
};extends 加载共享规则,rules 表达仓库自己的取舍,ignores 是需要审查的例外代码。过宽的 includes("bot") 会允许人工消息伪装成机器人。配置加载失败也必须失败,不能静默退化成无规则运行。
npx commitlint --print-config json > tmp/commitlint-effective.json
printf '%s\n' 'feat(api): add cursor pagination' | npx commitlint --verbose
printf '%s\n' 'update' | npx commitlint --verbose第一条消息应退出 0,第二条应报告 type 与 subject 相关错误并非零退出。--print-config 保存的是排障证据:共享配置升级后,团队能比较有效规则,而不是只比较源文件中一行版本号。
commit-msg Hook 读取的是 Git 提供的文件
消息在 commit-msg 阶段已经写入临时文件,Git 把文件路径作为第一个参数交给 Hook。commitlint 应通过 --edit 读取它,不能挂在还没有最终消息的 pre-commit 阶段。
# .husky/commit-msg
npx --no -- commitlint --edit "$1"npx --no 禁止在本地依赖缺失时从网络临时安装另一个版本。Hook 保持薄,只转交路径并原样传播退出码;Husky 的安装、core.hooksPath 和新 clone 启用问题由 Husky 主文承担。
用临时分支验证真实入口时,先提交一条违规消息,确认 Git 拒绝且 git log 没有新增对象;再提交合格消息并执行 npx commitlint --last --verbose。实验不能在有未备份修改的工作区进行,也不要把故意制造的坏历史推到共享仓库。
本地 Hook 可以被 --no-verify 绕过,网页编辑和机器人也未必运行仓库 Hook。这不是实现缺陷,而是客户端边界。强制约束必须在服务端再次执行。
CI 校验明确的提交区间
流水线不要猜 HEAD~1。平台事件应提供 base/head SHA,任务先证明两个对象存在,再检查区间:
set -eu
git cat-file -e "$BASE_SHA^{commit}"
git cat-file -e "$HEAD_SHA^{commit}"
npx commitlint --from "$BASE_SHA" --to "$HEAD_SHA" --verbose浅克隆经常让命令漏检或直接找不到 base。GitHub Actions 可获取完整历史,其他平台也要按事件语义取得准确引用。若采用 squash merge,PR 阶段还应检查将成为最终提交的标题;若保留所有提交,单看 PR 标题则不够。
机器人消息不应按作者邮箱一概跳过。依赖更新、代码生成和同步任务同样进入主线,应使用专用 type/scope 或精确忽略条件,并由机器人身份、工作流权限和分支保护共同约束。消息前缀不是身份凭证。
故障先分配置、输入和历史
“没有规则”通常是配置文件没有被发现、模块格式不匹配,或共享 preset 缺失。先运行 --print-config,不要靠重复安装掩盖加载问题。CI 突然报告大量旧 commit,优先打印 base/head、merge-base 与 fetch 深度;不断扩大 --from 只是换一种猜测。
Hook 文件存在但没有执行时,检查 git config --show-origin --get core.hooksPath、依赖安装阶段和可执行环境。GUI 客户端与终端 PATH 可能不同,稳定做法是调用项目脚本并验证真实客户端,而不是把个人绝对路径写进共享 Hook。
merge、revert 或版本消息被跳过时,查看 defaultIgnores 与自定义 ignores。默认忽略是产品行为;团队若要求这些消息也受控,应关闭或收窄规则,并为平台实际生成的消息保存正反 fixture。
升级、权限与退出
commitlint 配置能够加载 JavaScript、共享 preset、parser 和自定义规则,这些依赖会在开发机及 CI 权限下执行。锁文件、依赖审查和升级 diff 因此是门禁的一部分。动态生成 scope 时,只从可信 workspace 元数据读取,不能把未经转义的 PR 文本拼入 shell。
升级先在代表仓库比较 --print-config,再回放合格、违规、merge、revert、breaking change 和机器人样例。确认 Node 运行时、Hook 与 CI 区间都通过后再扩大范围。规则变化影响历史解释,应像 API 契约一样评审。
移除工具时,先让服务端替代规则通过正反实验,再删除 commit-msg 调用、配置与依赖。若 Husky 仍承载其他 Hook,不要删除整个 .husky/。卸载后违规消息仍应在不可绕过边界被拒绝,否则这不是迁移,而是控制消失。
架构评审看的是最终历史
可用的 commitlint 门禁能回答几个具体问题:主线最终保留哪条消息;有效规则由哪些包组合出来;本地绕过后哪一层复核;浅克隆和错误 SHA 是否会假绿;例外由谁批准;升级如何比较规则差异。消息可读只是表面结果,稳定输入、确定退出码和不可绕过复核才是它在工程体系里的价值。
