Changelog 不是给开发者写的
我在很多项目里见过同一种 CHANGELOG.md:密密麻麻全是 refactor(core): extract base class、chore: update deps、fix: typo in variable name。这些提交对维护代码确实有意义,但对一个想知道「这个版本改了什么」的用户来说,读完和没读一样。
这件事困扰了我很久,直到我意识到问题不在格式,在视角。
commit 是写给机器和未来自己的,changelog 是写给用户的
commit message 的受众是代码仓库的维护者——它描述的是「我改了哪里」。changelog 的受众是使用者——他们想知道的是「这对我有什么影响」。
这两件事不一样,却经常被混为一谈。于是出现了两种极端:一种是把所有 commit 原封不动 dump 进 changelog,全是技术噪音;另一种是每次发版都要花一两小时从头回忆「这版到底改了什么值得说」。
我现在的做法是在写 commit 时就分好层:
feat/fix/perf/security/breaking— 这些会进入 changelog,需要有用户可读的描述refactor/style/test/chore/ci/build— 这些不进 changelog,只是工程内务
分层本身不是关键,关键是在写 commit 时就想清楚「这条信息的受众是谁」。一旦 commit 阶段做了这个区分,changelog 的生成就几乎是机械的筛选和翻译工作。
翻译的粒度
用户友好不等于模糊。「修复了一些 bug」是最差的 changelog 写法——用户无从判断这个修复是否与自己相关。
好的描述应该回答:这个变化改变了什么可见行为?举个对比:
- 差:
fix(backend): fix null pointer exception - 好:
fix(backend): 修复题目列表在无数据时加载空白的问题
前者是代码层面的描述,后者是用户感知层面的描述。差别就在「从哪个视角出发」。
写 changelog 条目时,先问自己:如果用户不看代码,他能从这句话里知道什么变了、为什么对他有用吗?能,就是好描述;不能,说明你还在用开发者视角写。
过滤噪音是最重要的一步
我见过有人把所有类型的 commit 都塞进 changelog,包括「更新依赖」「调整 CI 配置」「规范代码格式」。这些条目不仅对用户没有价值,还会稀释真正重要的变更,让用户找不到他们关心的内容。
过滤的原则很简单:这条变更是否改变了用户直接感知到的某个行为或能力? 如果改变的只是代码内部结构、构建流程、或者开发者工具链,那就不属于 changelog 的范畴。
实际操作上,我会在生成 changelog 之前先用 commit type 做一次粗筛,然后再对 feat 和 fix 类的条目逐一检查描述是否足够具体——有时候一个 feat commit 的 subject 写得太技术性,还需要在 changelog 里重新描述。
一句话心法
commit 是存档,changelog 是对话——两件事的受众不同,不要用同一套语言写。