CHANGELOG.md规范化管理:让每次变更都有迹可循
项目迭代到第三个月,你还能准确说出 v1.2.0 和 v1.2.1 之间到底改了什么吗?靠翻 Git log?几百条 commit 里混着 fix bug、update、临时提交,找起来比考古还累。
CHANGELOG.md 解决的正是这个问题。它不是给机器看的,是给人看的------给三个月后的你、给接手项目的同事、给需要评估升级风险的运维。
一、为什么 Git log 不够用
Git log 记录的是开发过程 ,CHANGELOG 记录的是用户可见的变化。两者的受众完全不同。
一个典型的 commit 历史长这样:
a3f2c1d 修复了策略回测中日期越界的问题
8b4e2a1 调试用的print没删
c9d1f3e 重构数据加载模块
7a2b5c8 改了个变量名
...
而用户关心的是:升级到新版本后,我的回测结果会不会变?API 有没有破坏性改动?有没有新增功能?
CHANGELOG 就是把这些信息从噪声中提炼出来。
二、Keep a Changelog 格式规范
目前社区最广泛采用的规范是 Keep a Changelog。核心约定如下:
2.1 变更分类
每个版本下,变更按以下类别组织:
| 类别 | 含义 |
|---|---|
| Added | 新增功能 |
| Changed | 对现有功能的变更 |
| Deprecated | 即将移除的功能 |
| Removed | 已移除的功能 |
| Fixed | Bug 修复 |
| Security | 安全相关修复 |
分类的意义在于:使用者可以快速定位自己关心的部分。比如运维只关心 Security 和 Fixed,开发者关心 Added 和 Changed。
2.2 基本结构
markdown
# Changelog
本项目所有值得注意的变更都会记录在此文件中。
格式基于 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.0.0/),
版本号遵循 [Semantic Versioning](https://semver.org/lang/zh-CN/)。
## [Unreleased]
### Added
- 计划中的新功能
## [1.2.0] - 2024-01-15
### Added
- 新增数据源适配器接口 `DataSourceAdapter`
- 支持从 CSV 批量导入历史行情
### Changed
- 回测引擎默认使用向量化计算,速度提升约 3 倍
### Fixed
- 修复时区转换在跨日边界时的偏移问题 (#142)
### Removed
- 移除已废弃的 `legacy_backtest()` 函数
## [1.1.0] - 2023-12-01
...
几个容易忽略的细节:
[Unreleased]段永远保留在顶部,用于记录已合并但未发版的变更- 版本号用方括号包裹,后面跟发布日期
- 每条记录尽量附上 issue/PR 编号,方便追溯
- 倒序排列,最新版本在最上面
三、语义化版本号(SemVer)
版本号不是随便递增的数字。SemVer 规定格式为 MAJOR.MINOR.PATCH:
- MAJOR:不兼容的 API 变更(破坏性更新)
- MINOR:向后兼容的功能新增
- PATCH:向后兼容的 Bug 修复
对应到 CHANGELOG 的分类:
| 版本号变动 | 对应的 CHANGELOG 类别 |
|---|---|
| MAJOR +1 | Removed / Changed(破坏性) |
| MINOR +1 | Added |
| PATCH +1 | Fixed / Security |
实际项目中最容易犯的错误是:改了 API 签名但只升了 PATCH 号 。这会让依赖你项目的下游代码在 pip install --upgrade 后直接崩溃。
一个判断标准:如果你的变更会导致用户的现有代码报错或行为改变,那就是 MAJOR。
四、自动化生成 CHANGELOG
手动维护 CHANGELOG 最大的问题是容易忘。解决方案是:用 commit message 规范驱动自动生成。
4.1 Conventional Commits 规范
在提交时遵循以下格式:
<type>(<scope>): <description>
[optional body]
[optional footer]
type 的取值映射到 CHANGELOG 分类:
feat→ Addedfix→ Fixedrefactor/perf→ Changeddocs/style/test/chore→ 通常不记录BREAKING CHANGE→ 触发 MAJOR 版本
示例:
feat(data): 新增 Tushare 数据源适配器
fix(engine): 修复回测引擎在空数据时的除零错误
refactor(core): 重构订单撮合逻辑
BREAKING CHANGE: `Order.match()` 返回值从 bool 改为 MatchResult
4.2 用 Python 脚本自动生成
下面是一个可直接使用的脚本,从 Git 历史中提取 commit 并生成 CHANGELOG:
python
#!/usr/bin/env python3
"""从 git log 自动生成 CHANGELOG 片段"""
import re
import subprocess
from collections import defaultdict
from datetime import date
# commit type 到 CHANGELOG 分类的映射
TYPE_MAP = {
"feat": "Added",
"fix": "Fixed",
"perf": "Changed",
"refactor": "Changed",
"revert": "Removed",
}
# 匹配 conventional commit 格式
PATTERN = re.compile(
r"^(?P<type>\w+)(?:\((?P<scope>[^)]+)\))?!?:\s*(?P<desc>.+)$"
)
def get_commits(since_tag: str | None = None) -> list[str]:
"""获取指定 tag 之后的 commit message"""
cmd = ["git", "log", "--pretty=format:%s"]
if since_tag:
cmd.append(f"{since_tag}..HEAD")
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
return [line for line in result.stdout.strip().split("\n") if line]
def parse_commits(messages: list[str]) -> dict[str, list[str]]:
"""按分类聚合 commit"""
grouped: dict[str, list[str]] = defaultdict(list)
for msg in messages:
match = PATTERN.match(msg)
if not match:
continue
ctype = match.group("type")
category = TYPE_MAP.get(ctype)
if not category:
continue
scope = match.group("scope")
desc = match.group("desc")
entry = f"- **{scope}**: {desc}" if scope else f"- {desc}"
grouped[category].append(entry)
return dict(grouped)
def render(version: str, grouped: dict[str, list[str]]) -> str:
"""渲染为 Markdown 格式"""
lines = [f"## [{version}] - {date.today().isoformat()}", ""]
# 按固定顺序输出
for category in ["Added", "Changed", "Fixed", "Removed"]:
if category in grouped:
lines.append(f"### {category}")
lines.extend(grouped[category])
lines.append("")
return "\n".join(lines)
if __name__ == "__main__":
import sys
version = sys.argv[1] if len(sys.argv) > 1 else "Unreleased"
since = sys.argv[2] if len(sys.argv) > 2 else None
commits = get_commits(since)
grouped = parse_commits(commits)
print(render(version, grouped))
使用方式:
bash
# 生成 v1.1.0 之后到当前的所有变更
python changelog_gen.py v1.2.0 v1.1.0
输出示例:
markdown
## [v1.2.0] - 2024-01-15
### Added
- **data**: 新增 Tushare 数据源适配器
- **engine**: 支持多标的并行回测
### Fixed
- **engine**: 修复空数据时的除零错误
4.3 集成到 CI/CD
在 GitHub Actions 中,可以在发布 tag 时自动更新 CHANGELOG:
yaml
name: Generate Changelog
on:
push:
tags:
- 'v*'
jobs:
changelog:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 需要完整历史
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Generate changelog entry
run: |
python changelog_gen.py ${{ github.ref_name }} \
$(git describe --tags --abbrev=0 HEAD^) \
>> CHANGELOG.md
- name: Commit
run: |
git config user.name "github-actions"
git config user.email "actions@github.com"
git add CHANGELOG.md
git commit -m "docs: update CHANGELOG for ${{ github.ref_name }}"
git push
五、实际项目中的经验
5.1 不要记录所有 commit
docs: 修正错别字、chore: 升级依赖 这类变更对使用者没有意义。CHANGELOG 的价值在于筛选 ,而不是完整。脚本里通过 TYPE_MAP 过滤掉这些类型,正是这个目的。
5.2 Unreleased 段是核心
很多团队只在发版时才写 CHANGELOG,结果就是发版前要回忆过去一个月的所有变更。正确做法是:合并 PR 时就往 [Unreleased] 里加一条 。发版时只需把 [Unreleased] 改名为版本号,再开一个新的空段。
5.3 破坏性变更单独标注
对于 MAJOR 版本,建议在 CHANGELOG 顶部加一段迁移指南:
markdown
## [2.0.0] - 2024-02-01
### ⚠️ Breaking Changes
- `Engine.run()` 的参数从位置参数改为关键字参数
- 配置文件格式从 INI 迁移到 TOML,旧格式不再兼容
**迁移方式**:将 `config.ini` 转换为 `config.toml`,
可参考 `examples/migrate_config.py`。
### Changed
- ...
5.4 版本对比链接
在文件末尾维护版本对比链接,方便直接跳转到 diff:
markdown
[Unreleased]: https://github.com/user/repo/compare/v1.2.0...HEAD
[1.2.0]: https://github.com/user/repo/compare/v1.1.0...v1.2.0
[1.1.0]: https://github.com/user/repo/compare/v1.0.0...v1.1.0
5.5 和 release notes 的关系
CHANGELOG 是累积的、按时间倒序的完整记录;release notes 是单次发布的摘要。可以理解为:release notes 是 CHANGELOG 中某个版本段的子集或改写版。维护好 CHANGELOG,release notes 基本可以直接摘取。
六、一个完整的模板
把上面的实践整合成一个可复用的模板:
markdown
# Changelog
格式基于 [Keep a Changelog](https://keepachangelog.com/),
版本号遵循 [Semantic Versioning](https://semver.org/)。
## [Unreleased]
### Added
### Changed
### Fixed
### Removed
## [1.0.0] - 2024-01-01
### Added
- 初始版本发布
每次合并功能分支时,往 [Unreleased] 下对应的分类里加一行。发版时:
- 把
[Unreleased]改为[x.y.z] - 日期 - 在顶部新增空的
[Unreleased]段 - 打 tag,推送
这套流程跑顺之后,CHANGELOG 几乎不需要额外维护成本,却能在排查问题和评估升级风险时省下大量时间。
更多 Python 量化与自动化实战内容,请关注本站。
⭐ 今日观察列表
以下标的来自多周期趋势扫描系统(Gate.io合约),仅供技术分析学习参考,不构成任何投资建议。读者请自行判断。
数据更新时间:2026-10-09 09:01:36
| 标的 | 方向 | 信号类型 | 现价 | 参考止盈 | 参考止损 | 盈亏比 |
|---|---|---|---|---|---|---|
| ZEC 🔽 | 做空 | 扫荡做空 | $1185.05 | $1135.2779 | $1226.5267 | 1:1.2 |
| ACE 🔽 | 做空 | 弱做空 | $0.1764 | $0.169 | $0.1826 | 1:1.2 |
共 2 个标的。扫描频率:每15分钟更新。
以上数据由量化扫描系统自动生成,如有兴趣了解更多技术细节,请关注本站后续文章。