每次发版前最烦人的事情是什么?写 changelog。翻一个月的 commit history,对着一堆 fix typoupdate readmewipasdf 这样的 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. 反复确认有没有遗漏的重要改动

这活儿熟练工也要干两个小时。而且人整理的东西容易有主观偏差,比如觉得自己写的 updatefix 更重要,就把它归到 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 xxxfix 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 写得很随意,updatefixwip 满天飞,那 changelog 生成出来也是一堆噪音。工具只是放大镜,能放大好的规范,也能放大乱规范。

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

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

Logo

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

更多推荐