Conventional Commits 在 Hacker News 上引发了一场激烈争论——有开发者直言「这套规范把团队的注意力引向了错误的地方」。但争议的背后,真正的问题不是格式本身,而是你是否把它当作一个自动化流水线的入口。如果只是用来写 feat: 前缀,那确实是浪费时间;但如果把它接入 commitlint + semantic-release + changelog 全链路,它能为你省下数小时的手动版本管理工作。
本文不是「该不该用 Conventional Commits」的站队文,而是一份工程化落地指南——从规范解读、工具链配置、到常见的踩坑场景,帮你把提交规范从「仪式感」变成「生产力」。
🔧 一、Conventional Commits 规范与工程价值
1.1 规范核心速览
Conventional Commits 的格式并不复杂:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
常用的 type 如下:
| Type | 含义 | 触发版本变更 | 使用场景 |
|---|---|---|---|
feat |
新功能 | ✅ MINOR | 用户可见的新功能 |
fix |
修复 Bug | ✅ PATCH | 修复线上问题 |
docs |
文档 | ❌ | README、注释更新 |
style |
代码风格 | ❌ | 格式化、空格等 |
refactor |
重构 | ❌ | 不改变行为的代码调整 |
perf |
性能优化 | ✅ PATCH | 有性能提升的改动 |
test |
测试 | ❌ | 补充或修改测试 |
chore |
构建/工具 | ❌ | 依赖更新、CI 配置 |
ci |
CI 配置 | ❌ | GitHub Actions 等 |
BREAKING CHANGE |
破坏性变更 | ✅ MAJOR | API 不兼容改动 |
💡 提示:
BREAKING CHANGE写在 footer 中,或者在 type 后加!(如feat!:),两种写法语义等价,都会触发 MAJOR 版本升级。
1.2 工程价值:不是格式,是自动化信号
很多团队把 Conventional Commits 当成「代码审查的装饰品」——CI 里加个格式检查就算完事。这是最大的误区。
真正的价值在于:type 是一个给机器读的信号。 它告诉自动化工具三件事:
- 要不要发版 —
feat和fix触发新版本,docs和chore不触发 - 发什么级别的版本 —
feat升 MINOR,fix升 PATCH,BREAKING CHANGE升 MAJOR - 写进哪个 Changelog 分类 — 自动按 type 分组生成变更日志
如果你的项目没有自动化版本发布流程,那 Conventional Commits 确实「只是一堆格式约束」。但一旦你接入 semantic-release 或 release-please,整个发布流程可以完全自动化——从 commit 到 npm publish,零人工干预。
// semantic-release 的核心逻辑(简化版)
// 它读取自上个 tag 以来的所有 commit,根据 type 决定版本号
function determineVersion(commits) {
let bump = 'patch'; // 默认 patch
for (const commit of commits) {
if (commit.header.includes('BREAKING CHANGE')) {
return 'major'; // 破坏性变更直接跳到 major
}
if (commit.type === 'feat') {
bump = 'minor'; // 新功能升 minor
}
}
return bump;
}
⚠️ **警告:**不要在 monorepo 中对所有包使用同一个 semantic-release 实例。每个子包应独立管理版本号,否则一个包的
fix会触发所有包的版本更新。
🚀 二、全链路工具配置实战
2.1 Commitlint:格式校验第一道防线
安装核心依赖:
# 安装 commitlint CLI 和 conventional 配置
npm install --save-dev @commitlint/cli @commitlint/config-conventional
# 安装 husky 用于 git hook
npm install --save-dev husky
# 初始化 husky
npx husky init
创建 commitlint.config.js:
// commitlint.config.js
// 使用 conventional 规则,这是最常用的预设
export default {
extends: ['@commitlint/config-conventional'],
rules: {
// type 必须是以下之一
'type-enum': [
2, // 2 = error
'always',
[
'feat', // 新功能
'fix', // 修复
'docs', // 文档
'style', // 格式
'refactor', // 重构
'perf', // 性能
'test', // 测试
'build', // 构建
'ci', // CI
'chore', // 杂项
'revert', // 回滚
],
],
// subject 最大长度限制
'subject-max-length': [2, 'always', 72],
// body 每行最大长度
'body-max-line-length': [1, 'always', 100],
// 不允许 type 为空
'type-empty': [2, 'never'],
// 不允许 description 为空
'subject-empty': [2, 'never'],
// description 不以句号结尾
'subject-full-stop': [2, 'never', '.'],
// scope 可选,但如果有必须小写
'scope-case': [2, 'always', 'lower-case'],
},
};
配置 Husky 的 commit-msg hook:
# .husky/commit-msg
npx --no -- commitlint --edit ${1}
📌 **记住:**Husky v9+ 的 hook 文件就是纯 shell 脚本,不再需要
husky.shsource。直接写命令即可,第一行不需要#!/bin/sh。
2.2 Semantic Release:自动版本发布
安装配置:
npm install --save-dev semantic-release \
@semantic-release/changelog \
@semantic-release/git \
@semantic-release/github
创建 .releaserc.json:
{
"branches": ["main"],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/changelog",
{
"changelogFile": "CHANGELOG.md"
}
],
"@semantic-release/npm",
[
"@semantic-release/git",
{
"assets": ["CHANGELOG.md", "package.json"],
"message": "chore(release): ${nextRelease.version} [skip ci]"
}
],
"@semantic-release/github"
]
}
GitHub Actions 集成:
# .github/workflows/release.yml
name: Release
on:
push:
branches: [main]
permissions:
contents: write
issues: write
pull-requests: write
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # 必须获取完整历史,否则无法分析 commit
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npm test
- run: npx semantic-release
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
💡 提示:
fetch-depth: 0是关键——默认的 shallow clone 只有最新一个 commit,semantic-release 无法计算版本号差异。这是一个常见的 CI 配置遗漏。
2.3 对比:三种版本管理方案
| 方案 | 版本计算 | Changelog 生成 | npm 发布 | 学习成本 | 适用场景 |
|---|---|---|---|---|---|
| semantic-release | ✅ 全自动 | ✅ 自动生成 | ✅ 自动 | 中 | npm 包、开源库 |
| release-please | ✅ 全自动 | ✅ 自动生成 | 需配置 | 低 | Google 风格、Monorepo |
| standard-version(已停维) | ✅ 半自动 | ✅ 本地生成 | 手动 | 低 | 已不推荐使用 |
⚠️ 警告:
standard-version已于 2022 年停止维护,不再推荐使用。新项目请直接选择semantic-release或 Google 的release-please。
💡 三、常见踩坑与实战经验
3.1 踩坑一:Squash Merge 破坏了 Commit 分析
这是团队引入 Conventional Commits 后遇到的最高频问题。
如果你在 GitHub 上使用 Squash Merge,PR 的标题会被变成一个 commit message。如果 PR 标题不是 Conventional 格式,semantic-release 就会忽略这个 PR 的所有 commit。
❌ 错误的 PR 标题:
"修复登录页样式问题"
✅ 正确的 PR 标题:
"fix(auth): 修复登录页在移动端的布局溢出问题"
解决方案是在 CI 中校验 PR 标题:
# .github/workflows/pr-title.yml
name: PR Title Check
on:
pull_request:
types: [opened, edited, synchronize]
jobs:
check-title:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install --save-dev @commitlint/cli @commitlint/config-conventional
- name: Validate PR title
run: echo "${{ github.event.pull_request.title }}" | npx commitlint
📌 **记住:**如果团队使用 squash merge,PR 标题就是最终的 commit message。与其校验每个 commit,不如集中精力把 PR 标题管好。
3.2 踩坑二:Monorepo 的版本管理
在 monorepo 中,你通常不希望 packages/ui 的 fix 触发 packages/api 的版本更新。
使用 Lerna 或 Turborepo + Changesets 是更成熟的做法:
# 使用 Changesets 管理 monorepo 版本
npm install --save-dev @changesets/cli
npx changeset init
创建 changeset 的流程:
# 1. 开发完成后,运行 changeset 命令
npx changeset
# 2. 选择要更新的包和版本级别
# ? Which packages would you like to include?
# ◉ @myorg/ui (minor)
# ◯ @myorg/api (patch)
# 3. 写变更描述
# Added new Button component with variant support
# 4. 提交 .changeset/xxx.md 到 git
git add .changeset/
git commit -m "chore: add changeset for ui package"
Changesets 和 Conventional Commits 不冲突——你可以同时使用两者:
- Conventional Commits — 用于 commit 格式规范和自动 Changelog
- Changesets — 用于 monorepo 中的独立版本管理和发布
3.3 踩坑三:fix(deps) 污染 Changelog
Dependabot 或 Renovate 自动提交的依赖更新默认使用 fix(deps): 或 chore(deps): 前缀。如果用 fix,每次依赖升级都会触发一个 patch 版本发布,这在依赖频繁更新的项目中会导致版本号快速膨胀。
解决方案是在 .releaserc.json 中配置忽略规则:
// .releaserc.json 的 commit-analyzer 插件配置
[
"@semantic-release/commit-analyzer",
{
"preset": "angular",
"releaseRules": [
// 依赖更新不触发版本发布
{ "type": "fix", "scope": "deps", "release": false },
{ "type": "chore", "scope": "deps", "release": false },
// docs 和 style 类型的改动也不触发发布
{ "type": "docs", "release": false },
{ "type": "style", "release": false }
]
}
]
💡 **提示:**建议将 Renovate/Dependabot 的 commit 前缀配置为
chore(deps):而非fix(deps):,从源头避免误触发版本发布。
3.4 踩坑四:中文 commit message 的处理
很多国内团队习惯用中文写 commit message,这在 Conventional Commits 中完全没问题。但要注意两点:
第一,subject 长度限制。 中文字符的显示宽度是英文的两倍。72 字符的限制对中文来说偏长,建议调整为 36:
// commitlint.config.js 中调整
'subject-max-length': [2, 'always', 36],
第二,Changelog 的可读性。 如果你的项目面向国际社区,建议 commit message 使用英文;如果只面向团队内部,中文反而更高效。
一种折中方案是使用中文描述 + 英文 type:
✅ feat(auth): 支持微信扫码登录
✅ fix(api): 修复并发请求下订单重复创建的问题
❌ feat: add WeChat scan login support(如果团队全是中文用户)
✅ 四、最佳实践速查
以下是我根据多个团队落地经验总结的实践清单:
- ✅ 只在需要自动化的项目中引入 — 纯内部项目如果没有自动发版需求,简单的格式约定就够了
- ✅ PR 标题优先于 commit 校验 — 使用 squash merge 时,PR 标题才是最终 commit message
- ✅ 结合 Changesets 管理 Monorepo — Conventional Commits 管格式,Changesets 管版本
- ✅ CI 中同时校验 PR title 和 commit message — 双保险,避免合并后才发现格式错误
- ✅ 配置 commitlint 的
defaultIgnores— 允许 merge commit 和 revert commit 不遵循格式 - ❌ 不要在 merge commit 上卡 CI — merge commit 格式不统一是正常的,加
defaultIgnores: true - ❌ 不要用
fix(deps):作为依赖更新前缀 — 用chore(deps):避免触发无意义的版本发布 - ❌ 不要指望团队自觉遵守 — 没有 CI 校验的规范等于没有规范
- ⚠️ standard-version 已停维 — 新项目请用 semantic-release 或 release-please
- ⚠️ semantic-release 需要完整 git history — CI 中必须设置
fetch-depth: 0
🎯 总结
Conventional Commits 的价值不在于格式本身,而在于它为自动化工具链提供了一套结构化的信号。如果你的团队已经在用 semantic-release 做自动发版、用 commitlint 做格式校验、用自动 Changelog 生成变更日志——那这套规范的 ROI 非常高。
但如果你只是让团队「记住写 feat: 前缀」,却没有接入任何自动化工具,那确实是在浪费时间。
我的建议很简单:
⚡ **关键结论:**先搭好自动化流水线(semantic-release + commitlint + CI),再要求团队遵守格式。工具链先行,规范才有意义。
如果是一个全新项目,推荐的引入顺序是:
- 先配置 commitlint + husky(5 分钟搞定)
- 再配置 semantic-release 或 release-please(需要 CI 配置)
- 最后在 PR template 中注明 commit message 要求
- 逐步养成习惯,而不是一次性强推
commit 格式规范不是信仰,是工程手段。用对了,它就是你的自动版本管理引擎;用错了,它就是团队的格式警察。选择权在你手上。
相关工具推荐:
- 🔧 commitlint — Commit message 格式校验
- 🔧 semantic-release — 全自动版本发布
- 🔧 release-please — Google 出品的自动发版工具
- 🔧 Changesets — Monorepo 版本管理
- 🔧 Husky — Git hooks 管理
- 🔧 jsjson.com JSON 格式化工具 — 格式化你的配置文件