ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Conventional Commits 规范实战:从提交信息到自动化版本管理

2026/9/13 16:24:55 拓冰建站 浏览量
Conventional Commits 规范实战:从提交信息到自动化版本管理 写代码的人大概都经历过这种时刻git log 一拉出来满屏都是“update”“修改”“fix bug”“提交代码”根本分不清哪条提交是新功能哪条是修复了线上问题哪条只是改了文档。后来我强行把团队的提交规范换成了 Conventional Commits也就是约定式提交事情才变得清爽起来。Conventional Commits 的核心是一套前缀规范它规定每次 git commit 必须写成“类型 可选作用域 描述”的结构例如feat(login): add remember me。它解决的痛点很具体让提交信息可读、可检索能自动生成 CHANGELOG能驱动语义化版本号自动升级还能让 Code Review 更快进入状态。无论你是维护一个开源仓库还是和几十人一起协作这套前缀规范都值得认真落地。1. 先把规范讲清楚一次合规提交到底长什么样1.1 标准格式与三要素Conventional Commits 的完整格式是type[optional scope]: description [optional body] [optional footer(s)]type 是前缀表示本次改动的类型scope 是作用域一般写模块名、组件名或包名description 是对这次改动的简短描述。三者之间还有格式讲究冒号后面要跟空格description 尽量用祈使句、小写开头别超过 50 个字符。很多人以为“能跑就行”结果写成了feat:add logincommitlint 会直接报错因为冒号后少了一个空格。这类细节看起来吹毛求疵但恰恰是它保证了提交信息可以被稳定解析。一个完整的合规提交一般长这样feat(api): add user login endpoint Implement JWT-based authentication flow. Closes #123 BREAKING CHANGE: the login endpoint now requires a token instead of a session.第一行是标题行必须存在空行之后是 body说明为什么改、怎么改再往下是 footer用来关联 issue 或者标注破坏性变更。修复缺陷也有固定写法比如fix(parser): handle empty input。判断一次提交是否合规先看标题行的前缀再看描述是否说得清改动目的而不是看文件堆了多少。1.2 为什么固定格式能撑起自动化工具链很多人觉得“提交格式嘛能看懂就行”但在真实工程里这一点并不可靠。如果把提交记录比作快递面单收件人、电话、地址都有固定位置快递员才能批量扫描分拣提交信息同理只有把“类型、影响范围、描述”放到固定位置工具才能自动识别。语义化版本号、CHANGELOG、git bisect 这些能力全部依赖对提交信息的结构化解析。你写的是给机器看的协议而不只是给自己看的备忘。没有固定前缀时团队里每个人都在发明自己的“方言”有人写 fix有人写 fixed有人直接写“修改了登录问题”。一旦提交数量过千想统计“这个版本到底加了哪些功能”只能靠人工瞪眼。而遵循 Conventional Commits 之后一句git log --oneline --grep^feat就能把功能提交全筛出来。更关键的是工具链能从这些提交里自动判断版本号是加 patch 还是 minor这件事靠人脑判断很容易出错。1.3 Breaking Change 和版本号的联动在 Conventional Commits 里footer 中的 BREAKING CHANGE 是另一个核心。只要提交里出现它就代表这次改动不向后兼容需要提升主版本号。注意BREAKING CHANGE 必须写在 footer 且全大写例如feat(api)!: remove deprecated endpoints或者在 footer 里写BREAKING CHANGE: remove login v1 API两者等价。借助这个约定工具才知道这个 feat 不是普通的 minor 升级而是 major。语义化版本号的规则是fix 默认升 patchfeat 默认升 minorBREAKING CHANGE 默认升 major。如果你的项目还没有自动发版至少心里要清楚不带前缀的提交其实无从判断版本影响这也是这个规范能直接带来收益的地方。2. 常用前缀逐一说透feat、fix、docs、chore 该怎么选2.1 最常用的四个前缀feat、fix、docs、refactorfeat 和 fix 是出现频率最高的两个前缀。feat 代表新功能比如feat(login): add remember mefix 代表修复缺陷比如fix(login): resolve token expiry issue。这里有一个团队里反复出现的争议一个改动既有 bug 修复又调整了接口到底算 fix 还是 feat我的经验是如果对外行为发生了新增就写 feat如果只是让行为恢复到预期就写 fix。主次不分明时按对使用者的影响来定不要两个都写。docs 指文档变更包括 README、注释、接口文档例如docs(readme): update install instructions。refactor 指不改变外部行为、只调整内部结构的重构例如refactor(utils): extract formatDate helper。它和 fix 的边界在于重构不修 bug只改代码组织方式。如果你重构过程中顺手修了一个 bug要么拆成两次提交要么按更重要的影响来定类型。重构提交里混入 bug fix是最容易被 review 出来然后要求拆开的问题。2.2 容易被轻视的工程前缀build、ci、perf、test这四个前缀在业务代码里不常见但工程体系里非常有用。build 表示构建系统或外部依赖的变更比如build(deps): bump lodash to 4.17.21ci 表示持续集成配置的变更比如ci(github): add npm cache to workflow。它俩容易混记住一个判断标准build 影响最终产物ci 影响自动化流程。如果改了构建脚本那是 build如果改了 GitHub Actions 文件那是 ci。test 是测试相关变更比如test(utils): add edge cases for formatDate。perf 是性能优化比如perf(table): virtualize row rendering to reduce first paint time。需要留意的是perf 这个词虽然常见但在默认的 standard-version 规则里不会触发版本号变化。如果一次性能优化同时修复了吞吐量异常的问题很多团队会直接归到 fix因为它对使用者的价值是“坏掉的性能被修好了”。重要的不是教条而是前缀和内容对得上。2.3 style、chore、revert 的特殊性style 只用于代码格式调整例如补分号、调整缩进、重排 import不改变任何运行逻辑。很多新手会把样式修改写成 fixcommitlint 不拦但 Code Review 时会被追问“你修了啥 bug”。chore 是杂项通常用来兜底更新依赖、修改目录结构、格式化整个项目。问题是它很容易变成“垃圾桶”什么说不清的改动都往 chore 里扔。我建议团队约定 chore 的使用范围依赖升级、构建配置、工具链配置最多再算上项目基础文件变更不包含业务逻辑。业务逻辑哪怕再小也要用 feat、fix 或 refactor。revert 的格式比较特殊规范要求携带被回滚的那次提交信息例如revert: feat(api): add user login endpoint This reverts commit abcd1234.这样不仅方便看回滚原因也能让工具知道这次 revert 到底影响的是哪个功能点。如果你 revert 的是一个带 BREAKING CHANGE 的提交最好也在 footer 里补上说明否则版本号判断容易失真。2.4 scope 该什么时候写怎么写scope 是可选字段写在 type 后的小括号里比如feat(api):中的 api。我团队里的约定是scope 一律使用模块或包名不大写、不写路径、不用“should fix”这种动宾短语。一个过大的 scope比如feat(all):等于什么都没说一个过小的 scope比如fix(src/components/Button/index.tsx):又会让标题长到失去可读性。建议默认用小写短横线命名比如feat(user-profile):。scope 的真正价值在于快速过滤git log --grepfeat(user-profile)就能拉出某个模块的全部功能提交这在多包仓库里尤其有用。3. 把前缀焊死在提交里commitlint husky 自动拦截3.1 为什么必须用工具而不是靠人自觉没有自动化的规范等于没有规范。每个人都会在赶进度时写出“update”“fix”这种信息review 的人也不会逐条盯提交格式。所以落地 Conventional Commits 的第一步不是开会让全组背前缀表而是在本地和 CI 分别上一道闸。本地靠 git hooksCI 靠校验脚本两条腿走路。Git hooks 是 Git 在执行特定动作时触发的脚本commit-msg 这个 hook 会在提交信息写入后触发我们可以在里面调用 commitlint 做校验。husky 是管理 hooks 最方便的工具装好后可以把它变成一个工程化配置而不是让每个成员手动拷贝脚本。这里只有一条原则提交不符合规范就直接拒绝不给人讨价还价的机会。3.2 一套能直接用的配置husky commitlint以 npm 为例安装依赖并使用 husky 初始化npm install -D husky commitlint/cli commitlint/config-conventional npx husky initnpx husky init会在项目根目录生成.husky目录并创建一个pre-commithook 文件。接着创建 commitlint 配置文件并启用规则集// commitlint.config.js module.exports { extends: [commitlint/config-conventional] };然后手动添加.husky/commit-msg文件里面写npx --no -- commitlint --edit $1此时再运行git commit -m 随便写commitlint 会立刻拦截并告诉你缺少合法的 type。如果你还想限制 scope 的大小写、描述的最大长度可以在 rules 里加规则例如module.exports { extends: [commitlint/config-conventional], rules: { scope-case: [2, always, lower-case], header-max-length: [2, always, 100] } };规则数组里的 2 表示 error 级别填 0 表示关闭该规则。生产环境我一般还会打开subject-full-stop禁止 description 末尾加句号减少噪音。3.3 实测被 hook 拦下之后怎么处理假设你执行git commit -m 更新登录逻辑终端会输出类似✖ subject may not be empty的错误提交被中断。这时你有两个选择老老实实改成合规格式比如git commit -m fix(login): correct redirect after login然后重新提交。另一个选择是使用--no-verify跳过 hook。我从不建议团队成员滥用这个参数但必须承认它存在。跳过本地 hook 的提交一旦 push 到 CICI 里的校验还会再拦一次。如果连 CI 都跳过那讨论规范就没有意义了。4. 更省事的玩法commitizen 交互式填写与自动生成 CHANGELOG4.1 用 commitizen 把提交变成选择题commitlint 解决的是“不符合就拒绝”但很多人不是不想写规范而是记不住前缀和格式。这时候可以让 commitizen 上场。安装npm install -D commitizen cz-conventional-changelog配置 package.json{ scripts: { commit: git-cz }, config: { commitizen: { path: cz-conventional-changelog } } }之后运行npm run commit它会用交互式问题引导你选择 type、填写 scope、描述、body最后生成合规提交信息。相当于把提交从“默写题”变成了“选择题”。需要注意的是commitizen 只负责生成信息不负责校验所以 commitlint 仍然要保留。两者是互补关系commitizen 提升体验commitlint 守住底线。4.2 standard-version从提交记录到版本号和 CHANGELOG如果团队能持续输出合规提交下一步就可以让工具从这些提交中自动生成版本号和 CHANGELOG。standard-version 是其中比较轻量的一种方案它本地执行不需要长期跑在 CI 上。安装并配置脚本npm install -D standard-versionpackage.json 里加{ scripts: { release: standard-version } }运行时standard-version 会读取git log中符合 Conventional Commits 的提交判断该升 patch、minor 还是 major自动修改 package.json 的 version、生成 CHANGELOG.md、打上 git tag。默认规则是fix 对应 patchfeat 对应 minorBREAKING CHANGE 对应 major。如果某次发布想手动指定版本可以跑npx standard-version --release-as minor如果你想给 CHANGELOG 增加一组自定义前缀的归档可以在.versionrc里配置types但大部分团队不需要走这么深。工具的意义在于把重复劳动交给机器人只需要保证提交前缀是对的。4.3 在 CI 里再上一道保险本地 hook 能被绕过所以 CI 必须做兜底。最省事的做法是在 GitHub Actions 里直接使用现成的 commitlint actionname: Commit Convention Check on: [pull_request] jobs: commitlint: runs-on: ubuntu-latest steps: - uses: actions/checkoutv4 with: fetch-depth: 0 - uses: actions/setup-nodev4 with: node-version: 20 - run: npm ci - run: npx commitlint --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}这个 workflow 的作用是每次 PR 更新就检查 PR 里所有提交是否满足规范。如果你用的是 squash merge也可以在合并时把 PR 标题改成规范格式这样合并后的提交历史会非常干净。这里不需要让每一个 commit 都规规矩矩反而建议在 PR 汇总层面控制避免 rebase 多的分支被误杀。5. 老仓库改造与踩坑排查实录5.1 历史提交信息一团糟要不要“翻旧账”很多团队是半路引入 Conventional Commitsgit log 前面一大段都是“各种 update”。我的建议很明确不要重写历史。重写公共分支历史是一件破坏性极强的操作所有同事的本地分支都会受到影响。更务实的做法是“从现在开始”commitlint 只拦截新提交CI 只检查新 PR旧信息保持原样。等未来某个大版本重构时用 squash merge 把整个功能分支压成一条规范提交历史自然就变干净了。如果实在强迫症发作想整理个人仓库的本地历史可以用git rebase -i HEAD~n逐条修改但 force push 前务必和所有协作者确认。5.2 提交信息写错了怎么办这是出现频率最高的场景。如果提交还没有 push改起来很简单git commit --amend -m fix(login): resolve token expiry issue如果要改最近几条提交用git rebase -i HEAD~3把对应 commit 行的pick改成reword或edit保存后逐个修改。如果提交已经 push 到远程一律先走团队沟通再使用 force-with-lease 推送git push --force-with-lease不要在多人共享的分支上随意改写历史。我见过最稳的团队策略是主干分支禁止 force push所有提交都通过 PR squash merge 进入普通开发分支写错了直接用reset --hard后重推废掉旧提交即可。5.3 前缀选择困难症一张速查表用了半年以后我发现大多数纠结其实靠一张表就能解决。type适用场景典型示例是否影响版本feat新功能feat(login): add remember meminorfix修复缺陷fix(login): resolve token expirypatchdocs文档变更docs(readme): update install guide否style格式调整style(button): add missing semicolon否refactor重构且不改行为refactor(utils): extract formatDate否perf性能优化perf(table): reduce first paint time否test测试变更test(utils): add edge cases否build构建/依赖build(deps): bump lodash否ciCI 配置ci(github): use node 20否chore杂项chore: add .gitignore否revert回滚revert: feat(login): add remember me按回滚对象如果你的团队认为 perf 修了重要体验问题可以约定 perf 也触发 patch但需要在工具里额外配置。不是所有版本影响都只能依赖默认规则重点是团队内部要一致。5.4 我们踩过的一些坑第一坑把 BREAKING CHANGE 写成了 footer 里的普通说明工具没识别版本号跳错。记住必须全大写且放在BREAKING CHANGE:前缀后面。第二坑scope 用了路径名结果git log --grepuser-profile经常漏匹配。第三坑chore 滥用最后 CHANGELOG 里只有 feat 和 fix其他分类全是 chore信息密度骤降。第四坑团队里有人用feat: fix something这种描述完全违背类型意图review 时只能靠人眼抓。这些坑没有一个能靠工具全自动解决只能在规范文档里写清楚“什么情况用什么前缀”并在 PR 模板里加一个“本次改动类型”的选项逼着作者先做分类。最后再分享一个小技巧。我在终端里配置了一个 git alias把常用前缀和示例放在一起git config --global alias.types !git log --oneline --grep^feat --grep^fix --grep^docs --oneline -20这样想看最近的核心改动时一条命令就够了。踩过几次坑之后我的体会是Conventional Commits 不是银弹它更像一套团队共同遵守的暗号。哪怕你不在团队里自己维护一个小项目也建议从今天起用 feat、fix、docs 这几个前缀因为半年后再回来看 git log你会感谢当时把前缀写清楚的自己。这套规范真正的价值不在前缀本身而在它让提交记录从“人能看懂”进化成了“机器和人都能看懂”。