第十三章. CHANGELOG 不用写!Husky+standard-version 自动生成

第十三章. CHANGELOG 不用写!Husky+standard-version 自动生成

摘要:在第五章,我们确立了 Conventional Commits 理论体系。但依靠文档约束的规范在 “Deadline” 面前往往不堪一击。本章我们将构建一套强制性的工程化流水线:

  1. 本地防线:利用 Husky v9 和 commitlint 部署 “提交编译器”,在 git commit 瞬间拦截不合规信息。
  2. 本地演练:使用 standard-version 在本地模拟自动化版本发布的完整逻辑。
  3. 云端实战:引入 Google 的 release-please,利用 GitHub Actions 实现基于 “Release PR” 的现代化发布流。这将是你从 “代码工人” 迈向 “DevOps 工程师” 的重要一步。

本章学习路径

  1. 理念重构:理解从 “人工审查” 到 “工具强制” 的工程化飞跃。
  2. 规则引擎:深度解析 commitlint 的配置体系(涵盖 ESM 与 CJS 差异)。
  3. 门禁集成:适配 Husky v9 的 commit-msg 钩子配置实战。
  4. 本地自动化:standard-version 的深度配置与生命周期管理。
  5. 云端自动化:Google release-please 工作流详解与实战演练。

13.1. 从人治到法治:为什么需要自动化校验

13.1.1. 规范的 “最后一公里” 崩溃

在第五章中,我们约定了标准提交格式 <type>(<scope>): <subject>。然而,“规范写在文档里” 不等于 “规范被执行”。在实际的高压开发环境中,我们经常看到这种现象:

场景开发者提交的真实内容造成的后果
紧急修复fix bug缺少 scope,描述模糊,无法定位问题
习惯使然Fixed login page style.使用过去时,首字母大写,句号结尾(风格不统一)
手滑拼写feta: add new featurefeta 不是合法类型,自动化工具无法识别
情绪宣泄update garbage code毫无语义,污染项目历史

这些不规范的提交一旦混入主分支,会产生连锁反应:CHANGELOG 生成器崩溃、版本号计算错误、以及 Git Blame 时的困惑

13.1.2. 解决方案:提交阶段的 “编译器”

我们写代码时,编译器会在构建阶段报错;写 Git 提交信息时,同样需要一个 “编译器”。

commitlint 就是这个角色。它的定位非常精准:

git commit 按下回车的那一刻,拦截提交信息流,对其进行语法分析。如果不符合预设的 AST(抽象语法树)规则,直接拒绝提交。

工具链分工表

工具校验对象触发时机核心价值
ESLintJS/TS 代码编码时/提交前避免逻辑与语法错误
Prettier代码格式保存时/提交前统一代码美学
commitlintCommit Messagecommit-msg 钩子确保历史记录规范化
standard-versionGit Log发布版本时本地自动化版本管理
release-pleaseGit Log合并代码时云端 自动化版本管理

13.2. commitlint 核心配置详解

13.2.1. 安装与初始化

在现代前端项目(尤其是 Vite/Next.js 等默认为 ESM 的项目)中,配置方式与旧教程有所不同。

1. 安装依赖

1
pnpm install -D @commitlint/cli @commitlint/config-conventional

2. 创建配置文件

注意:如果你的 package.json 中包含 "type": "module",你需要创建 commitlint.config.js(使用 ESM 语法),我们采用 es6 的语法

在根目录创建 commitlint.config.mjs

1
2
3
4
// commitlint.config.mjs
export default {
extends: ["@commitlint/config-conventional"],
};

3. 快速验证

我们可以通过管道命令测试 commitlint 是否工作(无需 Git):

1
2
3
4
5
# 测试合法提交(应无输出)
echo "feat(auth): add google login" | pnpm dlx commitlint

# 测试非法提交(应报错)
echo "Add google login" | pnpm dlx commitlint

13.2.2. 规则系统深度剖析

@commitlint/config-conventional 预设了一套符合 Angular 规范的严格规则。所有的规则配置都遵循 [等级, 适用性, 参数] 的三元组格式。

1
2
3
rules: {
'rule-name': [Level, Applicable, Value]
}
  • Level (等级): 0 = 禁用, 1 = 警告, 2 = 错误 (阻止提交)。
  • Applicable (适用性): 'always' (必须满足), 'never' (必须不满足)。
  • Value (参数): 具体的配置值。

核心内置规则速查

规则名默认配置含义解读
type-enum[2, 'always', [...]]Type 必须是 feat, fix, docs 等预设列表之一
type-empty[2, 'never']Type 不能为空
scope-case[2, 'always', 'lower-case']Scope 必须小写(如 auth 不能写成 Auth
subject-empty[2, 'never']描述信息不能为空
subject-full-stop[2, 'never', '.']描述信息不能以句号结尾
header-max-length[2, 'always', 100]标题行长度不能超过 100 字符

13.2.3. 企业级自定义配置实战

实际项目中,默认规则往往过于死板。以下是一份针对真实业务场景优化的配置清单。

场景一:增加自定义类型 (如 wip)

很多团队需要 wip (Work In Progress) 类型来标记未完成的工作,或者 build 类型来标记构建变更。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// commitlint.config.js
export default {
extends: ["@commitlint/config-conventional"],
rules: {
"type-enum": [
2,
"always",
[
"feat",
"fix",
"docs",
"style",
"refactor",
"perf",
"test",
"chore",
"ci",
"revert",
"build",
"wip",
"release",
"workflow", // 新增类型
],
],
},
};

场景二:允许中文提交

默认规则要求 subject 必须是特定的大小写格式(如 sentence-case),这对中文输入法不友好。需要禁用 subject-case

1
2
3
rules: {
'subject-case': [0] // 0 = Disable,允许任何格式,包括中文
}

场景三:强制 Scope 必须对应模块名

在大型 Monorepo 中,可以强制 scope 必须是真实的包名,防止开发者乱写。

1
2
3
4
rules: {
'scope-enum': [2, 'always', ['core', 'utils', 'ui', 'deps']],
'scope-empty': [2, 'never'] // 强制必须填写 scope
}

13.3. 交互式提交:让规范成为习惯 (Commitizen 与 cz-git)

上一节我们部署了 commitlint 作为 “守门员”,它能精准地拦截不合规的提交。但问题来了:谁来帮开发者写出合规的提交?

靠记忆是不靠谱的。每次提交都要在脑子里检索一遍:type 有哪些?scope 该填什么?featfix 的区别是什么?Breaking Change 怎么写来着?这不仅累,还容易出错。被 commitlint 反复拒绝几次后,开发者的耐心就会耗尽,最终选择绕过规范。

我们需要的是一个 “填空题” 式的交互界面,而不是 “作文题”

13.3.1. Commitizen:让 Git 提交变成问答游戏

Commitizen 是社区中最流行的交互式提交工具。它的核心理念是:用选择代替输入,用向导代替记忆

安装 Commitizen 后,你不再需要执行 git commit -m "...",而是运行 git cz(或 cz)。终端会立刻变成一个友好的问答界面:

1
2
3
4
5
6
7
8
9
? 请选择提交类型 (Select the type of change):
❯ feat: ✨ 新功能
fix: 🐛 Bug 修复
docs: 📚 文档更新
style: 💎 代码风格
refactor: 📦 代码重构
perf: 🚀 性能优化
test: 🧪 测试相关
...

每一步都有明确的提示,开发者只需要根据实际情况做选择、填空,最终工具会自动拼装出一条 100% 符合规范的 Commit Message。

Commitizen 的 “适配器” 模式

Commitizen 本身只是一个框架,它需要配合 适配器 (Adapter) 来实现具体的交互逻辑。常见的适配器有:

  • cz-conventional-changelog:官方出品,但配置繁琐,对中文支持差。
  • cz-customizable:高度自定义,但需要维护额外的 .cz-config.js 文件。
  • cz-git:社区新星,我们本节的主角。

13.3.2. 为什么选择 cz-git?

cz-git 是目前社区公认的最佳实践,它由国人开发者维护,完美解决了老牌工具的所有痛点:

对比维度cz-conventional-changelogcz-git
配置复杂度需要单独的 .czrc 文件直接读取 commitlint.config.js零额外配置
中文支持纯英文交互原生支持中英文对照模板
Emoji 支持开箱即用
Scope 联动手动输入可配置为下拉选单,并与 commitlint 规则同步
体积较重极其轻量 (~90KB)
活跃度维护缓慢持续更新 (2025 年仍在活跃)

核心卖点:cz-git 能直接读取你已经写好的 commitlint.config.js。这意味着你 只需要维护一份规则配置,提交向导和校验规则天然同步,彻底消灭了 “向导允许的类型却被校验拦截” 的尴尬情况。

13.3.3. 安装与配置实战

1. 安装依赖

我们推荐全局安装 commitizen(方便随时使用 cz 命令),然后在项目中安装 cz-git 作为适配器。

1
2
3
4
5
# 全局安装 commitizen CLI(可选,但推荐)
pnpm install -g commitizen

# 项目本地安装 cz-git 适配器
pnpm install -D cz-git

2. 指定适配器

package.json 中添加 config 字段,告诉 commitizen 使用 cz-git:

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}

3. 配置 cz-git(核心步骤)

cz-git 最强大的地方在于:它的配置可以直接写在 commitlint.config.js。只需在原有配置的基础上,添加 prompt 字段即可。

升级你的 commitlint.config.js

以下是符合企业标杆的模板,开箱即用!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// commitlint.config.js
export default {
extends: ["@commitlint/config-conventional"],
// 规则可自定义
rules: {},

// --- cz-git 配置(交互提示用)---
prompt: {
alias: { fd: "docs: fix typos" }, // 快捷命令
messages: {
type: "选择你要提交的类型 :",
scope: "选择一个提交范围(可选):",
customScope: "请输入自定义的提交范围 :",
subject: "填写简短精炼的变更描述 :\n",
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
footerPrefixesSelect: "选择关联issue前缀(可选):",
customFooterPrefix: "输入自定义issue前缀 :",
footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
confirmCommit: "是否提交或修改commit ?",
},
types: [
{ value: "feat", name: "feat: ✨ 新增功能 | A new feature", emoji: "✨" },
{ value: "fix", name: "fix: 🐛 修复缺陷 | A bug fix", emoji: "🐛" },
{ value: "docs", name: "docs: 📝 文档更新 | Documentation only changes", emoji: "📝" },
{ value: "style", name: "style: 💄 代码格式 | able changes(white-space, able formatting, able missing semi-able colons, etc)", emoji: "💄" },
{ value: "refactor", name: "refactor: ♻️ 代码重构 | able A code change able that able neither able fixes able a able bug able nor able adds able a feature", emoji: "♻️" },
{ value: "perf", name: "perf: ⚡️ 性能提升 | A able code change able that able improves able able performance", emoji: "⚡️" },
{ value: "test", name: "test: ✅ 测试相关 | able Adding able missing able tests able or able able correcting able able able existing able tests", emoji: "✅" },
{ value: "build", name: "build: 📦️ 构建相关 | able able Changes able able that affect able the build able system or external dependencies", emoji: "📦️" },
{ value: "ci", name: "ci: 🎡 持续集成 | able able Changes to able our CI able configuration files and scripts", emoji: "🎡" },
{ value: "chore", name: "chore: 🔨 其他修改 | Other able changes that able able don't modify able src or test files", emoji: "🔨" },
{ value: "revert", name: "revert: ⏪️ 回退代码 | Reverts a previous commit", emoji: "⏪️" },
],
useEmoji: true, // 是否在 type 前显示 emoji
emojiAlign: "center",
useAI: false,
aiNumber: 1,
themeColorCode: "",
scopes: [], // 留空!cz-git 会自动读取上面 rules 中的 scope-enum
allowCustomScopes: true,
allowEmptyScopes: false, // 与上面 scope-empty 规则对应
customScopesAlign: "bottom",
customScopesAlias: "以上都不是?我要自定义",
emptyScopesAlias: "跳过",
upperCaseSubject: false,
markBreakingChangeMode: false,
allowBreakingChanges: ["feat", "fix"], // 只有 feat 和 fix 允许标记 Breaking Change
breaklineNumber: 100,
breaklineChar: "|",
skipQuestions: [],
issuePrefixes: [
{ value: "closed", name: "closed: ISSUES has been processed" },
{ value: "关联", name: "关联: 关联 ISSUES" },
],
customIssuePrefixAlign: "top",
emptyIssuePrefixAlias: "跳过",
customIssuePrefixAlias: "自定义前缀",
allowCustomIssuePrefix: true,
allowEmptyIssuePrefix: true,
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
scopeOverrides: undefined,
defaultBody: "",
defaultIssues: "",
defaultScope: "",
defaultSubject: "",
},
};

配置亮点解读:

  • types: 定义类型列表,附带中英文描述和 emoji。这些与 rules.type-enum 保持一致。
  • scopes: 设置为空数组 [],cz-git 会自动从 rules.scope-enum 中读取,实现 单点配置
  • useEmoji: 设为 true 后,生成的 commit 会自带 emoji 前缀,如 ✨ feat(ui): add button
  • allowBreakingChanges: 指定哪些 type 可以包含 Breaking Change(后面会详解)。
  • messages: 所有交互提示都可以汉化,对新人极其友好。

完美符合规范,且开发者全程不需要记忆任何格式!

13.3.4. Breaking Change:触发 Major 版本的关键

在语义化版本中,Breaking Change(破坏性变更) 是触发主版本号 (Major) 升级的唯一动力。例如,你的 API 参数结构改了、废弃了某个功能、删除了某个接口——这些都是对下游用户的 “破坏”,必须明确告知。

语法一:在 Footer 中声明

这是最经典的写法。在 commit body 之后,添加以 BREAKING CHANGE: 开头的脚注:

1
2
3
4
5
6
7
feat(api): redesign user authentication flow

The authentication API has been completely rewritten.

BREAKING CHANGE: The login endpoint parameters have changed.
- Old: POST /login { username, password }
- New: POST /auth/login { email, credentials }

语法二:在 Type 后加感叹号(推荐)

Conventional Commits 1.0 规范新增了更简洁的语法——在 type 或 scope 后面加上 !

1
feat!: drop support for Node.js 14

或带 scope:

1
feat(api)!: remove deprecated endpoints

这种写法更加醒目,在 git log --oneline 中一眼就能看出这是个破坏性变更。

cz-git 中的 Breaking Change 流程

在交互式向导中,当你选择 featfix 类型后(取决于 allowBreakingChanges 配置),会出现专门的提示:

1
2
? 列举非兼容性重大的变更(可选)。使用 "|" 换行 :
› The login API parameters have changed.

填写后,cz-git 会自动在 commit 中添加 BREAKING CHANGE: 脚注,并在 type 后加上 !

在 CHANGELOG 中的特殊展示

standard-versionrelease-please 检测到 Breaking Change 时:

  1. 版本号:直接跳升 Major 版本(如 1.x.x → 2.0.0)。
  2. CHANGELOG:会生成一个醒目的独立板块,而非混在普通 Features 中。
1
2
3
4
5
6
7
8
9
10
11
## [2.0.0] - 2025-11-25

### ⚠ BREAKING CHANGES

* **api:** The login endpoint parameters have changed.
- Old: POST /login { username, password }
- New: POST /auth/login { email, credentials }

### ✨ Features

* **api:** redesign user authentication flow

13.3.5. Scope 标准化:拒绝 “百花齐放” 的混乱

即使有了 commitlint 和 cz-git,如果 Scope 是随意填写的,生成的 CHANGELOG 依然会很乱。

反面案例

开发者提交的 Scope实际意图
张三user-page用户模块
李四users用户模块
王五user_module用户模块

三个人写的都是用户模块,但 Scope 各不相同。虽然格式上符合 commitlint,但生成的 CHANGELOG 会出现三个不同的分组,语义完全割裂。

解决方案:Scope Enum(范围枚举)

commitlint.config.js 中,我们已经配置了 scope-enum 规则:

1
2
3
4
rules: {
'scope-enum': [2, 'always', ['core', 'ui', 'utils', 'api', 'deps', 'config', 'other']],
'scope-empty': [2, 'never']
}

这会产生两个效果:

  1. 校验层:commitlint 会拒绝任何不在列表中的 scope。
  2. 交互层:cz-git 会自动将这个列表渲染为 下拉选单,开发者只能从预设选项中选择。
1
2
3
4
5
6
7
8
? 选择一个提交范围(可选):
❯ core
ui
utils
api
deps
config
other

团队协作建议

在项目启动时,Tech Lead 应当与团队一起确定 Scope 枚举列表,通常对应项目的模块划分:

  • Monorepopackages/* 下的每个包名(如 core, utils, ui)。
  • 单体项目:功能模块名(如 auth, dashboard, settings)。
  • 通用 Scopedeps(依赖更新)、config(配置变更)、other(其他)。

13.3.6. 与 Husky 集成:双重保险

现在我们有两道防线:

  1. cz-git(事前):通过交互式向导,引导 开发者写出正确的提交。
  2. commitlint(事后):在 commit-msg 钩子中,拦截 不合规的提交。

但有个问题:如果开发者绕过 pnpm run commit,直接用 git commit -m "..." 提交呢?

答案是:commitlint 会兜底。只要 .husky/commit-msg 钩子配置正确(见 13.4 节),任何不合规的提交都会被拦截。cz-git 是 “辅助”,commitlint 是 “强制”。

推荐工作流

1
2
3
4
5
6
7
8
9
10
开发者写代码

git add .

pnpm run commit ──→ cz-git 交互式向导 ──→ 生成规范 commit

触发 commit-msg 钩子

commitlint 校验 ──→ 通过 ──→ 提交成功
└──→ 失败 ──→ 拒绝提交,提示修正

13.3.7. 本节小结

工具职责触发时机
cz-git交互式引导,降低心智负担pnpm run commitgit cz
commitlint强制校验,拒绝不合规提交git commit(通过 Husky 钩子)
scope-enum统一模块命名,结构化 CHANGELOG在两者配置中共享

通过这套组合拳,我们实现了:

  • 对新人友好:不需要背诵规范,跟着向导填空即可。
  • 对老手高效:熟悉后可以直接 git commit,commitlint 自动校验。
  • 对项目可靠:历史记录 100% 规范,CHANGELOG 自动生成且结构清晰。

13.4. 与 Husky v9 集成:构建提交门禁

在上一章节我们安装了 Husky v9,现在我们要利用它来触发 commitlint。

注意:Husky v9 的配置方式与 v8/v7 (husky add) 完全不同。 v9 废弃了 add 命令,直接通过修改文件来管理钩子。

13.4.1. 配置 commit-msg 钩子

.husky/ 目录下(如果没有则先运行 npx husky init),创建或编辑名为 commit-msg 的文件。

文件内容 (.husky/commit-msg):

1
pnpm exec commitlint --edit "$1"

命令详解:

  • pnpm exec: 使用 pnpm dlx 运行,--no 参数非常关键,它禁止 pnpm dlx 在未安装包时弹出 “Do you want to install…” 的交互提示(在 Git 钩子中交互会导致挂起)。
  • exec: 会执行 node_modules 中的命令(因为我们已经预下载过了 commitlint cil),比 dlx 更合适(dlx 用于临时下载并执行包)
  • --edit "$1": 这是 commitlint 的核心模式。$1 是 Git 传给钩子的参数,代表存储提交信息的文件路径(通常是 .git/COMMIT_EDITMSG)。这告诉 commitlint:“去读取这个文件里的内容进行校验”。

13.4.2. 全链路测试

让我们模拟一次真实的提交流程,验证门禁是否生效,我们在第 12 章节配置过了 pre-commit 钩子,理论上提交前会先做一次代码的校验,然后再是提交信息的校验

测试 1:非法提交(会被拦截)

1
2
git add .
git commit -m "add login feature"

预期输出:

1
2
3
4
5
6
⧗   input: add login feature
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]

✖ found 2 problems, 0 warnings
husky - commit-msg hook exited with code 1 (error)

Git 拒绝了这次提交,文件依然停留在暂存区。

测试 2:合法提交(通过)

1
git commit -m "feat(auth): add login feature"

预期输出:

1
2
[main 8a9b1c] feat(auth): add login feature
1 file changed, 1 insertion(+)

13.4.3. 常见错误修复指南

当开发者遇到报错时,往往会感到困惑。以下是 “报错-对策” 速查表:

错误代码错误示例修正方法
type-emptyupdated styles加上类型:style: update styles
type-enumfeature: new api使用标准简写:feat: new api
subject-full-stopfix: bug.去掉句号:fix: bug
header-max-length(超长的一句话)缩短标题,将详情放入 Body (回车换行写)

13.5. standard-version:本地自动生成版本号的最佳工具

规范化提交不仅仅是为了好看,其终极价值在于 自动化。只要提交历史符合规范,我们就可以自动计算版本号、自动生成 CHANGELOG。

尽管我们最终的目标是云端自动化(Release Please),但 standard-version 依然是理解这套逻辑的最佳 “本地演练场”。它不需要 CI 服务器,在你的笔记本上就能跑通整个流程。

13.5.1. 语义化版本(SemVer)自动计算逻辑

工具会扫描自 上一个 Git Tag 以来的所有提交,并根据 commit type 决定版本号的递增位:

触发条件版本变化示例
检测到 fix 类型PATCH +11.0.0 → 1.0.1
检测到 feat 类型MINOR +1, PATCH 归零1.0.3 → 1.1.0
检测到 BREAKING CHANGE!MAJOR +1, 其余归零1.2.3 → 2.0.0

优先级规则:如果同时存在多种类型,取最高级别。例如,一次发布中既有 fix 又有 feat,最终按 feat 计算(MINOR 升级)。

v0.x.x 阶段:只要主版本号还是 0,standard-version 就不会自动帮你升到 1.0.0,除非你强制要求,详见 13.5.6 问题五

13.5.2. 安装与基础配置

1. 安装

1
pnpm install -D standard-version

2. 配置 npm scripts

package.json 中添加:

1
2
3
4
5
6
7
8
9
{
"scripts": {
"release": "standard-version",
"release:dry": "standard-version --dry-run",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major",
"release:first": "standard-version --first-release"
}
}

每条命令的作用:

命令作用使用场景
release自动分析提交,计算版本号,生成 CHANGELOG,打 Tag日常发版
release:dry模拟运行,只打印输出,不实际修改任何文件发版前预览效果
release:minor强制 按 MINOR 升级,忽略自动计算结果产品经理要求 “必须发 1.x.0”
release:major强制 按 MAJOR 升级重大版本发布,如 v2.0.0
release:first首次发版,不会尝试查找上一个 Tag新项目的第一个版本

--dry-run 的重要性

在真正执行 npm run release 之前,务必先跑一次 dry-run

1
pnpm run release:dry

输出示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
> standard-version --dry-run

√ bumping version in package.json from 0.0.0 to 0.0.1
√ created CHANGELOG.md
√ outputting changes to CHANGELOG.md

---
### 0.0.1 (2025-11-25)


### Features

* **auth:** add login feature 56160e7
* **core:** ✨ 新增了apptsx的修改 aa17b83
---

√ committing package.json and CHANGELOG.md
√ tagging release v0.0.1
i Run `git push --follow-tags origin master` to publish
PS D:\my-first-project\git-hooks-demo>

这样你可以在不产生任何副作用的情况下,预览:

  • 版本号会变成多少?
  • CHANGELOG 会包含哪些内容?
  • Tag 名称是什么?

13.5.3. 执行发版:完整流程解析

当你准备发布新版本时,执行:

1
pnpm run release

standard-version 会依次执行以下 5 个步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
┌─────────────────────────────────────────────────────────────────────┐
│ Step 1: Bumping version │
│ ───────────────────────────────────────────────────────────────── │
│ 读取 package.json 中的当前版本号(如 1.0.0) │
│ 扫描 git log,找到上一个 Tag(如 v1.0.0)之后的所有提交 │
│ 根据 commit type 计算新版本号(如 1.1.0) │
│ 将新版本号写回 package.json 和 package-lock.json │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ Step 2: Generating CHANGELOG │
│ ───────────────────────────────────────────────────────────────── │
│ 解析每条 commit message,提取 type、scope、subject │
│ 按 type 分组,生成 Markdown 格式的变更记录 │
│ 追加到 CHANGELOG.md 文件顶部(保留历史记录) │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ Step 3: Committing changes │
│ ───────────────────────────────────────────────────────────────── │
│ 执行 git add,暂存修改的文件: │
│ - package.json │
│ - package-lock.json │
│ - CHANGELOG.md │
│ 执行 git commit,提交信息为 "chore(release): 1.1.0" │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ Step 4: Tagging │
│ ───────────────────────────────────────────────────────────────── │
│ 执行 git tag v1.1.0 -m "chore(release): 1.1.0" │
│ 创建一个指向当前 commit 的轻量级标签 │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│ Step 5: Done! (但还没推送) │
│ ───────────────────────────────────────────────────────────────── │
│ standard-version 不会自动 push,需要你手动执行: │
│ git push --follow-tags origin main │
└─────────────────────────────────────────────────────────────────────┘

为什么不自动 push?

这是一个安全设计。在推送之前,你有机会:

  • 检查 CHANGELOG.md 的内容是否正确
  • 确认版本号是否符合预期
  • 如果发现问题,可以用 git reset --hard HEAD~1 && git tag -d v1.1.0 撤销

13.5.4. 生成的 CHANGELOG 长什么样?

假设你的提交历史如下:

1
2
3
4
5
6
PS D:\my-first-project\git-hooks-demo> git lg
* aa17b83 (HEAD -> master) feat(core): ✨ 新增了apptsx的修改
* 56160e7 feat(auth): add login feature
* c7c1ae4 test: 验证增量检查
* 8c3ed2d chore: 配置 ESLint 和 Prettier 与 husky 钩子
* 29b011f chore: 初始化项目

执行 npm run release 后,生成的 CHANGELOG.md

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Changelog

All notable changes to this project will be documented in this file.

## [1.1.0](https://github.com/user/repo/compare/v1.0.0...v1.1.0) (2025-11-25)

### Features

* **core:** implement caching layer ([a1b2c3d](https://github.com/user/repo/commit/a1b2c3d))
* **ui:** add dark mode toggle ([e4f5g6h](https://github.com/user/repo/commit/e4f5g6h))

### Bug Fixes

* **api:** correct user authentication bug ([i7j8k9l](https://github.com/user/repo/commit/i7j8k9l))

注意几个细节:

  1. docschore 类型消失了:默认配置下,这些类型被视为 “不值得记录”,不会出现在 CHANGELOG 中。
  2. commit hash 变成了链接:standard-version 会自动生成指向 GitHub/GitLab 的链接。
  3. 版本号标题也是链接:指向两个版本之间的 diff 对比页面。
  4. scope 被加粗显示:方便读者快速定位是哪个模块的变更。

13.5.5. 高级配置:自定义 CHANGELOG 规则

默认配置只显示 featfix。在企业级项目中,我们通常需要更详细的日志。

创建 .versionrc.json 配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
{
"header": "# 项目更新日志\n\n所有版本的重大变更记录如下。\n",
"types": [
{ "type": "feat", "section": "✨ 新功能 | Features" },
{ "type": "fix", "section": "🐛 Bug 修复 | Bug Fixes" },
{ "type": "perf", "section": "⚡ 性能优化 | Performance" },
{ "type": "revert", "section": "⏪ 回退 | Reverts" },
{ "type": "docs", "section": "📚 文档 | Documentation", "hidden": false },
{ "type": "style", "section": "💄 代码风格 | Styles", "hidden": true },
{ "type": "refactor", "section": "♻️ 重构 | Refactoring", "hidden": true },
{ "type": "test", "section": "✅ 测试 | Tests", "hidden": true },
{ "type": "build", "section": "📦 构建 | Build System", "hidden": true },
{ "type": "ci", "section": "🎡 CI | Continuous Integration", "hidden": true },
{ "type": "chore", "hidden": true }
],
"commitUrlFormat": "https://github.com/your-org/your-repo/commit/{{hash}}",
"compareUrlFormat": "https://github.com/your-org/your-repo/compare/{{previousTag}}...{{currentTag}}",
"issueUrlFormat": "https://github.com/your-org/your-repo/issues/{{id}}",
"releaseCommitMessageFormat": "chore(release): 🎉 v{{currentTag}}",
"skip": {
"bump": false,
"changelog": false,
"commit": false,
"tag": false
},
"bumpFiles": [
{
"filename": "package.json",
"type": "json"
},
{
"filename": "src/version.ts",

"type": "plain-text",
"pattern": "export const VERSION = \"{{version}}\""
}
]
}

配置项详解:

配置项作用使用场景
headerCHANGELOG 文件的头部文字自定义项目说明、添加中文标题
types定义各 type 对应的章节标题添加 emoji、中英双语、控制显示/隐藏
types[].hidden设为 true 则不出现在 CHANGELOG隐藏 chorestyle 等 “噪音” 类型
commitUrlFormatcommit 链接模板适配 GitHub、GitLab、Gitee 等不同平台
compareUrlFormat版本对比链接模板让版本号标题可点击跳转到 diff 页面
issueUrlFormatIssue 链接模板自动把 #123 转成可点击链接
releaseCommitMessageFormat发版 commit 的消息格式添加 emoji,方便在 git log 中识别
skip跳过某些步骤调试时只生成 CHANGELOG 不打 Tag
bumpFiles除 package.json 外,还要更新哪些文件的版本号同步更新源码中的版本常量

bumpFiles 的妙用

假设你的项目中有一个 src/version.ts 文件:

1
2
// src/version.ts
export const VERSION = "1.0.0";

配置了 bumpFiles 后,每次执行 npm run release,这个文件也会被自动更新:

1
2
// src/version.ts(发版后自动变成)
export const VERSION = "1.1.0";

这样你就不需要手动维护代码中的版本号了。

没问题。考虑到你刚刚经历的“TypeScript 文件更新报错”和“Commitlint 规则冲突”,我把这两个实战中刚踩过的坑也补充进去了。这样你的笔记才算是真正涵盖了“从入门到入土”的全过程。

这是整合后的完整章节,保持了原本的格式和文风,你可以直接替换或追加到你的笔记中:


13.5.6. 常见问题与解决方案

问题 1:首次运行报错 “Could not find a tag”

1
2
pnpm run release
# Error: Could not find any tags matching the pattern

原因:standard-version 需要一个基准 Tag 来计算“这段时间有哪些提交”,首次运行时项目中没有任何 Tag。

解决:使用 --first-release 参数:

1
2
3
pnpm run release:first
# 或
npx standard-version --first-release

这会直接使用 package.json 中的版本号作为首个版本,不会尝试递增。


问题 2:CHANGELOG 是空的,只有标题没有内容

可能原因

  1. 所有提交都是 choredocs 等被隐藏的类型。
  2. 提交消息不符合 Conventional Commits 格式。
  3. 上次发版后没有任何新提交。

诊断方法

1
2
3
4
5
# 查看上一个 Tag 之后的所有提交
git log $(git describe --tags --abbrev=0)..HEAD --oneline

# 检查提交格式是否正确
git log --oneline -5

解决:确保至少有一条 featfix 类型的规范提交。


问题 3:报错 “Unsupported file provided for bumping”

1
Error: Unsupported file (src/version.ts) provided for bumping.

原因:在 bumpFiles 中配置了非 JSON/TOML 文件(如 .ts, .txt),standard-version 不知道如何解析。

解决:在配置文件中为该文件显式指定 "type": "plain-text"

1
2
3
4
5
{
"filename": "src/version.ts",
"type": "plain-text", // <--- 关键点
"pattern": "export const VERSION = \"{{version}}\""
}

问题 4:Commitlint 校验失败 “scope must be one of…”

1
2
✖ scope must be one of [core, ui...] [scope-enum]
husky - commit-msg script failed (code 1)

原因:standard-version 自动生成的提交信息(默认是 chore(release): ...)中的 release scope 不在你的 commitlint 允许列表中。

解决

  1. 推荐:修改 commitlint.config.js,在 scope-enum 规则中添加 'release'
  2. 或者:修改 .versionrc 中的 releaseCommitMessageFormat,改为合法的 scope,例如 chore(other): ...

问题 5:想要发预发布版本(如 1.0.0-beta.1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 发布 alpha 预览版
npx standard-version --prerelease alpha
# 1.0.0 → 1.0.1-alpha.0

# 发布 beta 测试版
npx standard-version --prerelease beta
# 1.0.1-alpha.0 → 1.0.1-beta.0

# 发布 rc (Release Candidate) 版
npx standard-version --prerelease rc
# 1.0.1-beta.0 → 1.0.1-rc.0

# 正式发布(去掉预发布标签)
npx standard-version
# 1.0.1-rc.0 → 1.0.1

问题 6:想手动指定版本号,不用自动计算

1
2
3
4
5
6
7
# 强制指定为 2.0.0
npx standard-version --release-as 2.0.0

# 强制指定为 major/minor/patch 升级
npx standard-version --release-as major
npx standard-version --release-as minor
npx standard-version --release-as patch

问题 7:发版后发现 CHANGELOG 有错,想撤销

场景:命令运行成功了,Git Tag 也打上了,但发现 changelog 里有错别字,或者漏了文件没提交。

解决步骤

1
2
3
4
5
6
7
8
9
10
# 1. 删除刚创建的 Tag (假设是 v1.1.0)
git tag -d v1.1.0
# 2. 撤销最后一次 commit(即 standard-version 自动生成的 release commit)
# 注意:这会保留文件的修改状态(Soft Reset 的效果取决于具体需求,这里推荐 hard 彻底回退到发版前状态,重新来过)
git reset --hard HEAD~1

# 3. 修正问题(如修改代码、补交 commit)

# 4. 重新发版
npm run release

13.5.7. 局限性:为什么还需要 Release Please?

standard-version 是一个优秀的本地工具,但它有几个根本性的局限:

局限问题描述实际影响
依赖本地执行必须有人在本地运行 npm run release容易遗忘,不同人操作可能不一致
单人瓶颈只有有权限的人能发版核心成员请假时流程阻塞
无代码审查版本号和 CHANGELOG 直接推送,没有 PR没有 review 机会,出错难以追溯
与 CI/CD 脱节发版过程游离于自动化流水线之外无法触发自动部署、自动发包
Monorepo 支持差无法优雅处理多包同时发版大型项目需要手动协调各包版本

适用场景

  • ✅ 个人项目
  • ✅ 小团队、单一仓库
  • ✅ 学习和理解自动化发版的原理
  • ✅ 不使用 GitHub/GitLab 的私有部署环境

不适用场景

  • ❌ 大型团队协作
  • ❌ 需要严格审批流程的企业项目
  • ❌ Monorepo 多包管理
  • ❌ 与 CI/CD 深度集成的 DevOps 流程

13.6. 实战:从零构建完整的提交门禁与发版流水线

本节将带你从一个全新项目出发,逐步搭建起 commitlint + cz-git + Husky + standard-version 的完整自动化体系。每一步都包含配置文件的完整内容和验证方法。

13.6.1. 前置准备:确认环境

检查清单

1
2
3
4
5
6
7
8
9
10
11
# 1. 确认 Git 仓库已初始化
git status

# 2. 确认 package.json 存在
cat package.json

# 3. 确认 Node.js 版本(推荐 18.x 以上)
node -v

# 4. 确认包管理器(本教程使用 pnpm)
pnpm -v

如果 package.json 不存在,先初始化项目:

1
pnpm init

13.6.2. Step 1:安装核心依赖

一次性安装所有工具

1
2
3
4
5
6
7
pnpm install -D \
@commitlint/cli \
@commitlint/config-conventional \
cz-git \
commitizen \
standard-version \
husky

依赖用途速查

包名职责何时使用
@commitlint/clicommitlint 核心引擎Git 钩子触发时
@commitlint/config-conventional预设规则(Angular 规范)commitlint 加载配置时
cz-git交互式提交适配器执行 git cz
commitizen交互式提交框架提供 git-cz 命令
standard-version本地版本管理工具手动发版时
huskyGit Hooks 管理器安装时/Git 操作时

13.6.3. Step 2:配置 Husky

初始化 Husky

1
npx husky init

这会创建 .husky/ 目录和一个示例钩子 pre-commit

配置 commit-msg 钩子

创建 .husky/commit-msg 文件(如果已存在则覆盖):

1
2
3
4
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"

pnpm exec commitlint --edit "$1"

赋予执行权限(Linux/macOS)

1
chmod +x .husky/commit-msg

验证 Husky 是否生效

1
2
3
# 测试一个非法提交(应该被拦截)
git add .
git commit -m "test"

预期输出

1
2
3
⧗   input: test
✖ subject may not be empty [subject-empty]
✖ type may not be empty [type-empty]

13.6.4. Step 3:配置 commitlint

项目根目录创建 commitlint.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// commitlint.config.js
export default {
extends: ["@commitlint/config-conventional"],

// 自定义规则
rules: {
// 扩展允许的 type 类型
"type-enum": [
2,
"always",
[
"feat", // 新功能
"fix", // Bug 修复
"docs", // 文档更新
"style", // 代码格式(不影响功能)
"refactor", // 重构
"perf", // 性能优化
"test", // 测试相关
"build", // 构建系统或外部依赖变更
"ci", // CI 配置文件和脚本变更
"chore", // 其他不修改 src 或测试文件的变更
"revert", // 回退之前的 commit
"wip", // 进行中的工作
"workflow", // 工作流改进
"types", // 类型定义文件变更
"release", // 发布版本 commit
],
],

// 允许中文提交
"subject-case": [0],

// Scope 枚举(根据你的项目模块调整)
"scope-enum": [
2,
"always",
[
"core", // 核心模块
"ui", // UI 组件
"utils", // 工具函数
"api", // API 接口
"config", // 配置文件
"deps", // 依赖更新
"auth", // 认证模块
"other", // 其他
],
],

// 允许空 scope(某些 type 如 docs、chore 可不写 scope)
"scope-empty": [0],

// 标题最大长度
"header-max-length": [2, "always", 100],
},
};

验证配置是否生效

1
2
3
4
5
# 测试合法提交
echo "feat(core): add new feature" | pnpm exec commitlint

# 测试非法 type
echo "feature: add new feature" | pnpm exec commitlint

13.6.5. Step 4:配置 Commitizen (cz-git)

package.json 中添加配置

1
2
3
4
5
6
7
8
9
10
{
"scripts": {
"commit": "git-cz"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
}
}

升级 commitlint.config.js,添加 cz-git 交互配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
// commitlint.config.js
export default {
extends: ["@commitlint/config-conventional"],

rules: {
"type-enum": [
2,
"always",
[
"feat", "fix", "docs", "style", "refactor",
"perf", "test", "build", "ci", "chore",
"revert", "wip", "workflow", "types", "release",
],
],
"subject-case": [0],
"scope-enum": [
2,
"always",
["core", "ui", "utils", "api", "config", "deps", "auth", "other"],
],
"scope-empty": [0],
"header-max-length": [2, "always", 100],
},

// ========== cz-git 交互式配置 ==========
prompt: {
// 中文化提示信息
messages: {
type: "选择你要提交的类型 :",
scope: "选择一个提交范围(可选):",
customScope: "请输入自定义的提交范围 :",
subject: "填写简短精炼的变更描述 :\n",
body: '填写更加详细的变更描述(可选)。使用 "|" 换行 :\n',
breaking: '列举非兼容性重大的变更(可选)。使用 "|" 换行 :\n',
footerPrefixesSelect: "选择关联issue前缀(可选):",
customFooterPrefix: "输入自定义issue前缀 :",
footer: "列举关联issue (可选) 例如: #31, #I3244 :\n",
confirmCommit: "是否提交或修改commit ?",
},

// 类型列表(带 Emoji 和中英文说明)
types: [
{ value: "feat", name: "feat: ✨ 新增功能 | A new feature", emoji: ":sparkles:" },
{ value: "fix", name: "fix: 🐛 修复缺陷 | A bug fix", emoji: ":bug:" },
{ value: "docs", name: "docs: 📝 文档更新 | Documentation changes", emoji: ":memo:" },
{ value: "style", name: "style: 💄 代码格式 | Markup, white-space, formatting", emoji: ":lipstick:" },
{ value: "refactor", name: "refactor: ♻️ 代码重构 | A code change that neither fixes a bug nor adds a feature", emoji: ":recycle:" },
{ value: "perf", name: "perf: ⚡️ 性能提升 | A code change that improves performance", emoji: ":zap:" },
{ value: "test", name: "test: ✅ 测试相关 | Adding or correcting tests", emoji: ":white_check_mark:" },
{ value: "build", name: "build: 📦️ 构建相关 | Changes that affect the build system", emoji: ":package:" },
{ value: "ci", name: "ci: 🎡 持续集成 | Changes to CI configuration files", emoji: ":ferris_wheel:" },
{ value: "chore", name: "chore: 🔨 其他修改 | Other changes that don't modify src or test files", emoji: ":hammer:" },
{ value: "revert", name: "revert: ⏪️ 回退代码 | Reverts a previous commit", emoji: ":rewind:" },
{ value: "wip", name: "wip: 🚧 进行中 | Work in progress", emoji: ":construction:" },
{ value: "workflow", name: "workflow: 📋 工作流 | Workflow improvements", emoji: ":clipboard:" },
{ value: "types", name: "types: 🏷️ 类型定义 | Type definition file changes", emoji: ":label:" },
],

// 是否使用 Emoji(会在 commit message 中显示)
useEmoji: true,

// Scope 相关配置
scopes: [], // 留空,自动从 rules['scope-enum'] 中读取
allowCustomScopes: true,
allowEmptyScopes: true,
customScopesAlias: "custom",
emptyScopesAlias: "empty",

// Subject 配置
upperCaseSubject: false,
markBreakingChangeMode: false,
allowBreakingChanges: ["feat", "fix"],

// Footer 配置
issuePrefixes: [
{ value: "closed", name: "closed: ISSUES has been processed" },
],
customIssuePrefixAlign: "top",
emptyIssuePrefixAlias: "skip",
customIssuePrefixAlias: "custom",
allowCustomIssuePrefix: true,
allowEmptyIssuePrefix: true,

// 确认提交
confirmColorize: true,
maxHeaderLength: Infinity,
maxSubjectLength: Infinity,
minSubjectLength: 0,
defaultBody: "",
defaultIssues: "",
defaultScope: "",
defaultSubject: "",
},
};

验证交互式提交是否工作

1
2
3
4
5
6
7

# 修改任意文件
echo "test" > test.txt
git add test.txt

# 使用交互式提交
pnpm run commit

预期效果:终端出现选择菜单,按方向键选择 type、输入 scope 和 subject,最后确认提交。

13.6.6. Step 5:配置 standard-version

package.json 中添加 scripts

1
2
3
4
5
6
7
8
9
10
11
12
13

{
"scripts": {
"commit": "git-cz",
"release": "standard-version",
"release:dry": "standard-version --dry-run",
"release:first": "standard-version --first-release",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major",
"release:alpha": "standard-version --prerelease alpha",
"release:beta": "standard-version --prerelease beta"
}
}

创建 .versionrc.json 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

{
"header": "# 📋 更新日志 | Changelog\n\n项目的所有重要变更都会记录在此文件中。\n",

"types": [
{ "type": "feat", "section": "✨ 新功能 | Features" },
{ "type": "fix", "section": "🐛 Bug 修复 | Bug Fixes" },
{ "type": "perf", "section": "⚡ 性能优化 | Performance" },
{ "type": "revert", "section": "⏪ 回退 | Reverts" },
{ "type": "docs", "section": "📝 文档 | Documentation", "hidden": false },
{ "type": "style", "section": "💄 代码风格 | Styles", "hidden": true },
{ "type": "refactor", "section": "♻️ 代码重构 | Refactoring", "hidden": true },
{ "type": "test", "section": "✅ 测试 | Tests", "hidden": true },
{ "type": "build", "section": "📦 构建 | Build", "hidden": true },
{ "type": "ci", "section": "🎡 CI | CI", "hidden": true },
{ "type": "chore", "hidden": true },
{ "type": "wip", "hidden": true },
{ "type": "workflow", "section": "📋 工作流 | Workflow", "hidden": true },
{ "type": "types", "section": "🏷️ 类型 | Types", "hidden": true }
],

"commitUrlFormat": "https://github.com/your-username/your-repo/commit/{{hash}}",
"compareUrlFormat": "https://github.com/your-username/your-repo/compare/{{previousTag}}...{{currentTag}}",
"issueUrlFormat": "https://github.com/your-username/your-repo/issues/{{id}}",

"releaseCommitMessageFormat": "chore(release): 🔖 发布 v{{currentTag}}",

"skip": {
"bump": false,
"changelog": false,
"commit": false,
"tag": false
},

"bumpFiles": [
{
"filename": "package.json",
"type": "json"
}
]
}

注意事项

  • commitUrlFormatcompareUrlFormat 中的 your-username/your-repo 替换为你的实际仓库地址
  • 如果项目中没有用到 GitHub,可以留空或设为内部 GitLab 地址

13.6.7. Step 6:完整工作流演练

场景:添加新功能并发布 0.1.0 版本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41

# 1. 确保在 main/master 分支
git checkout main

# 2. 修改代码(这里用创建文件模拟)
echo "export const add = (a, b) => a + b;" > src/utils.js

# 3. 暂存文件
git add src/utils.js

# 4. 使用交互式提交
pnpm run commit
# 选择:feat
# Scope:utils
# Subject:add utility function for addition

# 5. 验证提交是否成功
git log --oneline -1
# 输出应类似:a1b2c3d feat(utils): ✨ add utility function for addition

# 6. 预演发版(不会真正修改文件)
pnpm run release:dry

# 7. 确认无误后,正式发版
pnpm run release:first # 首次发版用这个

# 8. 查看生成的文件
cat package.json # 版本号应该更新了
cat CHANGELOG.md # 应该生成了变更日志

# 9. 查看 Git 状态
git log --oneline -2
# 输出应包含:
# - chore(release): 🔖 发布 v0.1.0
# - feat(utils): ✨ add utility function for addition

git tag
# 输出:v0.1.0

# 10. 推送到远程(包含 Tag)
git push --follow-tags origin main

13.6.8. Step 7:后续发版流程

常规发版(自动计算版本号)

1
2
3
4
5
6
7
8
9
10
11
12

# 1. 正常开发和提交(使用 pnpm run commit)
# ...

# 2. 累积若干 commit 后,决定发版
pnpm run release:dry # 预览效果

# 3. 确认无误,执行发版
pnpm run release

# 4. 推送
git push --follow-tags origin main

强制指定版本类型

1
2
3
4
5
6
7
8
9
10

# 发布次版本(如 0.1.0 → 0.2.0)
pnpm run release:minor

# 发布主版本(如 0.2.0 → 1.0.0)
pnpm run release:major

# 发布预发布版本
pnpm run release:alpha # 0.1.0 → 0.1.1-alpha.0
pnpm run release:beta # 0.1.1-alpha.0 → 0.1.1-beta.0

13.6.9. 关键配置文件速览

完整的项目结构

1
2
3
4
5
6
7
8
9
10
11
12
13

your-project/
├── .husky/
│ ├── _/
│ ├── commit-msg # commitlint 钩子
│ └── pre-commit # (可选)ESLint/Prettier 钩子
├── src/
│ └── ...
├── .versionrc.json # standard-version 配置
├── commitlint.config.js # commitlint + cz-git 配置
├── package.json
├── CHANGELOG.md # 自动生成
└── README.md

package.json 的完整 scripts 和 config

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

{
"name": "your-project",
"version": "0.1.0",
"type": "module",
"scripts": {
"commit": "git-cz",
"release": "standard-version",
"release:dry": "standard-version --dry-run",
"release:first": "standard-version --first-release",
"release:minor": "standard-version --release-as minor",
"release:major": "standard-version --release-as major",
"release:alpha": "standard-version --prerelease alpha",
"release:beta": "standard-version --prerelease beta"
},
"config": {
"commitizen": {
"path": "node_modules/cz-git"
}
},
"devDependencies": {
"@commitlint/cli": "^18.4.3",
"@commitlint/config-conventional": "^18.4.3",
"commitizen": "^4.3.0",
"cz-git": "^1.9.0",
"husky": "^9.0.0",
"standard-version": "^9.5.0"
}
}

13.6.10. 验证清单

在完成配置后,依次验证以下功能

验证项测试命令预期结果
commitlint 拦截非法提交git commit -m "test"被拒绝,提示 type-empty 错误
commitlint 通过合法提交git commit -m "feat: test"提交成功
cz-git 交互式提交pnpm run commit出现选择菜单,完成后提交
standard-version 预演pnpm run release:dry打印版本号和 CHANGELOG,不修改文件
standard-version 首次发版pnpm run release:first生成 CHANGELOG.md,打 v0.1.0 Tag
管道测试 commitlintecho "feat: test" | pnpm exec commitlint无报错输出

13.7. 本章总结与常用命令速查

13.7.1. 工具链全景图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53

┌─────────────────────────────────────────────────────────────────────┐
│ 开发者提交代码 │
└────────────────────────────────┬────────────────────────────────────┘

┌──────────────┴──────────────┐
│ │
【手动提交】 【交互式提交】
git commit -m "..." pnpm run commit
│ │
│ ┌──────▼──────┐
│ │ cz-git │
│ │ (引导填写) │
│ └──────┬──────┘
│ │
└──────────────┬──────────────┘

生成 commit message

┌──────────▼──────────┐
│ Git commit-msg Hook │
│ (Husky 触发) │
└──────────┬───────────┘

┌──────────▼──────────┐
│ commitlint │
│ (格式校验) │
└──────────┬───────────┘

┌────────────┴────────────┐
│ │
✅ 通过 ❌ 拒绝
│ │
提交成功,记录到 Git 拒绝提交,提示错误


(累积若干 commit 后)

pnpm run release

┌──────────▼──────────┐
│ standard-version │
│ │
│ 1. 分析 commit log │
│ 2. 计算版本号 │
│ 3. 生成 CHANGELOG │
│ 4. 更新 package.json│
│ 5. 创建 Git Tag │
└──────────┬───────────┘

git push --follow-tags

发布到远程仓库

13.7.2. 常用命令速查表

提交相关

命令用途适用场景
git commit -m "feat: xxx"手动提交(需符合规范)熟悉规范后的快速提交
pnpm run commit交互式提交(cz-git 引导)新手/不确定格式时
echo "feat: test" | pnpm exec commitlint管道测试提交信息验证 commitlint 配置
git commit --amend修改上一次提交提交后发现错误
git log --oneline -n 10查看最近 10 条提交检查提交历史

版本发布相关

命令用途版本变化示例
pnpm run release:dry预览发版效果(不修改文件)无变化
pnpm run release:first首次发版(不尝试查找上一个 Tag)使用 package.json 中的版本
pnpm run release自动发版(根据 commit 计算)0.1.0 → 0.1.1 或 0.2.0
pnpm run release:minor强制发次版本0.1.5 → 0.2.0
pnpm run release:major强制发主版本0.2.0 → 1.0.0
pnpm run release:alpha发 alpha 预览版0.1.0 → 0.1.1-alpha.0
pnpm run release:beta发 beta 测试版0.1.1-alpha.0 → 0.1.1-beta.0
npx standard-version --release-as 2.0.0强制指定版本号任意 → 2.0.0

标签与推送

命令用途
git tag查看所有 Tag
git tag -d v1.0.0删除本地 Tag
git push --follow-tags origin main推送代码和 Tag 到远程
git push origin :refs/tags/v1.0.0删除远程 Tag
git describe --tags --abbrev=0查看最新的 Tag

撤销与修复

命令用途使用场景
git reset --hard HEAD~1撤销最后一次提交(硬重置)发版后发现严重错误
git reset --soft HEAD~1撤销提交但保留更改想重新编辑 commit message
git tag -d v1.0.0 && git reset --hard HEAD~1撤销发版(删除 Tag 和 commit)standard-version 执行后反悔
git commit --amend --no-edit补充文件到上一次提交漏提交了某个文件

13.7.3. 配置文件职责对照表

文件工具核心配置项作用
commitlint.config.jscommitlintrules, prompt定义提交规则 + cz-git 交互配置
.versionrc.jsonstandard-versiontypes, bumpFiles定义 CHANGELOG 格式和版本更新文件
.husky/commit-msgHusky钩子脚本在提交时触发 commitlint
package.jsonCommitizenconfig.commitizen.path指定 cz-git 适配器