---
title: "更新日志怎么写？从一次 Bug 修复开始维护 CHANGELOG"
source: https://www.iowill.com/blog/2026/10/maintaining-a-changelog/
date: 2026-10-15
updated: 2026-10-15
tags: [Git, 更新日志, 版本管理, 语义化版本, 软件工程]
categories: [编程开发]
---

# 更新日志怎么写？从一次 Bug 修复开始维护 CHANGELOG

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

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

之前写过[版本号怎么定](https://www.iowill.com/blog/2026/10/semantic-versioning/)。版本号能表达兼容性变化，具体改了哪些行为，还得靠更新日志说明。这次继续用一个虚构的文章列表工具举例，项目名、版本和日期都用于演示。

## 把“修复 Bug”写具体

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

开发过程中，提交记录可能是：

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

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

```markdown
### Fixed

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

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

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

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

![将排序修复、测试和重构提交合并成一条说明触发条件与前后行为的更新日志](https://img.iowill.com/file/%E7%BC%96%E7%A8%8B%E5%BC%80%E5%8F%91/%E6%9B%B4%E6%96%B0%E6%97%A5%E5%BF%97%E6%80%8E%E4%B9%88%E5%86%99/body1.png)

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

## 根目录放一份 CHANGELOG.md

[Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 提供了一套容易采用的组织方式：每个版本单独记录，新版放在前面，写明发布日期，把同类变化归在一起；尚未发布的改动放在顶部的 `Unreleased`。

常用分组如下：

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

没有内容的分组可以省略。这一点与早期访谈中保留空分组、标注“Nothing”的做法不同；项目后来的记录已明确改为去掉空分组。采用格式时，以[现行说明](https://keepachangelog.com/zh-CN/1.1.0/)为准。

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

```markdown
# 更新日志

这里记录文章列表工具各版本对使用者有影响的变化。
日志结构参考 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](https://semver.org/lang/zh-CN/)

![核对发布范围与兼容性后，将分类筛选从 Unreleased 移入 1.5.0，并更新日期和版本链接](https://img.iowill.com/file/%E7%BC%96%E7%A8%8B%E5%BC%80%E5%8F%91/%E6%9B%B4%E6%96%B0%E6%97%A5%E5%BF%97%E6%80%8E%E4%B9%88%E5%86%99/body2.png)

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

## 弃用和删除要分开交代

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

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

```markdown
### Added

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

### Deprecated

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

真正发布 `2.0.0`、移除旧字段时，再写：

```markdown
### Removed

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

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

## 自动生成后，还要改哪几处

GitHub 的自动发布说明可以汇总合并的 PR、贡献者和完整变更链接，也支持用标签分组或排除部分内容。它很适合帮忙收集发布材料，官方流程同样要求检查生成结果。[GitHub 自动生成发布说明](https://docs.github.com/en/repositories/releasing-projects-on-github/automatically-generated-release-notes)

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

约定式提交也能帮工具识别变更。`feat`、`fix`、`BREAKING CHANGE` 等标记有助于自动分类和版本计算，但不是所有 `fix` 都必然兼容；破坏性修改可以出现在任何提交类型中。[Conventional Commits 1.0.0](https://www.conventionalcommits.org/zh-hans/v1.0.0/)

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

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

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

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