CHANGELOG.md规范化管理:让每次变更都有迹可循

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
...

几个容易忽略的细节:

  1. [Unreleased] 段永远保留在顶部,用于记录已合并但未发版的变更
  2. 版本号用方括号包裹,后面跟发布日期
  3. 每条记录尽量附上 issue/PR 编号,方便追溯
  4. 倒序排列,最新版本在最上面

三、语义化版本号(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 → Added
  • fix → Fixed
  • refactor / perf → Changed
  • docs / 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] 下对应的分类里加一行。发版时:

  1. 把 [Unreleased] 改为 [x.y.z] - 日期
  2. 在顶部新增空的 [Unreleased] 段
  3. 打 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分钟更新。


以上数据由量化扫描系统自动生成,如有兴趣了解更多技术细节,请关注本站后续文章。

相关推荐
漂着的圆木3 天前
Flow音效可分轨后,AI视频怎样防止音画错位与混错版本
版本管理·ai视频·音画同步·googleflow·音效制作
RuoyiOffice12 天前
SpringBoot3+Vue3 低代码业务表单:从拖拽设计、独立建表到审批与数据查询
spring boot·vue3·数据建模·flowable·版本管理·低代码表单·ruoyi office
ZGi.ai18 天前
开源 AI Agent Runtime:制度版本问答
知识库·版本管理·ai智能体·zgi·agentruntime·制度问答
又見山19 天前
版本管理与防勒索恢复机制:同步盘的最后一道防线
版本管理·同步网盘
RuoyiOffice23 天前
SpringBoot3+OnlyOffice 自动保存版本:定时回存、间隔合并与 50 版上限怎么配
spring boot·在线文档·onlyoffice·版本管理·spring boot 3·自动保存·ruoyi office
ZGi.ai1 个月前
ZGI Agent 发布:配置改了,线上为何还是旧版本
workflow·版本管理·企业ai·zgi·agent发布·运行记录
达达车2 个月前
git使用技巧记录
git·使用技巧·版本管理
ZGi.ai2 个月前
ZGI Agent:修改配置时,怎样稳住线上版本?
版本管理·aiagent·zgi·agent发布·配置快照·agentruntime
牵着毛驴唱着歌4 个月前
JaVers 版本历史功能完整实现指南
java·javers·变更记录