【Bug已解决】Consider adding a changelog to track version history 解决方案

📅 2026/7/23 8:17:18
【Bug已解决】Consider adding a changelog to track version history 解决方案
【Bug已解决】Consider adding a changelog to track version history 解决方案一、现象长是什么样的在迭代一个库如 DeepSpeed 这类底层训练框架时用户和贡献者经常问0.19.0 到 0.20.0 到底改了什么 但项目里只有 git log没有一份人类可读的、按版本组织的变更记录。结果是用户升级后行为变了却不知道是哪个改动导致的只能逐条翻 commit贡献者提 PR 后没人把它归到下一个版本的变更里release 时漏写安全/破坏性变更breaking change藏在某条 commit message 里用户踩坑才知道。现象特征这不是运行 bug而是可观测性/发布工程的缺口——版本历史存在 git 里但没有聚合、分类、按版本可读的 changelog导致改了什么对使用者不透明。二、背景一份好的 changelog参考 Keep a Changelog 规范应该按版本倒序组织最新的在最上面分类条目Added / Changed / Fixed / Removed / Deprecated / Security关联版本号与日期每条变更对应哪个 release可读优先给人看不是给 git 看。问题在于手写 changelog 容易和 git 历史脱节忘了更新、写错版本。更好的做法是从结构化 commit / PR 标签自动生成让 changelog 和代码变更同源。对于 DeepSpeed 这种库changelog 尤其重要ZeRO 行为、并行策略的微小改动都可能影响用户训练结果没有版本级变更说明升级就是盲更。三、根因根因一句话项目只有 git 历史、没有一份按版本聚合分类的 changelog导致每个版本改了什么对使用者不透明升级行为变化难追溯、破坏性变更易踩坑且手写 changelog 易与 git 脱节漏写。具体无聚合git log 是线性流水没有按版本切开的视图无分类改动混在一起无法区分 Added/Fixed/Security易脱节手写 changelog 靠人记常漏写或写错版本升级盲盒用户不知道 0.19→0.20 改了什么行为变了难定位破坏性变更隐蔽breaking change 藏 commit 里用户踩了才发现。本质是发布历史没有工程化成一个可读、可溯源的制品。四、最小可运行复现下面用纯 Python 模拟从结构化 commit 自动聚合成 changelogfrom typing import List, Dict # 模拟带 conventional-commit 前缀的提交 COMMITS [ (feat, AutoEP: 自动专家并行, 0.20.0), (fix, ZeRO-3 分片在 world_size1 时正确跳过, 0.20.0), (sec, 修复自托管 runner token 泄露, 0.20.0), (feat, Muon 优化器支持, 0.19.0), (fix, ds_z3_config 解析错误, 0.19.0), ] def build_changelog(commits: List[tuple], version: str) - str: cats {feat: Added, fix: Fixed, sec: Security, chg: Changed} lines [f## {version}, ] bucket: Dict[str, List[str]] {} for kind, msg, ver in commits: if ver ! version: continue bucket.setdefault(cats.get(kind, Changed), []).append(msg) for cat in (Added, Changed, Fixed, Security): if cat in bucket: lines.append(f### {cat}) for m in bucket[cat]: lines.append(f- {m}) lines.append() return \n.join(lines) def demo(): print(build_changelog(COMMITS, 0.20.0)) if __name__ __main__: demo()输出## 0.20.0 ### Added - AutoEP: 自动专家并行 ### Fixed - ZeRO-3 分片在 world_size1 时正确跳过 ### Security - 修复自托管 runner token 泄露按版本 分类聚合比 raw git log 可读得多。复现了结构化聚合 changelog的价值。五、解决方案第一层定义 changelog 结构 从 commit 生成第一层落地一份规范 changelog用 conventional-commit 前缀feat/fix/...标记 PRrelease 时脚本聚合from typing import List, Dict, Tuple CATEGORY_MAP { feat: Added, add: Added, fix: Fixed, chg: Changed, change: Changed, sec: Security, security: Security, dep: Deprecated, remove: Removed, } def generate_changelog(commits: List[Tuple[str, str, str]]) - str: 从 (kind, msg, version) 列表生成按版本倒序的 changelog。 by_ver: Dict[str, Dict[str, List[str]]] {} for kind, msg, ver in commits: cat CATEGORY_MAP.get(kind, Changed) by_ver.setdefault(ver, {}).setdefault(cat, []).append(msg) lines [# Changelog, ] for ver in sorted(by_ver.keys(), reverseTrue): lines.append(f## {ver}) lines.append() for cat in (Added, Changed, Fixed, Deprecated, Removed, Security): if cat in by_ver[ver]: lines.append(f### {cat}) for m in by_ver[ver][cat]: lines.append(f- {m}) lines.append() return \n.join(lines) def demo(): cl generate_changelog(COMMITS) print(cl.splitlines()[0], ... (生成按版本倒序的 changelog)) if __name__ __main__: demo()核心是generate_changelog按version分组、按类别分类、倒序排列。贡献者在 PR 标题用feat:/fix:/sec:前缀release 脚本自动聚合changelog 与 git 同源不再脱节。六、解决方案第二层关联版本号 自动写入 CHANGELOG.md第一层生成了文本第二层把它持久化为CHANGELOG.md并和版本发布绑定且只增量更新最新版本不重写历史from typing import List, Tuple def prepend_version(changelog_path: str, version: str, entries: List[str]): 把新版本的变更增量拼到 CHANGELOG.md 顶部保留历史。 header f## {version}\n\n body \n.join(f- {e} for e in entries) new_block header body \n\n try: old open(changelog_path, r, encodingutf-8).read() except FileNotFoundError: old # Changelog\n\n # 在 # Changelog 标题后插入新版本块 if old.startswith(# Changelog): parts old.split(\n, 1) updated parts[0] \n\n new_block (parts[1] if len(parts) 1 else ) else: updated # Changelog\n\n new_block old with open(changelog_path, w, encodingutf-8) as f: f.write(updated) return updated def demo(): prepend_version(/tmp/CHANGELOG.md, 0.21.0, [AutoEP 支持多模态专家, 修复 ZeRO-2 通信重叠竞态]) with open(/tmp/CHANGELOG.md) as f: print(f.read()[:200]) if __name__ __main__: demo()prepend_version只把最新版本增量插入顶部历史块原样保留避免重写导致冲突。release 流程里调用它changelog 随每次发版自动增长与版本号强绑定。七、解决方案第三层CI 校验 不变量测试第三层加护栏PR 必须有合规前缀否则 changelog 无法归类且 release 时 changelog 必须包含本次版本import re from typing import List VALID_PREFIXES (feat, fix, chg, sec, dep, remove, docs, test) def check_pr_title(title: str) - bool: CI 门禁PR 标题需有合规前缀否则 changelog 无法归类。 m re.match(r^(\w):, title) if not m: raise ValueError(fPR 标题需前缀如 feat:: {title}) if m.group(1) not in VALID_PREFIXES: raise ValueError(f未知前缀 {m.group(1)}无法归类到 changelog) return True def test_changelog_covers_version(changelog: str, version: str) - bool: assert f## {version} in changelog, fchangelog 缺少版本 {version} 的条目 return True def demo(): check_pr_title(feat: 添加 AutoEP 支持) try: check_pr_title(随便改了点东西) except ValueError as e: print(CI 拦截无前缀 PR, e) test_changelog_covers_version(# Changelog\n\n## 0.21.0\n, 0.21.0) print(OK: changelog 覆盖版本、PR 前缀合规) if __name__ __main__: demo()check_pr_title在 CI 拦下无前缀 PR保证每条变更都能归类进 changelogtest_changelog_covers_version在 release 时确认本次版本已在 changelog漏写就红。八、落地建议如果你想加 changelog建议定规范Keep a Changelog 格式按版本倒序、分类条目。commit/PR 前缀contributor 用feat:/fix:/sec:标记。自动生成脚本从 commit 聚合changelog 与 git 同源。增量写入prepend_version只插最新版本保留历史。CI 校验PR 无前缀拦截release 时版本必在 changelog。关联版本changelog 与版本号/发版流程绑定。九、排查清单如果改了什么对用户不透明查有无 changelog没有就按 Keep a Changelog 建。是否手写脱节改用从 commit 自动聚合。PR 前缀contributor 是否用分类前缀。增量写入新版本插顶部保留历史。CI 校验无前缀 PR 拦截、release 版本必在 changelog。版本绑定changelog 与发版流程关联。可读性按版本倒序、分类清晰。十、小结项目缺少 changelog根因是只有 git 线性历史、没有一份按版本聚合分类的可读变更记录导致每个版本改了什么对使用者不透明升级行为变化难追溯、破坏性/安全变更易踩坑且手写 changelog 易与 git 脱节漏写。它不影响运行但让升级变成盲更。修复分三层第一层定义规范 changelog 结构用 conventional-commit 前缀feat/fix/sec标记 PRgenerate_changelog按版本倒序、分类聚合changelog 与 git 同源第二层用prepend_version把最新版本增量写入CHANGELOG.md顶部、保留历史与发版流程绑定第三层加 CI 门禁check_pr_title拦截无前缀 PR 保证可归类、test_changelog_covers_version确保 release 版本必在 changelog。核心心法是changelog 不应是 release 前靠人回忆手写的文档而应是 commit/PR 分类前缀的自动化聚合产物——让它和代码变更同源、随每次发版增量生长版本历史才对使用者真正透明可追溯。