每次发版前最烦人的事情是什么?写 changelog。翻一个月的 commit history,对着一堆 fix typo、update readme、wip、asdf 这样的 commit message,欲言又止。

release-management 仓库里的 changelog 自动化模块,解决的就是这个问题。

为什么手动写 changelog 是浪费时间

想象一个典型场景:ops-transformer 要从 1.2.0 发到 2.0.0,这两个月里仓库有 300 多次 commit。如果手动整理:

  1. 打开 git log,从第一个 commit 翻到最后一个
  2. 每次 commit 都要点进去看 diff,判断是 feature 还是 fix
  3. 把内容归类到不同 section
  4. 检查有没有 breaking change 要特别标注
  5. 反复确认有没有遗漏的重要改动

这活儿熟练工也要干两个小时。而且人整理的东西容易有主观偏差,比如觉得自己写的 update 比 fix 更重要,就把它归到 New Features 里——其实只是改了变量名。

自动化生成的基本原理

changelog 生成的本质是:从结构化的 commit message 里提取信息,按版本聚合,按类别分组。

核心逻辑其实不复杂:

from release_management import ChangeLog

# 从指定 tag 范围生成 changelog
changelog = ChangeLog.generate(
    repo="ops-transformer",
    from_tag="v1.2.0",
    to_tag="v2.0.0",
    # conventional commit 格式解析
    categories={
        "feat": "New Features",       # feat: 开头的 commit
        "fix": "Bug Fixes",           # fix: 开头的 commit
        "perf": "Performance",        # perf: 开头的 commit
        "docs": "Documentation",      # docs: 开头的 commit
        "refactor": "Refactoring",    # refactor: 开头的 commit
    },
    # 过滤掉哪些类型的 commit 不出现在 changelog 里
    exclude=["style", "test", "chore"]
)

print(changelog)

输出的前提是 commit message 符合 Conventional Commits 规范,比如:

feat(flash-attention): add v2 implementation supporting 32k context
fix(moe-router): correct topk overflow when experts is odd
perf(mc2-allgather): reduce communication overhead by 15%
docs: update README for v2 migration guide

feat: 后面括号里的 scope 会被保留下来,用于标注是哪个子模块的改动。changelog 里会显示 flash-attention: add v2 implementation...,阅读体验好很多。

自定义过滤规则:怎么过滤噪音 commit

自动生成有一个问题:仓库里有很多没有意义的 commit,比如依赖升级、CI 配置更新、merge branch 这些。直接放进 changelog 会显得很业余。

# 定义过滤规则,正则匹配排除噪音 commit
changelog = ChangeLog.generate(
    repo="ops-transformer",
    from_tag="v1.2.0",
    to_tag="v2.0.0",
    filters={
        # 排除所有依赖相关的 commit
        "exclude_patterns": [
            r"^chore(deps):",
            r"^chore\(ci\):",
            r"^style:",
            r"^test:",
            r"Merge branch",
            r"^Update .*lock",
        ],
        # 只保留超过 N 个字符的 commit message
        # 太短的 commit 往往是 typo fix 或者无关紧要的改动
        "min_message_length": 20,
        # merge commit 通常不包含实质内容
        "exclude_merges": True,
    }
)

有一个容易忽略的细节:breaking change 不会自动标注。Conventional Commits 规范里 breaking change 要在 footer 里写 BREAKING CHANGE:,或者在 message 末尾加 !:。但很多开发者不知道这个约定,代码写完了才发现有个 API 不兼容。

# 自动扫描 breaking change
changelog = ChangeLog.generate(
    repo="ops-transformer",
    from_tag="v1.2.0",
    to_tag="v2.0.0",
    # 扫描常见的不兼容模式
    breaking_patterns=[
        r"^BREAKING CHANGE:",
        r"remove.*parameter",
        r"rename.*argument",
        r"change.*return.*type",
    ],
    # 识别到 breaking change 自动提升到 Breaking Changes section
    auto_breaking=True
)

生成后人工 review 的几个检查点

自动化生成之后,人工 review 仍然必要,但重点变了——不是从头整理,而是检查生成结果有没有明显错误。

我一般会过这几个点:

第一,大功能有没有被漏掉。如果某个 commit 加了新的融合模式,但在 changelog 里找不到,那八成是被 filter 规则误杀了。去 commit history 里搜一下关键词,确认是漏掉了还是确实被过滤了。

第二,分组是否合理。有时候同一个 commit 改了多个文件,conventional commit 的 scope 只能标一个,其他子模块的改动容易被忽略。这种情况要手动拆分成多条。

第三,语气是否一致。自动化生成的是 add xxx 和 fix xxx,但 changelog 作为一个对外文档,应该统一人称和时态。我一般会用脚本做一次批量替换:

# 统一 changelog 语气:第三人称、一般过去时
changelog = ChangeLog.generate(...)
normalized = changelog.replace(
    "add", "Added"
).replace(
    "fix", "Fixed"
).replace(
    "update", "Updated"
)

和 CI 集成:每次 PR 合入自动更新

最理想的使用方式是让 changelog 生成成为 CI 流程的一部分,而不是发版前临时抱佛脚。

# .github/workflows/changelog.yml
name: Changelog Update

on:
  pull_request:
    types: [closed]
    branches: [main]

jobs:
  update-changelog:
    if: github.event.pull_request.merged == true
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Generate changelog entry
        run: |
          python -m release_management.changelog \
            --repo ${{ github.event.pull_request.head.repo.name }} \
            --sha ${{ github.event.pull_request.head.sha }} \
            --output changelog_entry.txt

      - name: Create PR for changelog update
        uses: peter-evans/create-pull-request@v4
        with:
          title: "docs: update changelog"
          body-file: changelog_entry.txt
          branch: changelog/${{ github.event.pull_request.number }}

这样每次 PR 合并都会自动生成一条 changelog entry,后续发版的时候直接合并这些 entry 就行了,changelog 内容早就准备好了。

工具不是万能的

最后说一个反直觉的点:changelog 工具再好,也解决不了根本问题——commit message 质量。

如果团队里 commit message 写得很随意,update、fix、wip 满天飞,那 changelog 生成出来也是一堆噪音。工具只是放大镜,能放大好的规范,也能放大乱规范。

所以在用 release-management 的 changelog 模块之前,先把 commit message 规范建起来。Conventional Commits 不难学,团队里约定一个 scope 列表(比如 flash-attention、moe-router、mc2-allgather),每次 commit 前花 30 秒想清楚这条 commit 改了什么——之后写 changelog 能省两小时。

仓库在 https://atomgit.com/cann/release-management,changelog 相关的 API 文档可以直接看源码里的 changelog.py,逻辑很清晰。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐