要我说,能把 Git 提交这件事从一个"个人随手记"变成"团队可追溯、机器可解读"的工程化能力,靠的不是给每个人发一份《提交规范文档》,而是把规范嵌进工具链里,让不规范的提交根本过不去。这个项目标题看起来就是一份配置文档的事,实际上它串起了一整条"Conventional Commits 规范 + commitlint 校验 + husky 钩子 + Standard Version 自动发版"的工具链。配置完这套东西,你会得到一个非常爽的效果:git commit的时候有交互式问答帮你写规范提交信息,写错了直接被拦截,等到要发版的时候跑一条命令,版本号自动升、CHANGELOG.md 自动生成、Git tag 自动打上,全程不需要手动改动任何文本文件。这篇就把整个配置过程、工具选型逻辑、以及我实际踩过的坑完整写出来,给正在做团队规范建设、或者想给自己项目配一套自动化发版流程的朋友一个可直接抄作业的参考。1. 整体设计:一套工具链解决"提交信息乱+版本靠手记"1.1 为什么这题值得写:Changelog 不是写出来的,是约定出来的先解决一个最核心的问题:为什么要做这件事?业界有句不太好听的大实话——文档没人看,但工具会管人。如果你的团队依然处于"提交信息随便写、发版手动改版本号、CHANGELOG 靠人肉回想"的阶段,你要面对的痛点非常具体:翻 Git 历史看某个改动的时候,看到的是一堆"update"、"fix bug"、"改一下"这样无意义的描述,完全无法定位真实的改动范围。发版之前需要人工翻 commit 记录,逐个比对哪些改动需要同步到交付文档里,漏一个变更、错标一个破坏性更新,线上事故就是这么埋下的。版本号靠改package.json里的字段来维护,今天改成 1.1.0,明天上线又手动升到 1.2.0,但 Git tag 和 CHANGELOG 不同步,发行记录和代码状态完全脱节。这套方案的核心思路,就是让规范前置到提交的那一刻。以 Conventional Commits 规范作为唯一数据源,所有提交信息、版本号、CHANGELOG 都由同一个结构驱动生成,这样流程就变成了一条自动化的流水线,而不是靠岗位责任心驱动的人工记账。1.2 方案选型对比:为什么不推荐 semantic-release在工具选型上,目前主流的自动化发版方案主要就是两套:semantic-release 和 standard-version。我最终选的是 standard-version,对比逻辑如下:维度semantic-releasestandard-version运行环境强制绑定 CI/CD 流水线本地命令即可运行,也可以进流水线自动推送自动 push commit、tag、npm publish默认不推送,需要手动 push 或配置配置复杂度需要配置 CI 平台,学习成本高一个命令 + 一份可选的配置文件反向操作出错后需要 reset 版本状态支持 --release-as 显式指定版本,好控制适用场景开源项目、重 CI 流程、多人高频发版内网项目、中小团队、接入 CI 但不想被绑定说直白一点:semantic-release 的"自动化"是激进的,它把整个发布流程接管了,适合完全信任流水线的团队。但很多项目实际没那么高频发版,团队也需要"看得见、可控制"的发布过程。standard-version 走的是"半自动"路线——版本号计算、CHANGELOG 生成、打 tag 都自动化,但推送和发布保留人工触发,出问题的时候你有足够的时间和空间去干预。我自己的标准是:能让机器算的绝不让人手动算,但留一个确认按钮比全自动更稳。1.3 工具链全景图与工作流整套方案的参与者有三个核心角色,每个角色只管一件事:commitizen:解决"怎么写"。提供交互式问答界面,引导开发者按规范格式生成提交信息,相当于给提交这件事套了个填写表单。commitlint + husky:解决"必须这么写"。husky 负责在 commit-msg 阶段拦截提交,commitlint 负责校验提交信息是否符合规范,不符合直接拒绝提交。standard-version:解决"怎么用这些信息"。自动分析提交历史里的 feat/fix/BREAKING CHANGE 等类型,计算出下一个版本号,更新版本文件,生成 CHANGELOG.md,并打上对应的 Git tag。整体工作流是:开发者执行git commit→ 触发 commitizen 交互问答 → 生成规范提交信息 → husky 触发 commitlint 校验 → 通过后完成提交。需要发版时,执行npm run release→ standard-version 分析历史提交 → 自动完成版本号更新 + CHANGELOG 生成 + tag 标记。这套设计最巧妙的地方在于:它把"规范"从人的记忆里搬到了工具的规则里。开发者不需要背任何规范,也不需要在提交前回忆"fix 和 feat 的区别是什么",commitizen 的问答会告诉你每一步要填什么;commitlint 则负责兜底,防止有人绕过交互式工具直接写了个无效提交。2. 核心细节解析:Conventional Commits + 三件套配置里容易被忽略的坑2.1 Conventional Commits 规范里的"隐性规则"配置之前,先把规范本身吃透。它规定提交信息必须符合这样的结构:<type>[optional scope]: <description> [optional body] [optional footer(s)]这份规范表面看起来就五行字,但在实际配置的时候有几条"隐性规则"特别容易踩:第一个坑是 type 的作用域。feat 和 fix 是目前所有工具默认识别的两个核心类型,直接影响版本号的自动变更。feat 对应下一个 minor 版本(比如当前 1.0.0,下一个是 1.1.0),fix 对应下一个 patch 版本(比如当前 1.0.0,下一个是 1.0.1)。但要注意,chore、docs、style、refactor 这类类型默认情况下不会触发版本号升级,如果你的团队习惯把所有改动都写成 chore,那这个版本号就会一直"跳不动"。所以配置 type 的枚举范围时,要结合团队实际的工作习惯来定,而不是把规范文档里的推荐列表原样抄过来。第二个坑是 BREAKING CHANGE 的写法。在 commit 的 message 里明确写BREAKING CHANGE:,或者 footer 里带!标记,standard-version 才会把这次变更识别为 major 版本升级。很多团队第一次用的时候,破坏了 API 但没在提交信息里写这个标记,版本号就只升了个 patch,等到外部依赖方禅了车才回来翻历史。这个要专门写进团队的规范里:涉及破坏性变更,提交信息必须显式声明。第三个坑是 scope 的使用边界。小团队建议不写 scope,因为大部分提交的粒度就一个模块,写 scope 反而增加了问答步骤的负担。但如果项目是多个业务线并存,scope 会很有价值,比如feat(user-center): 新增用户注册流程。决策逻辑是:只有当你觉得"光看 type 不够定位模块归属"的时候才启用 scope。2.2 commitlint 配置:rule 的优先级与误判commitlint 的核心是配置规则,我这边实际用的配置框架如下:// commitlint.config.js module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert']], 'subject-case': [0], 'header-max-length': [2, 'always', 100] } };head-max-length设成 100 这个值我记得是从 console 到 Git 平台普遍支持的行宽来的。subject-case我直接关掉了,因为团队里有人习惯英文首字母大写,有人习惯小写,这个不强制反而减少摩擦。配置上有两个容易误判的细节:rules 的三要素数组[level, applicable, value]里,level=2 是 error 级别,会导致提交直接失败;level=1 是 warning,不阻断提交。千万别全改成 1,否则 commitlint 就变成"建议你改改,但你不改我也没办法",失去了强制意义。extends 继承的配置是会被覆盖的。type-enum在 relocated 配置后,就必须显式包含你需要的全部 type,否则 toolkit 默认配置里的perf、revert等就被"覆盖"掉了,容易出现"别的机器上能提交,你这台机器上说 type 不合法"的灵异现象。2.3 husky 新老版本差异:别再复制老教程了husky 在 6.0 之后有过一次大改版,很多 2020 年之前的技术文章用的配置方式已经失效了。我一开始接手项目的时候就踩过这个版本坑。老版本(5.x 及之前)的做法是:# 内部自动在 package.json 里写入依赖 npx husky install新版本(6.x+,目前是 9.x)的正确做法是:# 1. 在 package.json 的 scripts 里加 prepare 钩子 { "scripts": { "prepare": "husky" } } # 2. 创建具体的钩子文件 npx husky add .husky/commit-msg # 这个命令会在 .husky 目录下生成一个 commit-msg 文件关键区别在于:新版 husky 不再依赖 package.json 内的依赖注入,而是把 Git hooks 的实际执行位置放在项目根目录的.husky/目录里。这个目录要提交到 Git 仓库里,所有克隆下来的开发人员会自动继承这套 hook 配置。commit-msg 钩子文件里的内容长这样:npx --no -- commitlint --edit $1注意这里用了npx --no,它的作用是防止 npx 在找不到包时去远程下载,确保每次执行的都是项目本地安装的 commitlint 版本,避免"这台机器上校验通过、那台机器上校验不过"的分裂问题。2.4 standard-version 的版本号计算逻辑standard-version 的版本计算规则,说白了就是一套"自动语义化版本(SemVer)"逻辑。理解它的判断顺序很关键:首先看package.json里的version字段,作为当前版本基线。然后扫描git log里从上一次 version tag 到当前 HEAD的所有提交。只要发现某个 commit 包含BREAKING CHANGE标记(或者 type 为带!的 feat),下一个版本号的主版本 +1。否则如果有feat类型提交,次版本 +1。否则如果有fix类型提交,补丁版本 +1。如果以上都没有,报错提示:没有发现需要升级的内容,或者不产生任何版本变化。这个逻辑说明了两件事:第一,第一次发布之前的历史 commit 不参与计算,所以首次发版要用--first-release参数单独处理;第二,你想让版本号升得准确,前提就是提交信息必须规范,这两者的关系是强绑定的。3. 实操过程与核心环节实现:从零配置一份可直接落地的仓库3.1 初始化环境与安装依赖首先要把本地环境准备好,要求 Node.js 版本 >= 18(我用的 20 LTS),Git 版本 >= 2.30,然后在一个干净的目录里初始化:mkdir my-project cd my-project npm init -y接着安装核心依赖:# 安装 commitizen 相关依赖 npm install --save-dev commitizen @commitlint/cli @commitlint/config-conventional # 安装 husky npm install --save-dev husky # 安装 standard-version npm install --save-dev standard-version这里有一个选型细节:很多教程会建议把commitizen装成全局包,我不推荐。原因很简单——团队里每个人都在自己机器上全局装一遍,版本不一致,交互界面和配置行为都可能不同。项目级安装,配合npx使用,所有成员得到的体验完全一致。3.2 commitizen 交互式提交初始化安装完依赖后,需要把 commitizen 和适配器绑定起来。我用的是cz-conventional-changelog适配器:npx commitizen init cz-conventional-changelog --save-dev --save-exact这个命令会在package.json里写入一段 config 配置,内容如下:{ "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } } }然后在 scripts 里加两个命令:{ "scripts": { "commit": "cz", "release": "standard-version", "release:minor": "standard-version --release-as minor", "release:patch": "standard-version --release-as patch" } }这里解释一下为什么有release:minor和release:patch这两个"手动覆盖"的命令。standard-version 默认是自动分析提交历史来决定版本号的,但实际发版的时候会有一些特殊情况,比如这次上线只是修了一个小 bug,却希望版本号走到 minor 来对齐外部依赖方某些策略;或者团队在某个 release 周期里主动规划了一个大版本。有手动控制能力会让流程更灵活,不会在自动化规则面前无路可退。3.3 commitlint + husky 配置核心步骤先配置 husky:# 初始化 husky npx husky init # 添加 commit-msg 钩子文件 npx husky add .husky/commit-msg执行完之后,.husky/目录下应该有这些文件:.husky/ ├── _ │ ├── .gitignore │ ├── husky.sh │ └── pre-commit └── commit-msg_目录是 husky 内部框架,不要手动删。commit-msg文件里的内容就是提交信息校验的触发点,我们要把npx --no -- commitlint --edit $1这行写进去,整体内容如下:npx --no -- commitlint --edit $1然后验证一下 commitlint 配置。在项目根目录创建配置文件(我们已经在上面写出了示例,直接保存为commitlint.config.js):前面那段 commitlint 配置里,我特意把header-max-length保留在 100,没有用网上抄的 72。因为系统内部有大量带长 scope 的模块名,72 的宽度确实拦下来过几条合法的提交信息,改到 100 之后几乎没有误报了。这个数值取决于你们的项目实际情况,我的建议是:先设 100,如果团队提交 header 经常超长,再压下来也不迟。3.4 standard-version 配置与发布 releasestandard-version 的配置文件叫.versionrc.js(也可以叫.versionrc.json),我这边实际用的配置如下:module.exports = { // 参与版本号变更计算的头部信息类型 types: [ { type: 'feat', section: 'Features' }, { type: 'fix', section: 'Bug Fixes' }, { type: 'chore', section: 'Others' }, { type: 'docs', section: 'Others' }, { type: 'style', section: 'Others' }, { type: 'refactor', section: 'Others' }, { type: 'perf', section: 'Others' }, { type: 'test', section: 'Others' }, { type: 'build', section: 'Others' }, { type: 'ci', section: 'Others' }, { type: 'revert', section: 'Others' } ], // 指定参与版本变更的提交类型,只计算 feat/fix/revert preset: 'conventionalcommits', // 跳过自动打 tag skip: { tag: false }, // 不修改 package-lock.json 等其他文件 packageFiles: [], // 更新版本号的文件列表 bumpFiles: ['package.json', 'package-lock.json', 'README.md'] };这里有几个容易绕进去的配置点:types 里的 section 决定每一类 commit 被归并到 CHANGELOG 的哪个小标题下。让chore、docs、refactor都归到 Others 里,CHANGELOG 就会很整洁,不会出现一个 change log 里堆了十几个小分类的混乱感。packageFiles 和 bumpFiles 的区分:packageFiles是你扫描版本号所依据的文件列表(一般就是 package.json),bumpFiles是版本号更新后要同步修改内容的文件。默认情况下 package.json 会自动处理,如果你有用到 README.md 里写版本号徽章这种习惯,把 README.md 加进bumpFiles即可。首次发布版本的时候,因为仓库里还没有任何 version tag,standard-version 没法从历史中找到基线,所以要用首次发布模式:npm run release -- --first-release这个命令会做三件事:把 package.json 的版本号从 1.0.0 升到 1.0.0-1(如果原来没有版本号则用 1.0.0);生成 CHANGELOG.md;打上对应的 tag(但不会 push 到远端)。后续的正常发版流程就是:npm run release它会自动执行"分析提交 → 计算版本号 → 更新 package.json → 生成 CHANGELOG.md → 自动打 tag"这一整套动作。3.5 验证:一次完整的 release 流程为了让你直观感受整个流程,我模拟一遍完整的实际操作过程。执行交互式提交:npm run commitcommitizen 的交互界面上会依次出现这些问答:? 请选择您这次提交的变化类型: (Use arrow keys) feat: 增加新功能 fix: 修复 Bug ❯ chore: 对构建过程或辅助工具的更改 ? 请选择影响范围(可选,直接回车跳过): user-center ? 请输入简短的提交描述(必填): 新增用户中心注册逻辑 ? 请提供更详细的变更描述(可选):提交完成后,如果信息合法,commitlint 直接放行。如果我在问答里故意选了一个feat:类型但描述写了一串超过 100 字符的长文本,commit-msg 钩子就会拦截:> npx --no -- commitlint --edit .git/COMMIT_EDITMSG ⧗ input: feat(user-center): 新增用户中心注册逻辑,同时调整了用户协议页面的展示顺序和文案内容,并且重新组织了用户中心的子路由配置结构 ✖ subject may not be longer than 100 characters [header-max-length]紧接着发版流程:npm run release最终产出的 CHANGELOG.md 结构是这样的:# Changelog ## [1.1.0] - 2025-01-15 ### Features - **user-center:** 新增用户中心注册逻辑 ### Others - **deps:** 更新某依赖版本整个流程一气呵成,不需要人再去手动编辑任何文件。4. 常见问题与排查技巧实录4.1 husky 侧:钩子文件不触发现象:commit 的时候 commit-msg 钩子完全不执行,直接提交成功。首先检查.husky/目录是否存在于 Git 仓库里。如果你是在 husky init 之前就已经git init了,钩子目录是可以被识别到的;但如果是克隆仓库,需要确认.husky目录被提交上去了,否则 hook 根本不存在。排查命令如下:# 查看当前 hook 的真实状态 git config core.hooksPath # 正确输出应该指向 .husky/_如果输出结果是空或者指向.git/hooks,说明 husky init 没有生效,重新执行:npx husky init另一个典型坑:Windows 环境下的 shell 路径。如果钩子文件第一行写的#!/bin/sh,在 Windows 上可能因为换行符(LF/CRLF)问题报错。把.husky/目录下的文件都设为 LF 换行,或者团队统一要求 Git 配置core.autocrlf input,都可解决。具体操作是在项目根目录建超一个.gitattributes,加入*.sh text eol=lf。4.2 commitlint 侧:类型名不合法现象:跑 commit 的时候,明明提交信息看起来没问题,但 commitlint 报type must be one of [...]。这种情况几乎都是配置文件里的type-enum里没有包含你正在使用的那个 type。比如你写了feat, 配置里也是feat,还报错,那就去看 extends 是不是把规则覆盖了。最保险的验证方式是直接跑命令测试一条提交信息:echo "feat: 测试" | npx commitlint如果这个配置是合法的,任何报错都说明配置文件本身有问题。用这个方式做回归测试非常高效,不用反复触发真实提交。另外要记得:commitlint 的配置文件修改之后需要重启任何本地维持 watch 状态的 Node 进程,如果你们在跑某种热重载的本地服务,可能会有旧配置残留。4.3 standard-version 侧:版本号不升或升多了现象一:跑 standard-version 之后,版本号没有变化,也没有 CHANGELOG 更新。这通常意味着提交历史里没有任何 feat/fix 类型,或者全部是 chore 类型。解决方式是先审视一遍提交记录,看看是否真的没有功能变动。如果是"首次发布",记得加--first-release。现象二:改了一个很小的修复,版本号直接从 1.0.0 跳到 2.0.0。这是最昂贵的教训——肯定是某个提交信息里写了BREAKING CHANGE标记。排查方法:# 查看自上次发版以来所有包含 BREAKING 的提交 git log <old-tag>..HEAD --grep="BREAKING" --oneline如果审阅后确认这次变更并不应该升 major,可以直接用手动指定版本号的方式重跑:npm run release:patchstandard-version 会基于当前版本直接走 patch 逻辑,覆盖刚才自动计算出来的 major 版本。要提醒的是,这种操作方式属于救火场景,不做为常态。提交规范的意义就在于省的每次发布前还要手动去审版本号,能不走手动覆盖就不走。4.4 老项目回填与团队成员迁移如果你的仓库里已经有大量不规范的提交历史,把这套配置接进去的时候不要指望历史提交被自动规范化,不可能,也没必要。standard-version 只从最近一次 version tag 之后计算变更,所以老历史的"脏"不会影响新流程的正确性。但对团队里已经习惯"自由提交"的成员来说,刚上这套工具确实会有一段阵痛期。我给团队执行过一套迁移策略,效果不错:先用两周时间并行跑,commit-msg 规则设为 warning(level=1)级别,只提醒不拦截,让大家适应交互式问答。两周后再改成 error(level=2)级别,正式强制。同步做好一次培训,重点讲清楚"提交类型不是提交文案装饰,它直接决定版本号和 CHANGELOG",让成员理解为什么自己提交的时候会卡住。这一步做与不做,区别是很大的。像我之前带的项目,因为 skipped 了培训直接上了硬规范,结果团队连续三天提交都被拦,当天就把 commitlint 给临时屏蔽了,最后整个规范形同虚设。后来我改成"先软后硬"的节奏,两周内大家自然就把符合规范的提交信息背诵下来了。5. 我实际用下来的几点心得这套配置在几个项目里跑了也有一段时间了,说几个实际感受:第一,提交信息的价值密度是整个工具链里最高的。版本号可以随时靠手动释放,CHANGELOG 可以外包给文档工具,但提交信息是唯一人脑到工具都不需要二次翻译的数据。把规范嵌进 commitizen 问答里,等于把团队交底成本压到了最低。第二,standard-version 比起完全自动化的 release 工具,更适合中国团队的协作习惯。我们很多项目还要接定制化发布流程,比如发 CDN、同步内网仓库、触发外部构建,半自动的发布方式可以保证自动化计算和人工确认之间的平衡,不会出现一个 git push 就把生产包发出去的"惊喜"。第三,不要把这套配置当成"写完就结束"的一次性任务。工具链本身是静态的,但团队的提交习惯会逐渐演变。比如一开始 type-enum 只列了少量类型,等团队开始做性能优化专项,就需要把perf加进来。types配置也是,CHANGELOG 生成之后如果发现归类不合理,随时调整.versionrc.js里的 section 即可。如果你正准备给项目配这套流程,我的建议是:不用想着一口气把全链路全部接好,先把commitlint + husky搭起来,强制提交规范,跑两周之后再把 standard-version 接上,你会明显感受到"提交历史即文档"这件事带来的效率提升,那种翻代码不再靠"git log --oneline"人肉搜的感觉,真的挺值的。
要我说,能把 Git 提交这件事从一个"个人随手记"变成"团队可追溯、机器可解读"的工程化能力,靠的不是给每个人发一份《提交规范文档》,而是把规范嵌进工具链里,让不规范的提交根本过不去。这个项目标题看起来就是一份配置文档的事,实际上它串起了一整条"Conventional Commits 规范 + commitlint 校验 + husky 钩子 + Standard Version 自动发版"的工具链。配置完这套东西,你会得到一个非常爽的效果:git commit的时候有交互式问答帮你写规范提交信息,写错了直接被拦截,等到要发版的时候跑一条命令,版本号自动升、CHANGELOG.md 自动生成、Git tag 自动打上,全程不需要手动改动任何文本文件。这篇就把整个配置过程、工具选型逻辑、以及我实际踩过的坑完整写出来,给正在做团队规范建设、或者想给自己项目配一套自动化发版流程的朋友一个可直接抄作业的参考。1. 整体设计:一套工具链解决"提交信息乱+版本靠手记"1.1 为什么这题值得写:Changelog 不是写出来的,是约定出来的先解决一个最核心的问题:为什么要做这件事?业界有句不太好听的大实话——文档没人看,但工具会管人。如果你的团队依然处于"提交信息随便写、发版手动改版本号、CHANGELOG 靠人肉回想"的阶段,你要面对的痛点非常具体:翻 Git 历史看某个改动的时候,看到的是一堆"update"、"fix bug"、"改一下"这样无意义的描述,完全无法定位真实的改动范围。发版之前需要人工翻 commit 记录,逐个比对哪些改动需要同步到交付文档里,漏一个变更、错标一个破坏性更新,线上事故就是这么埋下的。版本号靠改package.json里的字段来维护,今天改成 1.1.0,明天上线又手动升到 1.2.0,但 Git tag 和 CHANGELOG 不同步,发行记录和代码状态完全脱节。这套方案的核心思路,就是让规范前置到提交的那一刻。以 Conventional Commits 规范作为唯一数据源,所有提交信息、版本号、CHANGELOG 都由同一个结构驱动生成,这样流程就变成了一条自动化的流水线,而不是靠岗位责任心驱动的人工记账。1.2 方案选型对比:为什么不推荐 semantic-release在工具选型上,目前主流的自动化发版方案主要就是两套:semantic-release 和 standard-version。我最终选的是 standard-version,对比逻辑如下:维度semantic-releasestandard-version运行环境强制绑定 CI/CD 流水线本地命令即可运行,也可以进流水线自动推送自动 push commit、tag、npm publish默认不推送,需要手动 push 或配置配置复杂度需要配置 CI 平台,学习成本高一个命令 + 一份可选的配置文件反向操作出错后需要 reset 版本状态支持 --release-as 显式指定版本,好控制适用场景开源项目、重 CI 流程、多人高频发版内网项目、中小团队、接入 CI 但不想被绑定说直白一点:semantic-release 的"自动化"是激进的,它把整个发布流程接管了,适合完全信任流水线的团队。但很多项目实际没那么高频发版,团队也需要"看得见、可控制"的发布过程。standard-version 走的是"半自动"路线——版本号计算、CHANGELOG 生成、打 tag 都自动化,但推送和发布保留人工触发,出问题的时候你有足够的时间和空间去干预。我自己的标准是:能让机器算的绝不让人手动算,但留一个确认按钮比全自动更稳。1.3 工具链全景图与工作流整套方案的参与者有三个核心角色,每个角色只管一件事:commitizen:解决"怎么写"。提供交互式问答界面,引导开发者按规范格式生成提交信息,相当于给提交这件事套了个填写表单。commitlint + husky:解决"必须这么写"。husky 负责在 commit-msg 阶段拦截提交,commitlint 负责校验提交信息是否符合规范,不符合直接拒绝提交。standard-version:解决"怎么用这些信息"。自动分析提交历史里的 feat/fix/BREAKING CHANGE 等类型,计算出下一个版本号,更新版本文件,生成 CHANGELOG.md,并打上对应的 Git tag。整体工作流是:开发者执行git commit→ 触发 commitizen 交互问答 → 生成规范提交信息 → husky 触发 commitlint 校验 → 通过后完成提交。需要发版时,执行npm run release→ standard-version 分析历史提交 → 自动完成版本号更新 + CHANGELOG 生成 + tag 标记。这套设计最巧妙的地方在于:它把"规范"从人的记忆里搬到了工具的规则里。开发者不需要背任何规范,也不需要在提交前回忆"fix 和 feat 的区别是什么",commitizen 的问答会告诉你每一步要填什么;commitlint 则负责兜底,防止有人绕过交互式工具直接写了个无效提交。2. 核心细节解析:Conventional Commits + 三件套配置里容易被忽略的坑2.1 Conventional Commits 规范里的"隐性规则"配置之前,先把规范本身吃透。它规定提交信息必须符合这样的结构:<type>[optional scope]: <description> [optional body] [optional footer(s)]这份规范表面看起来就五行字,但在实际配置的时候有几条"隐性规则"特别容易踩:第一个坑是 type 的作用域。feat 和 fix 是目前所有工具默认识别的两个核心类型,直接影响版本号的自动变更。feat 对应下一个 minor 版本(比如当前 1.0.0,下一个是 1.1.0),fix 对应下一个 patch 版本(比如当前 1.0.0,下一个是 1.0.1)。但要注意,chore、docs、style、refactor 这类类型默认情况下不会触发版本号升级,如果你的团队习惯把所有改动都写成 chore,那这个版本号就会一直"跳不动"。所以配置 type 的枚举范围时,要结合团队实际的工作习惯来定,而不是把规范文档里的推荐列表原样抄过来。第二个坑是 BREAKING CHANGE 的写法。在 commit 的 message 里明确写BREAKING CHANGE:,或者 footer 里带!标记,standard-version 才会把这次变更识别为 major 版本升级。很多团队第一次用的时候,破坏了 API 但没在提交信息里写这个标记,版本号就只升了个 patch,等到外部依赖方禅了车才回来翻历史。这个要专门写进团队的规范里:涉及破坏性变更,提交信息必须显式声明。第三个坑是 scope 的使用边界。小团队建议不写 scope,因为大部分提交的粒度就一个模块,写 scope 反而增加了问答步骤的负担。但如果项目是多个业务线并存,scope 会很有价值,比如feat(user-center): 新增用户注册流程。决策逻辑是:只有当你觉得"光看 type 不够定位模块归属"的时候才启用 scope。2.2 commitlint 配置:rule 的优先级与误判commitlint 的核心是配置规则,我这边实际用的配置框架如下:// commitlint.config.js module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [2, 'always', ['feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'build', 'ci', 'chore', 'revert']], 'subject-case': [0], 'header-max-length': [2, 'always', 100] } };head-max-length设成 100 这个值我记得是从 console 到 Git 平台普遍支持的行宽来的。subject-case我直接关掉了,因为团队里有人习惯英文首字母大写,有人习惯小写,这个不强制反而减少摩擦。配置上有两个容易误判的细节:rules 的三要素数组[level, applicable, value]里,level=2 是 error 级别,会导致提交直接失败;level=1 是 warning,不阻断提交。千万别全改成 1,否则 commitlint 就变成"建议你改改,但你不改我也没办法",失去了强制意义。extends 继承的配置是会被覆盖的。type-enum在 relocated 配置后,就必须显式包含你需要的全部 type,否则 toolkit 默认配置里的perf、revert等就被"覆盖"掉了,容易出现"别的机器上能提交,你这台机器上说 type 不合法"的灵异现象。2.3 husky 新老版本差异:别再复制老教程了husky 在 6.0 之后有过一次大改版,很多 2020 年之前的技术文章用的配置方式已经失效了。我一开始接手项目的时候就踩过这个版本坑。老版本(5.x 及之前)的做法是:# 内部自动在 package.json 里写入依赖 npx husky install新版本(6.x+,目前是 9.x)的正确做法是:# 1. 在 package.json 的 scripts 里加 prepare 钩子 { "scripts": { "prepare": "husky" } } # 2. 创建具体的钩子文件 npx husky add .husky/commit-msg # 这个命令会在 .husky 目录下生成一个 commit-msg 文件关键区别在于:新版 husky 不再依赖 package.json 内的依赖注入,而是把 Git hooks 的实际执行位置放在项目根目录的.husky/目录里。这个目录要提交到 Git 仓库里,所有克隆下来的开发人员会自动继承这套 hook 配置。commit-msg 钩子文件里的内容长这样:npx --no -- commitlint --edit $1注意这里用了npx --no,它的作用是防止 npx 在找不到包时去远程下载,确保每次执行的都是项目本地安装的 commitlint 版本,避免"这台机器上校验通过、那台机器上校验不过"的分裂问题。2.4 standard-version 的版本号计算逻辑standard-version 的版本计算规则,说白了就是一套"自动语义化版本(SemVer)"逻辑。理解它的判断顺序很关键:首先看package.json里的version字段,作为当前版本基线。然后扫描git log里从上一次 version tag 到当前 HEAD的所有提交。只要发现某个 commit 包含BREAKING CHANGE标记(或者 type 为带!的 feat),下一个版本号的主版本 +1。否则如果有feat类型提交,次版本 +1。否则如果有fix类型提交,补丁版本 +1。如果以上都没有,报错提示:没有发现需要升级的内容,或者不产生任何版本变化。这个逻辑说明了两件事:第一,第一次发布之前的历史 commit 不参与计算,所以首次发版要用--first-release参数单独处理;第二,你想让版本号升得准确,前提就是提交信息必须规范,这两者的关系是强绑定的。3. 实操过程与核心环节实现:从零配置一份可直接落地的仓库3.1 初始化环境与安装依赖首先要把本地环境准备好,要求 Node.js 版本 >= 18(我用的 20 LTS),Git 版本 >= 2.30,然后在一个干净的目录里初始化:mkdir my-project cd my-project npm init -y接着安装核心依赖:# 安装 commitizen 相关依赖 npm install --save-dev commitizen @commitlint/cli @commitlint/config-conventional # 安装 husky npm install --save-dev husky # 安装 standard-version npm install --save-dev standard-version这里有一个选型细节:很多教程会建议把commitizen装成全局包,我不推荐。原因很简单——团队里每个人都在自己机器上全局装一遍,版本不一致,交互界面和配置行为都可能不同。项目级安装,配合npx使用,所有成员得到的体验完全一致。3.2 commitizen 交互式提交初始化安装完依赖后,需要把 commitizen 和适配器绑定起来。我用的是cz-conventional-changelog适配器:npx commitizen init cz-conventional-changelog --save-dev --save-exact这个命令会在package.json里写入一段 config 配置,内容如下:{ "config": { "commitizen": { "path": "./node_modules/cz-conventional-changelog" } } }然后在 scripts 里加两个命令:{ "scripts": { "commit": "cz", "release": "standard-version", "release:minor": "standard-version --release-as minor", "release:patch": "standard-version --release-as patch" } }这里解释一下为什么有release:minor和release:patch这两个"手动覆盖"的命令。standard-version 默认是自动分析提交历史来决定版本号的,但实际发版的时候会有一些特殊情况,比如这次上线只是修了一个小 bug,却希望版本号走到 minor 来对齐外部依赖方某些策略;或者团队在某个 release 周期里主动规划了一个大版本。有手动控制能力会让流程更灵活,不会在自动化规则面前无路可退。3.3 commitlint + husky 配置核心步骤先配置 husky:# 初始化 husky npx husky init # 添加 commit-msg 钩子文件 npx husky add .husky/commit-msg执行完之后,.husky/目录下应该有这些文件:.husky/ ├── _ │ ├── .gitignore │ ├── husky.sh │ └── pre-commit └── commit-msg_目录是 husky 内部框架,不要手动删。commit-msg文件里的内容就是提交信息校验的触发点,我们要把npx --no -- commitlint --edit $1这行写进去,整体内容如下:npx --no -- commitlint --edit $1然后验证一下 commitlint 配置。在项目根目录创建配置文件(我们已经在上面写出了示例,直接保存为commitlint.config.js):前面那段 commitlint 配置里,我特意把header-max-length保留在 100,没有用网上抄的 72。因为系统内部有大量带长 scope 的模块名,72 的宽度确实拦下来过几条合法的提交信息,改到 100 之后几乎没有误报了。这个数值取决于你们的项目实际情况,我的建议是:先设 100,如果团队提交 header 经常超长,再压下来也不迟。3.4 standard-version 配置与发布 releasestandard-version 的配置文件叫.versionrc.js(也可以叫.versionrc.json),我这边实际用的配置如下:module.exports = { // 参与版本号变更计算的头部信息类型 types: [ { type: 'feat', section: 'Features' }, { type: 'fix', section: 'Bug Fixes' }, { type: 'chore', section: 'Others' }, { type: 'docs', section: 'Others' }, { type: 'style', section: 'Others' }, { type: 'refactor', section: 'Others' }, { type: 'perf', section: 'Others' }, { type: 'test', section: 'Others' }, { type: 'build', section: 'Others' }, { type: 'ci', section: 'Others' }, { type: 'revert', section: 'Others' } ], // 指定参与版本变更的提交类型,只计算 feat/fix/revert preset: 'conventionalcommits', // 跳过自动打 tag skip: { tag: false }, // 不修改 package-lock.json 等其他文件 packageFiles: [], // 更新版本号的文件列表 bumpFiles: ['package.json', 'package-lock.json', 'README.md'] };这里有几个容易绕进去的配置点:types 里的 section 决定每一类 commit 被归并到 CHANGELOG 的哪个小标题下。让chore、docs、refactor都归到 Others 里,CHANGELOG 就会很整洁,不会出现一个 change log 里堆了十几个小分类的混乱感。packageFiles 和 bumpFiles 的区分:packageFiles是你扫描版本号所依据的文件列表(一般就是 package.json),bumpFiles是版本号更新后要同步修改内容的文件。默认情况下 package.json 会自动处理,如果你有用到 README.md 里写版本号徽章这种习惯,把 README.md 加进bumpFiles即可。首次发布版本的时候,因为仓库里还没有任何 version tag,standard-version 没法从历史中找到基线,所以要用首次发布模式:npm run release -- --first-release这个命令会做三件事:把 package.json 的版本号从 1.0.0 升到 1.0.0-1(如果原来没有版本号则用 1.0.0);生成 CHANGELOG.md;打上对应的 tag(但不会 push 到远端)。后续的正常发版流程就是:npm run release它会自动执行"分析提交 → 计算版本号 → 更新 package.json → 生成 CHANGELOG.md → 自动打 tag"这一整套动作。3.5 验证:一次完整的 release 流程为了让你直观感受整个流程,我模拟一遍完整的实际操作过程。执行交互式提交:npm run commitcommitizen 的交互界面上会依次出现这些问答:? 请选择您这次提交的变化类型: (Use arrow keys) feat: 增加新功能 fix: 修复 Bug ❯ chore: 对构建过程或辅助工具的更改 ? 请选择影响范围(可选,直接回车跳过): user-center ? 请输入简短的提交描述(必填): 新增用户中心注册逻辑 ? 请提供更详细的变更描述(可选):提交完成后,如果信息合法,commitlint 直接放行。如果我在问答里故意选了一个feat:类型但描述写了一串超过 100 字符的长文本,commit-msg 钩子就会拦截:> npx --no -- commitlint --edit .git/COMMIT_EDITMSG ⧗ input: feat(user-center): 新增用户中心注册逻辑,同时调整了用户协议页面的展示顺序和文案内容,并且重新组织了用户中心的子路由配置结构 ✖ subject may not be longer than 100 characters [header-max-length]紧接着发版流程:npm run release最终产出的 CHANGELOG.md 结构是这样的:# Changelog ## [1.1.0] - 2025-01-15 ### Features - **user-center:** 新增用户中心注册逻辑 ### Others - **deps:** 更新某依赖版本整个流程一气呵成,不需要人再去手动编辑任何文件。4. 常见问题与排查技巧实录4.1 husky 侧:钩子文件不触发现象:commit 的时候 commit-msg 钩子完全不执行,直接提交成功。首先检查.husky/目录是否存在于 Git 仓库里。如果你是在 husky init 之前就已经git init了,钩子目录是可以被识别到的;但如果是克隆仓库,需要确认.husky目录被提交上去了,否则 hook 根本不存在。排查命令如下:# 查看当前 hook 的真实状态 git config core.hooksPath # 正确输出应该指向 .husky/_如果输出结果是空或者指向.git/hooks,说明 husky init 没有生效,重新执行:npx husky init另一个典型坑:Windows 环境下的 shell 路径。如果钩子文件第一行写的#!/bin/sh,在 Windows 上可能因为换行符(LF/CRLF)问题报错。把.husky/目录下的文件都设为 LF 换行,或者团队统一要求 Git 配置core.autocrlf input,都可解决。具体操作是在项目根目录建超一个.gitattributes,加入*.sh text eol=lf。4.2 commitlint 侧:类型名不合法现象:跑 commit 的时候,明明提交信息看起来没问题,但 commitlint 报type must be one of [...]。这种情况几乎都是配置文件里的type-enum里没有包含你正在使用的那个 type。比如你写了feat, 配置里也是feat,还报错,那就去看 extends 是不是把规则覆盖了。最保险的验证方式是直接跑命令测试一条提交信息:echo "feat: 测试" | npx commitlint如果这个配置是合法的,任何报错都说明配置文件本身有问题。用这个方式做回归测试非常高效,不用反复触发真实提交。另外要记得:commitlint 的配置文件修改之后需要重启任何本地维持 watch 状态的 Node 进程,如果你们在跑某种热重载的本地服务,可能会有旧配置残留。4.3 standard-version 侧:版本号不升或升多了现象一:跑 standard-version 之后,版本号没有变化,也没有 CHANGELOG 更新。这通常意味着提交历史里没有任何 feat/fix 类型,或者全部是 chore 类型。解决方式是先审视一遍提交记录,看看是否真的没有功能变动。如果是"首次发布",记得加--first-release。现象二:改了一个很小的修复,版本号直接从 1.0.0 跳到 2.0.0。这是最昂贵的教训——肯定是某个提交信息里写了BREAKING CHANGE标记。排查方法:# 查看自上次发版以来所有包含 BREAKING 的提交 git log <old-tag>..HEAD --grep="BREAKING" --oneline如果审阅后确认这次变更并不应该升 major,可以直接用手动指定版本号的方式重跑:npm run release:patchstandard-version 会基于当前版本直接走 patch 逻辑,覆盖刚才自动计算出来的 major 版本。要提醒的是,这种操作方式属于救火场景,不做为常态。提交规范的意义就在于省的每次发布前还要手动去审版本号,能不走手动覆盖就不走。4.4 老项目回填与团队成员迁移如果你的仓库里已经有大量不规范的提交历史,把这套配置接进去的时候不要指望历史提交被自动规范化,不可能,也没必要。standard-version 只从最近一次 version tag 之后计算变更,所以老历史的"脏"不会影响新流程的正确性。但对团队里已经习惯"自由提交"的成员来说,刚上这套工具确实会有一段阵痛期。我给团队执行过一套迁移策略,效果不错:先用两周时间并行跑,commit-msg 规则设为 warning(level=1)级别,只提醒不拦截,让大家适应交互式问答。两周后再改成 error(level=2)级别,正式强制。同步做好一次培训,重点讲清楚"提交类型不是提交文案装饰,它直接决定版本号和 CHANGELOG",让成员理解为什么自己提交的时候会卡住。这一步做与不做,区别是很大的。像我之前带的项目,因为 skipped 了培训直接上了硬规范,结果团队连续三天提交都被拦,当天就把 commitlint 给临时屏蔽了,最后整个规范形同虚设。后来我改成"先软后硬"的节奏,两周内大家自然就把符合规范的提交信息背诵下来了。5. 我实际用下来的几点心得这套配置在几个项目里跑了也有一段时间了,说几个实际感受:第一,提交信息的价值密度是整个工具链里最高的。版本号可以随时靠手动释放,CHANGELOG 可以外包给文档工具,但提交信息是唯一人脑到工具都不需要二次翻译的数据。把规范嵌进 commitizen 问答里,等于把团队交底成本压到了最低。第二,standard-version 比起完全自动化的 release 工具,更适合中国团队的协作习惯。我们很多项目还要接定制化发布流程,比如发 CDN、同步内网仓库、触发外部构建,半自动的发布方式可以保证自动化计算和人工确认之间的平衡,不会出现一个 git push 就把生产包发出去的"惊喜"。第三,不要把这套配置当成"写完就结束"的一次性任务。工具链本身是静态的,但团队的提交习惯会逐渐演变。比如一开始 type-enum 只列了少量类型,等团队开始做性能优化专项,就需要把perf加进来。types配置也是,CHANGELOG 生成之后如果发现归类不合理,随时调整.versionrc.js里的 section 即可。如果你正准备给项目配这套流程,我的建议是:不用想着一口气把全链路全部接好,先把commitlint + husky搭起来,强制提交规范,跑两周之后再把 standard-version 接上,你会明显感受到"提交历史即文档"这件事带来的效率提升,那种翻代码不再靠"git log --oneline"人肉搜的感觉,真的挺值的。