从 conventional commits 到一份体验日志
阅读约 1 分钟 更新于
Conventional commits 免费给体验日志带来三样东西:每个变化的类型、它触及的系统部分,以及它是否会破坏什么。除此之外,它什么都不给。措辞、分组和挑选,也就是体验日志真正的内容,完全留给你自己去解决,假装并非如此的流水线,最终只会发布一份排过版的 git log。
feat(exports): add CSV column selection
fix(auth): reject expired refresh tokens
chore(deps): bump node-pg to 8.11
这是 Conventional Commits 格式的三次提交。从这些提交中,机器能告诉你其中一个是新功能、一个是修复、一个是日常维护,以及每一个各自触及了系统的哪个部分。这确实很有用,也正是这个约定的全部承诺:一份能被人类之外的东西读懂的提交历史。错误在于以为这样就得到了一份体验日志。它给你的其实只是原材料。
这个约定规定了什么
一个类型、一个可选的范围,以及一段描述:type(scope): description。类型通常是 feat、fix、chore、docs、refactor、test、perf、build、ci。有两种标记表示破坏性变更:冒号前的 !,或者一段 BREAKING CHANGE: 脚注。工具依据 feat 和 fix 来决定次版本号和补丁版本号的升级,依据破坏性标记来决定主版本号的升级。
| 提交能给你的 | 体验日志需要的 | 谁来补上这个差距 |
|---|---|---|
feat / fix / chore | Added / Fixed / 内部 | 一次映射,自动完成 |
(scope) | 读者能认得出的分组 | 一个人,每个范围一次 |
! 或 BREAKING CHANGE: | 谁会受影响、什么时候、该怎么做 | 一个人,每次都要 |
| 写给审查者看的描述 | 写给客户看的结果 | 一个人,每条条目都要 |
| 一次提交 | 一个变化,可能对应许多次提交 | 压缩合并规则,或者一个人 |
这个标记告诉的是工具,而不是调用方,那是如何停用一个 API和什么是破坏性变更这两篇文章的主题。这是一份很小的规范,即使你从来不打算从中生成任何东西,遵循它依然值得,因为它强迫每次提交都做出一个决定:这是不是用户能看到的变化。
Conventional commits 在哪里停下
它止步于这一句话。这个约定所捕捉到的一切,都只是关于一个变化的元数据;变化本身仍然是用审查者的词汇描述的。
提交信息是写给审查者看的。 fix(auth): reject expired refresh tokens 是准确的,但对客户什么都没说。体验日志的读者想看到的是”当会话真正过期时你会被登出,而不再是偶尔看到 401 错误”。
范围是内部的。 exports、auth、ingest 都是模块名。它们很稳定,这让它们适合用来分组,但对代码库之外的任何人都毫无意义。
一个变化往往对应好几次提交。 一个跨越十一次提交合并进来的功能,会产生十一条条目,其中十条都是噪音,而通过压缩合并来掩盖这一点,会丢失审查历史。
chore 是个筐,不是一个分类。 依赖升级、CI 变更和重命名都会落进这里,其中有些确实和用户相关,但大多数并不相关。
所以:这个约定免费给了你类型、范围和是否破坏性的状态,却把措辞、分组和挑选完全留给你自己。这三样,才是体验日志真正的内容。 体验日志的每一条条目,到底应该由谁来负责 讨论了到底 应该由谁来承担这份措辞、分组和挑选的工作,因为这个约定本身对此完全没有意见。
如何从 conventional commits 生成一份体验日志
分两层,第二层必须是强制的。
第一层,自动化。 在合并时,从提交推导出一条草稿条目:把类型映射到体验日志的类型(feat 对应 Added,fix 对应 Fixed,破坏性标记对应带标记的 Changed),把范围作为元数据保留而不是当作文字,附上指向 PR 的链接。把它放进 Keep a Changelog 所要求的那个 Unreleased 小节里。
第二层,人工,且必须存在。 在发布之前,每一条草稿条目要么被用用户的词汇改写成一行文字,要么被标记为内部条目并从公开视图中移除。这是人们总想跳过的一步,而跳过它正是体验日志读起来像一份 diff 的原因。
一个重要的设计细节是,第二层在整条流水线里不是可选的。如果一次发布可以带着未经编辑的草稿被切出去,那它就一定会在大家都很忙的那一周被切出去。哪些步骤属于机器,哪些属于人,正是体验日志自动化这篇文章的全部内容。
切出一次发布,同时也是 git 标签、发布本身,和这条体验日志记录彼此对齐、或者开始偏离同步的那个时刻;Git 标签、发布记录,和你的体验日志 讲解了该怎样让这三者保持同步。
三个陷阱
压缩合并会吃掉脚注。 如果你的平台在压缩合并时把 PR 标题当作提交信息,那个分支里某次提交的 BREAKING CHANGE: 脚注就会消失,你的工具会悄悄地不再看见这次破坏性变更。检查一下你的压缩合并模板到底保留了什么。
回滚提交会产生幽灵条目。 一个第二天就被回滚的 fix,除非推导过程会去核对回滚记录,否则会为一件从未真正发布过的事情生成一条条目。大多数工具都不会去核对。
版本号升级和体验日志会失去同步。 如果版本号是从提交计算出来的,而体验日志是之后手工写的,两者大约会在两次发布之内就出现偏差。要么在同一个流程里计算这两者,要么就接受其中一个是错的。
如果你只想要没有流水线的机械部分
我们的体验日志生成器会在浏览器里完成推导这一步:粘贴提交记录,得到分好组、标好类型的条目。它被刻意设计成确定性的,完全在客户端运行,所以你粘贴的提交记录永远不会离开你的机器,这在提交信息来自私有仓库时很重要。它诚实地完成了收集这一半,完全不去尝试第二层,因为第二层是一种判断,而一个假装能完成它的工具,最终生成的正是这篇文章所反对的那种体验日志。
如果需要完整的流水线版本,体验日志工具汇总了现有的选择。
小结
Conventional commits 能够可靠且低成本地回答”这是什么类型的变化”。它无法回答”我们应该告诉人们什么”,无论在提交信息之上堆多少工具都无法回答,因为这个信息从来就不存在于提交信息里。为重写这一步预留预算吧。
FAQ
Conventional commits 会自动生成体验日志吗? 它会自动生成草稿:带类型、带范围、带链接的条目。写给客户看的措辞、分组以及决定省略什么,依然需要一个人来完成,跳过这一步的流水线,发布的其实就是提交信息本身。
哪些 conventional commit 类型会出现在体验日志里?
feat 和 fix 总会出现,分别对应 Added 和 Fixed。perf 通常也会出现,对应 Changed。chore、docs、refactor、test、build 和 ci 默认是内部条目,只有在有人把它们提升出来时才会出现。
Conventional commits 如何标记破坏性变更?
在类型或范围后面加一个 !(feat(api)!: ...),或者在提交正文里加一段 BREAKING CHANGE: 脚注。如果压缩合并只保留了 PR 标题,两者都会丢失。
自动化体验日志一定需要 conventional commits 吗? 不需要。对于通过 pull request 合并代码的团队,PR 标签、PR 模板和 issue 链接能传递同样的元数据。当变化的单位是一次提交时,conventional commits 是成本最低的选择。
本文的技术内容未经独立审核。如有错误,请告诉我们,我们会更正。