开源项目的自动化发版工具链:ChangeLog 生成与语义化版本的工程实践
一、发版的负担:手工整理 Release Notes 为什么是一种工程浪费
开源项目的发布流程通常涉及:整理 commit log → 编写 ChangeLog → 确定版本号 → 打 tag → 创建 GitHub Release → 发布到 npm/PyPI 包管理器。如果每次发版都手动完成这些步骤,保守估计需要 15-30 分钟。
如果你每周发版一次,这是可接受的。但如果你维护多个项目,或者你的项目活跃度很高(每周 3-5 个 PR),手动发版会成为瓶颈。更糟的是,人工确定版本号容易出错——一个 breaking change 的 PR 被当作 patch 版本发布,用户升级后系统崩溃。
自动发版工具链的价值不是"节省 15 分钟",而是消除人工判断的不一致性。版本的提升(major/minor/patch)应该由代码变更的内容决定,而不是由开发者的主观判断。
graph LR A[开发者提交 PR] --> B[Conventional Commits<br/>feat: / fix: / BREAKING CHANGE:] B --> C[PR 合并到 main] C --> D[semantic-release 分析] D --> E{commit 类型} E -->|feat:| F[minor 版本 +1] E -->|fix:| G[patch 版本 +1] E -->|BREAKING CHANGE:| H[major 版本 +1] F --> I[生成 ChangeLog] G --> I H --> I I --> J[创建 GitHub Release] J --> K[npm publish] K --> L[通知 Slack/Discord] style H fill:#ff6b6b,color:#fff style F fill:#ffd43b,color:#000 style G fill:#51cf66,color:#fff本文将搭建一套基于 semantic-release 的自动化发版工具链,覆盖 Commit 规范、ChangeLog 生成和发布验证。
二、Conventional Commits:自动化发版的基石
自动化发版的前提是 commit message 的规范化。如果每个 commit 都是update、fix bug、WIP这样的描述,没有任何工具能从 commit 信息中推断出版本应该怎么变。
Conventional Commits 规范定义了标准的 commit message 格式:
<type>[optional scope]: <description> [optional body] [optional footer(s)]关键 type 与版本的影响:
| Type | 版本影响 | 示例 |
|---|---|---|
feat: | minor | feat: add user search API |
fix: | patch | fix: resolve login timeout issue |
BREAKING CHANGE: | major | feat!: drop support for Node 14 |
docs: | 不触发发版 | docs: update API reference |
chore: | 不触发发版 | chore: upgrade dependencies |
refactor: | 不触发发版 | refactor: extract auth middleware |
在团队中强制执行 Conventional Commits 需要两个工具:commitlint(本地提交时检查)和 GitHub Actions 中的 commit message 检查。
三、完整自动化发版方案实现
commitlint 配置(.commitlintrc.js):
module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', [ 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert', ]], 'subject-min-length': [2, 'always', 10], 'subject-max-length': [2, 'always', 100], }, };配合 Husky(Git hooks):
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit "$1"'semantic-release 配置(.releaserc.json):
{ "branches": ["main"], "plugins": [ "@semantic-release/commit-analyzer", "@semantic-release/release-notes-generator", "@semantic-release/changelog", "@semantic-release/npm", "@semantic-release/github", [ "@semantic-release/git", { "assets": ["CHANGELOG.md", "package.json"], "message": "chore(release): ${nextRelease.version} [skip ci]" } ] ] }GitHub Actions 自动发版 Workflow:
name: Release on: push: branches: [main] jobs: release: runs-on: ubuntu-latest permissions: contents: write issues: write pull-requests: write id-token: write steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - uses: pnpm/action-setup@v2 - uses: actions/setup-node@v4 with: node-version: '20' cache: 'pnpm' registry-url: 'https://registry.npmjs.org' - run: pnpm install --frozen-lockfile - run: pnpm test - run: pnpm build - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} run: npx semantic-release发包前验证(额外的质量检查,在 semantic-release 之前执行):
- name: Pre-release checks run: | # 检查 package.json 的必要字段 node -e " const pkg = require('./package.json'); const required = ['name', 'version', 'main', 'types', 'files']; const missing = required.filter(f => !pkg[f]); if (missing.length) { console.error('Missing fields:', missing.join(', ')); process.exit(1); } " # 检查构建产物 test -f dist/index.mjs || (echo "Missing dist/index.mjs" && exit 1) test -f dist/index.cjs || (echo "Missing dist/index.cjs" && exit 1) # 检查包大小 SIZE=$(du -sh dist | cut -f1) echo "Package size: $SIZE"质量验证脚本(在发布前运行 E2E 测试):
- name: E2E Smoke Test run: | # 本地安装构建产物测试 mkdir /tmp/test-project && cd /tmp/test-project npm init -y npm install $GITHUB_WORKSPACE node -e "const lib = require('my-lib'); console.log('Import OK:', Object.keys(lib));"四、自动化发版的边界条件
Monorepo 的支持:如果使用 Monorepo,需要配置 semantic-release-monorepo 插件,按包分别计算版本、分别发布。
预发布版本:对于 alpha/beta 版本,需要配置预发布分支。semantic-release 支持branches: ['main', {name: 'beta', prerelease: true}],在 beta 分支上自动生成1.0.0-beta.1格式的版本。
首次发版:如果仓库还没有 tag(v1.0.0 之前的版本),semantic-release 默认从1.0.0开始。如果需要 0.x 版本,需要显式配置"initialVersion": "0.1.0"。
不适用场景:
- 非语义化版本的项目(如使用日期版本号
2024.01.15) - 严格的手动审批流程(如金融、医疗软件)
- Commit message 不规范的历史仓库(需要先用工具规范化历史 commit)
五、总结
自动化发版工具链的核心价值是用 commit message 驱动版本号变更,消除人工判断的不一致性。semantic-release 是这一领域最成熟的开源方案——社区支持广,插件丰富。
落地路径:先用 commitlint + husky 在团队中强制 Conventional Commits;然后引入 semantic-release,在 CI 中配置自动发版;最后添加预发布验证步骤(E2E smoke test、包体积检查)。
少即是多。自动发版不是为了让开发者更懒,而是为了让每次发版都遵循一致的规则。消除了"这次该升 minor 还是 patch"的纠结,你的精力可以更多投入到代码本身。