更新日志怎么写?从一次 Bug 修复开始维护 CHANGELOG
准备升级一个工具,打开更新说明,只看到“修复已知问题,优化使用体验”。到底修了什么?之前遇到的错误还在不在?升级后需不需要改配置?这句话几乎没有帮上忙。
更新日志(Changelog)应该能回答这些问题。它按版本整理值得使用者关注的变化,让人不用逐个翻提交,也能知道新版本改了什么、会影响哪里。
之前写过版本号怎么定。版本号能表达兼容性变化,具体改了哪些行为,还得靠更新日志说明。这次继续用一个虚构的文章列表工具举例,项目名、版本和日期都用于演示。
把“修复 Bug”写具体
假设工具现在是 1.4.2,约定按发布时间从新到旧返回文章。同一时刻发布多篇文章时,列表顺序却不稳定,重新构建后可能变动。
开发过程中,提交记录可能是:
1 | fix: 修正排序 |
这三条记录能帮助维护者追踪代码,但用户更关心自己遇到的现象有没有解决。发布说明可以合并成一条:
1 | ### Fixed |
这条记录交代了触发条件、原来的问题和修复后的行为。这里假设工具以前没有承诺同一时刻文章的具体顺序,因此补上稳定排序不会改变既有约定。如果之前已经约定另一种顺序,就不能直接当作兼容修复。
不用给每条记录套固定句式,但至少让人看得出它在说哪个功能。“优化导出”可以具体到“CSV 导出改为分批读取,减少一次性加载的数据量”;“更快、更稳定”如果没有测试依据,就别拿来代替说明。
Olivier Lacan 在 2014 年的访谈里也谈到过这类困扰:笼统的修复说明,让使用者无法确认自己关心的问题是否解决。他讨论更新日志时,着眼的是项目外部使用者需要知道的变化。The Changelog 第 127 期

三次相关提交可以整理成一条修复说明,让使用者知道问题何时出现,以及升级后有什么变化。
根目录放一份 CHANGELOG.md
Keep a Changelog 提供了一套容易采用的组织方式:每个版本单独记录,新版放在前面,写明发布日期,把同类变化归在一起;尚未发布的改动放在顶部的 Unreleased。
常用分组如下:
| 分组 | 记录内容 |
|---|---|
| Added | 新增功能 |
| Changed | 现有行为的调整 |
| Deprecated | 仍可使用、但准备淘汰的功能 |
| Removed | 已经移除的功能 |
| Fixed | 问题修复 |
| Security | 安全相关修正 |
没有内容的分组可以省略。这一点与早期访谈中保留空分组、标注“Nothing”的做法不同;项目后来的记录已明确改为去掉空分组。采用格式时,以现行说明为准。
下面是一份示例文件。日期是演示发布日期;链接中的 OWNER、REPO 和版本标签要换成自己仓库里真实存在的值。
1 | # 更新日志 |
Unreleased 里的分类筛选尚未发布,用户安装 1.4.3 不会获得它。版本段落一定要与实际发布内容对应,不能因为功能已经合并进主分支,就写进当前稳定版的说明。
这里把 1.4.2 链接到它的 Release 页面,其他版本链接到两次标签间的比较页。比较页方便追查实现细节,正文负责解释这些改动有什么用。若项目不在 GitHub,可以换成对应平台的版本或比较链接。
哪些改动值得写进去
以这份文章列表工具为例,我会这样取舍:
| 改动 | 更新日志里的处理 |
|---|---|
| 修复特定日期下的排序错误 | 写清触发条件和修复后的结果 |
| 增加分类筛选参数 | 说明参数用途,以及不传参数时是否兼容 |
| 重命名内部变量 | 通常留在提交记录里即可 |
| 改变默认导出目录 | 写出旧目录、新目录及对已有脚本的影响 |
| 提高运行时最低版本 | 明确新要求,提醒升级环境 |
| 修正文档里错误的安装命令 | 如果会影响安装,值得记录 |
取舍依据是使用者会不会因此改变操作,而不只是改动落在哪类文件里。一次看似很小的配置修改,可能让已有部署无法启动;几百行内部重构,也可能完全不影响外部用法。
依赖升级同样要看影响。只是整理开发依赖,可以留在维护记录里;如果解决了已披露的漏洞,应说明受影响范围、修复版本,并链接到相应安全公告。不要只给出一个依赖名称,让用户自己猜为什么需要升级。
开发时随手记,发布时再核对
对个人项目,可以在实现一项面向用户的改动时,顺手往 Unreleased 加一条。多人协作则可以让 PR 同时提交变更说明。写的时候上下文还在,比隔几周翻几十条提交更容易说清楚。
Unreleased 也不要当成愿望清单。这里只放已经进入待发布代码的变化;还在讨论的功能放到 Issue 或路线图。功能被撤回时,相关条目也要一起调整。
继续前面的例子:1.4.3 已发布,分类筛选还在 Unreleased。准备发布 1.5.0 时,可以按下面的顺序处理:
- 对照本次发布的分支或提交范围,确认分类筛选确实在内,没有混入其他分支的功能。
- 把这一条移到
1.5.0小节,填实际发布日期,顶部继续保留Unreleased。 - 将
1.5.0的比较链接设为v1.4.3...v1.5.0,把Unreleased链接的起点改为v1.5.0。 - 核对包版本、发布标签、构建产物和 Release 说明,确认它们对应同一次发布。
如果同时维护多个版本分支,比较链接的终点应指向相应分支,不能全部照抄 HEAD。标签也应落在实际发布的提交上,避免日志描述的是一份代码,用户拿到的是另一份。
日志里写 Fixed 还是 Changed,不会替你决定版本号。对于已经遵守 SemVer 且进入 1.0.0 以后的项目,兼容修复、新增兼容功能、不兼容 API 修改,分别涉及修订号、次版本号、主版本号。一次发布混合多类改动时,要考虑其中要求最高的兼容性变化。SemVer 2.0.0

示例中的分类筛选随 1.5.0 发布;顶部继续保留 Unreleased,供下一轮开发记录使用。
弃用和删除要分开交代
假设旧版用 limit 配置每页数量,之后打算统一为 pageSize。如果一发布新版就删掉旧字段,读者只看到“整理分页配置”,升级后很可能才发现文章数量不对。
可以先提供兼容期,在 1.5.0 中新增 pageSize,仍接受 limit,并说明两个字段同时出现时如何处理。比如约定新字段优先,日志可以写成:
1 | ### Added |
真正发布 2.0.0、移除旧字段时,再写:
1 | ### Removed |
这让用户知道现在还能不能用、何时需要改,以及不改会发生什么。删除时间尚未确定,就写“将在后续主版本移除”,不要随意承诺一个日期。若遵守 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”改写成具体的问题、影响和处理结果,这份文件就已经有用了。