准备升级一个工具,打开更新说明,只看到“修复已知问题,优化使用体验”。到底修了什么?之前遇到的错误还在不在?升级后需不需要改配置?这句话几乎没有帮上忙。

更新日志(Changelog)应该能回答这些问题。它按版本整理值得使用者关注的变化,让人不用逐个翻提交,也能知道新版本改了什么、会影响哪里。

之前写过版本号怎么定。版本号能表达兼容性变化,具体改了哪些行为,还得靠更新日志说明。这次继续用一个虚构的文章列表工具举例,项目名、版本和日期都用于演示。

把“修复 Bug”写具体

假设工具现在是 1.4.2,约定按发布时间从新到旧返回文章。同一时刻发布多篇文章时,列表顺序却不稳定,重新构建后可能变动。

开发过程中,提交记录可能是:

1
2
3
fix: 修正排序
test: 补充相同日期的测试
refactor: 提取比较函数

这三条记录能帮助维护者追踪代码,但用户更关心自己遇到的现象有没有解决。发布说明可以合并成一条:

1
2
3
4
### Fixed

- 修复发布时间相同的文章在重新构建后顺序变化的问题;
同一时刻的文章现在按 slug 升序排列,其他日期的排序保持不变。

这条记录交代了触发条件、原来的问题和修复后的行为。这里假设工具以前没有承诺同一时刻文章的具体顺序,因此补上稳定排序不会改变既有约定。如果之前已经约定另一种顺序,就不能直接当作兼容修复。

不用给每条记录套固定句式,但至少让人看得出它在说哪个功能。“优化导出”可以具体到“CSV 导出改为分批读取,减少一次性加载的数据量”;“更快、更稳定”如果没有测试依据,就别拿来代替说明。

Olivier Lacan 在 2014 年的访谈里也谈到过这类困扰:笼统的修复说明,让使用者无法确认自己关心的问题是否解决。他讨论更新日志时,着眼的是项目外部使用者需要知道的变化。The Changelog 第 127 期

将排序修复、测试和重构提交合并成一条说明触发条件与前后行为的更新日志

三次相关提交可以整理成一条修复说明,让使用者知道问题何时出现,以及升级后有什么变化。

根目录放一份 CHANGELOG.md

Keep a Changelog 提供了一套容易采用的组织方式:每个版本单独记录,新版放在前面,写明发布日期,把同类变化归在一起;尚未发布的改动放在顶部的 Unreleased。

常用分组如下:

分组 记录内容
Added 新增功能
Changed 现有行为的调整
Deprecated 仍可使用、但准备淘汰的功能
Removed 已经移除的功能
Fixed 问题修复
Security 安全相关修正

没有内容的分组可以省略。这一点与早期访谈中保留空分组、标注“Nothing”的做法不同;项目后来的记录已明确改为去掉空分组。采用格式时,以现行说明为准。

下面是一份示例文件。日期是演示发布日期;链接中的 OWNER、REPO 和版本标签要换成自己仓库里真实存在的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# 更新日志

这里记录文章列表工具各版本对使用者有影响的变化。
日志结构参考 Keep a Changelog,版本号遵循 SemVer 2.0.0。

## [Unreleased]

### Added

- listPosts() 新增可选的 category 参数,支持按分类筛选;
不传参数时保持原有行为。

## [1.4.3] - 2026-10-15

### Fixed

- 修复发布时间相同的文章在重新构建后顺序变化的问题;
同一时刻的文章现在按 slug 升序排列。

## [1.4.2] - 2026-10-09

### Fixed

- 修复空文章列表导出 CSV 时抛出异常的问题,改为输出仅含表头的文件。

[Unreleased]: https://github.com/OWNER/REPO/compare/v1.4.3...HEAD
[1.4.3]: https://github.com/OWNER/REPO/compare/v1.4.2...v1.4.3
[1.4.2]: https://github.com/OWNER/REPO/releases/tag/v1.4.2

Unreleased 里的分类筛选尚未发布,用户安装 1.4.3 不会获得它。版本段落一定要与实际发布内容对应,不能因为功能已经合并进主分支,就写进当前稳定版的说明。

这里把 1.4.2 链接到它的 Release 页面,其他版本链接到两次标签间的比较页。比较页方便追查实现细节,正文负责解释这些改动有什么用。若项目不在 GitHub,可以换成对应平台的版本或比较链接。

哪些改动值得写进去

以这份文章列表工具为例,我会这样取舍:

改动 更新日志里的处理
修复特定日期下的排序错误 写清触发条件和修复后的结果
增加分类筛选参数 说明参数用途,以及不传参数时是否兼容
重命名内部变量 通常留在提交记录里即可
改变默认导出目录 写出旧目录、新目录及对已有脚本的影响
提高运行时最低版本 明确新要求,提醒升级环境
修正文档里错误的安装命令 如果会影响安装,值得记录

取舍依据是使用者会不会因此改变操作,而不只是改动落在哪类文件里。一次看似很小的配置修改,可能让已有部署无法启动;几百行内部重构,也可能完全不影响外部用法。

依赖升级同样要看影响。只是整理开发依赖,可以留在维护记录里;如果解决了已披露的漏洞,应说明受影响范围、修复版本,并链接到相应安全公告。不要只给出一个依赖名称,让用户自己猜为什么需要升级。

开发时随手记,发布时再核对

对个人项目,可以在实现一项面向用户的改动时,顺手往 Unreleased 加一条。多人协作则可以让 PR 同时提交变更说明。写的时候上下文还在,比隔几周翻几十条提交更容易说清楚。

Unreleased 也不要当成愿望清单。这里只放已经进入待发布代码的变化;还在讨论的功能放到 Issue 或路线图。功能被撤回时,相关条目也要一起调整。

继续前面的例子:1.4.3 已发布,分类筛选还在 Unreleased。准备发布 1.5.0 时,可以按下面的顺序处理:

  1. 对照本次发布的分支或提交范围,确认分类筛选确实在内,没有混入其他分支的功能。
  2. 把这一条移到 1.5.0 小节,填实际发布日期,顶部继续保留 Unreleased。
  3. 将 1.5.0 的比较链接设为 v1.4.3...v1.5.0,把 Unreleased 链接的起点改为 v1.5.0。
  4. 核对包版本、发布标签、构建产物和 Release 说明,确认它们对应同一次发布。

如果同时维护多个版本分支,比较链接的终点应指向相应分支,不能全部照抄 HEAD。标签也应落在实际发布的提交上,避免日志描述的是一份代码,用户拿到的是另一份。

日志里写 Fixed 还是 Changed,不会替你决定版本号。对于已经遵守 SemVer 且进入 1.0.0 以后的项目,兼容修复、新增兼容功能、不兼容 API 修改,分别涉及修订号、次版本号、主版本号。一次发布混合多类改动时,要考虑其中要求最高的兼容性变化。SemVer 2.0.0

核对发布范围与兼容性后,将分类筛选从 Unreleased 移入 1.5.0,并更新日期和版本链接

示例中的分类筛选随 1.5.0 发布;顶部继续保留 Unreleased,供下一轮开发记录使用。

弃用和删除要分开交代

假设旧版用 limit 配置每页数量,之后打算统一为 pageSize。如果一发布新版就删掉旧字段,读者只看到“整理分页配置”,升级后很可能才发现文章数量不对。

可以先提供兼容期,在 1.5.0 中新增 pageSize,仍接受 limit,并说明两个字段同时出现时如何处理。比如约定新字段优先,日志可以写成:

1
2
3
4
5
6
7
8
### Added

- 新增 pageSize 配置项,用于指定每页文章数量。

### Deprecated

- limit 仍可使用,但计划在 2.0.0 移除,请改用 pageSize。
两项同时配置时,以 pageSize 为准。

真正发布 2.0.0、移除旧字段时,再写:

1
2
3
4
### Removed

- 不再读取 limit 配置项。升级前请将 limit: 18 改为 pageSize: 18;
仅保留旧字段会使用默认每页数量。

这让用户知道现在还能不能用、何时需要改,以及不改会发生什么。删除时间尚未确定,就写“将在后续主版本移除”,不要随意承诺一个日期。若遵守 SemVer,标记公共 API 弃用本身也要求递增次版本号,真正的不兼容删除则涉及主版本号。SemVer 第 7、8 条

自动生成后,还要改哪几处

GitHub 的自动发布说明可以汇总合并的 PR、贡献者和完整变更链接,也支持用标签分组或排除部分内容。它很适合帮忙收集发布材料,官方流程同样要求检查生成结果。GitHub 自动生成发布说明

但 PR 标题如果只有“修复分页”,自动生成出来仍然只有这几个字。发布前可以重点检查:同一功能的多次修复是否需要合并,内部重构是否挤占了主要内容,以及参数变更有没有写出迁移办法。

约定式提交也能帮工具识别变更。feat、fix、BREAKING CHANGE 等标记有助于自动分类和版本计算,但不是所有 fix 都必然兼容;破坏性修改可以出现在任何提交类型中。Conventional Commits 1.0.0

如果让 AI 帮忙整理,也应给它本次发布范围内的提交或 PR,而不是只给一个版本号。特别要检查它有没有把“准备支持”写成“已经支持”,或把模糊的性能改动扩写成未经测试的提升数字。

个人项目可以把仓库里的 CHANGELOG.md 作为维护入口,发布时把本版本段落同步到 GitHub Releases;较长的迁移步骤另写文档,再从两处链接过去。这样后续纠错有明确的原稿,不容易出现两份说明各写各的。

发现旧日志漏了一项重要变化,可以补充说明;如果某个版本已经发出又撤回,也应留下撤回原因和可用替代版本,方便已经安装它的人追查。文字说明的修订,不意味着可以悄悄替换已发布的软件包。遵守 SemVer 的项目需要用新版本发布软件内容的修改。SemVer 第 3 条

如果项目以前从没维护过更新日志,可以先从下一次发布开始。核实得清楚的旧版本逐步补录,记不清的不要靠猜。先把最近一次“修复 Bug”改写成具体的问题、影响和处理结果,这份文件就已经有用了。