ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

【码动四季·秋】从 commit 到发版全自动:Conventional Commits + semantic-release 发布流水线实战

【码动四季·秋】从 commit 到发版全自动:Conventional Commits + semantic-release 发布流水线实战 本文为 AtomGit 码动四季·开源同行征稿活动参与文章开源仓库的文件都就位之后我回头看了一眼 git 历史发现一个尴尬的事实仓库里的版本只有两个——“刚开始和现在”。中间 1287 次提交没有版本号没有 CHANGELOG想找哪次提交改了命名规则只能靠git log -S考古。不是我不想发版是每次发版的成本劝退了我核对这段时间改了什么、手写 CHANGELOG、决定版本号加几位、打 tag、写 release notes——全人工。上一次认真发版还是半年前之后改动越攒越多越发版不动。这篇讲我怎么把这个流程彻底交给机器提交规范怎么落地、semantic-release 怎么从 commit 自动推出版本号和 CHANGELOG、CI 怎么串起来、文档仓库非 npm 项目怎么适配。四个我真实踩过的坑也一并在第四节。本文要点Conventional Commits 结构化提交 · semantic-release 选型对比 · commitlint hook 强制落地 · 非 npm 仓库适配 · 基线 tag 处理旧历史 · 4 个真实踩坑 · 发版耗时 40-60 分钟 → 0 分钟适用版本semantic-release v24.x / commitlint v19.x / Conventional Commits 1.0.02026 年现行版本行为以官方文档为准一、发版之痛的根源commit message 不是写给人的是写给流水线的先看一段我仓库改造前的真实 commit 历史节选风格未做修饰a3f2c11 更新命名规则 8b1e04d fix 2c9d7f3 修改了一堆东西 d4e8a92 修复 mermaid 渲染问题顺便更新了两个规则文件 e7f1b33 feat: 新增图片管理规则首次尝试规范提交这种历史的致命问题不是难看是信息无法机器读取哪次是功能新增、哪次是缺陷修复、哪次破坏了兼容性——没有任何结构化信号。人读着费劲流水线更是无从下手。Conventional Commits 的解法是把 commit message 变成结构化数据type(scope): subject feat(rules): 新增图片管理规则 ← 功能新增 → 次版本号 1 fix(mermaid): 修复渲染背景色 ← 缺陷修复 → 修订号 1 feat!: 迁移到单一配置源 ← ! 表示 breaking → 主版本号 1type 与语义化版本SemVer的映射关系是整套机制的基石feat→ minor、fix→ patch、BREAKING CHANGE!或 footer→ major。commit 写规范了版本号就是推导题不是判断题。二、方案对比为什么选 semantic-release主流的三条自动化发版路线维度semantic-releaserelease-pleasechangesets版本决策全自动从 commit 推断人工确认 release PR人工写变更集CHANGELOG 质量完全由 commit 生成由 PR 标题与描述生成由手写变更集生成上手曲线陡插件体系 严格规范平缓中等Monorepo 支持弱一般强核心优势适用场景单包 纪律性强的提交想要人工把关的团队多包联动发版选型逻辑我的仓库是单包不需要 Monorepo 联动、单人维护人工确认 release PR 是给自己加活、且我已经决定把提交纪律管起来否则整套机制无意义。三条里 semantic-release 的短板——陡峭上手曲线、对提交规范的强依赖——对单人仓库恰恰不构成障碍。而它零人工的版本决策正是我要买的东西。一个容易被忽略的细节只有 chore/docs 类型提交时不会发版。这个行为对内容仓库很重要——改错别字、调格式不该消耗版本号。三、四步落地从提交规范到自动发版3.1 第一步提交规范落地commitlint hook规范光写在文档里没用必须用工具在提交入口强制。commitlint 负责校验 message 格式hook 负责提交那一刻拦截# 安装Node 18npminstall-Dcommitlint/cli commitlint/config-conventional// commitlint.config.js — 在 Conventional Commits 默认规则上做两点收紧module.exports{extends:[commitlint/config-conventional],rules:{// type 白名单允许 content内容仓库特有的内容更新类型type-enum:[2,always,[feat,fix,docs,content,refactor,chore,revert]],// subject 禁止空泛描述——中文 description 也必须有信息量subject-min-length:[2,always,8],},};# lefthook.yml — commit-msg hook 拦截commit-msg: commands: commitlint: run: npx commitlint--edit{1}content这个自定义 type 是文档仓库的适配点官方规范里文章更新只能挤进docs但我需要把内容更新新文章、数据修订和文档说明改 README、改规则说明区分开——两者对版本号的语义不同content参与发版docs不参与。3.2 第二步semantic-release 配置非 npm 仓库的关键适配semantic-release 默认假设你在发 npm 包。内容仓库不需要 npm 发布只需要 tag CHANGELOG Release插件按需裁剪// release.config.js — 文档仓库适配版module.exports{// 关键branches 配置。main 直发另开 beta 预发通道branches:[main,{name:beta,prerelease:true},],// 注意没有 semantic-release/npm —— 非 npm 仓库不需要plugins:[semantic-release/commit-analyzer,// 从 commit 推断版本semantic-release/release-notes-generator,// 生成 release notes[semantic-release/changelog,{changelogFile:CHANGELOG.md,// CHANGELOG 落成文件入库}],[semantic-release/exec,{// 发版成功的后置动作同步版本号到 README 徽标数据源prepareCmd:echo v${nextRelease.version} .version,}],[semantic-release/git,{// CHANGELOG 和版本文件随发布 commit 回写仓库assets:[CHANGELOG.md,.version],message:chore(release): v${nextRelease.version} [skip ci],}],],};3.3 第三步CI 接入与权限CI 侧要做两件事跑 commitlint 全量校验PR 里的每个 commit 都要合规以及发版 job。权限是最容易翻车的点第 2 个坑详述# .atomcode/ci/pipeline.ymlAtomGit CI结构同 GitHub Actionsstages:-lint-releasecommit-lint:stage:lintscript:-npx commitlint--from origin/main--to HEADsemantic-release:stage:releaseonly:[main]# 只有 main 触发发版script:-npx semantic-release# 环境变量GIT_AUTHOR_NAME / GIT_AUTHOR_NAME 等 CI 机器人身份# 以及有 contents 写权限的 token变量注入不落明文CI 触发后的完整调用时序——从 push 到 Release 通知各环节的责任边界3.4 第四步历史基线处理首次接入必做旧历史全是更新“fix这种不可解析的 messagesemantic-release 首次运行时从上个 tag 开始分析——而我的仓库没有上个 tag。它的默认行为是从第一个 commit 开始扫1287 个自由体” commit 扫出来的版本推断毫无意义。解法是先打一个基线 tag告诉流水线历史到此为止# 以当前状态为 v1.0.0 基线之后的 commit 才参与版本推导gittag-av1.0.0-m首次规范化发版基线gitpush origin v1.0.0这个动作还顺带解决了一个心理问题不用为旧历史不合规焦虑——基线之前的历史不参与解析规范只约束未来。基线处理在整条流水线里的位置一图看清——先打基线、规范只增量生效、旧历史静默隔离四、四个真实踩坑1. squash merge 把版本信号揉没了现象开启 PR 合并后用 squash——5 个feat/fix被压成 1 个 commit标题还是 PR 标题不含 type。那次 semantic-release 推断结果无发版实际该发 minor。根因squash 把多条结构化 commit 揉成一条非规范 message版本信号在合并环节丢失。解决squash 后的 commit 标题必须重写为合规的 Conventional Commit 格式或者干脆用 merge commit 保留原始历史。我在 PR 模板里加了合并前检查 commit 标题这一条。教训流水线读的是合并后的最终历史中间过程再规范最后一步变形就全白费。2. CI 机器人权限不足tag 打了 Release 没建现象第一版 CI 配置里 token 只有 push 权限semantic-release 走到创建 Release一步 403但 tag 已经推上去了——仓库陷入有 tag 无 Release的脏状态下一次运行还把 tag 当成已发版。根因发版动作需要 tag、Release、push 三种写权限只配了其中一种。解决给 CI token 显式开 Release 写权限并养成发版后去 Release 页确认的习惯。排查命令git tag -l与平台 Release 列表对账。教训权限是 CI 发版的第一故障源——三种权限一次配齐好过每次 403 再补。3. breaking change 逃逸现象重构.atomcode目录结构的那次提交实际是破坏性变更依赖旧路径的脚本全挂但 message 写成了refactor: 迁移配置目录——没有!也没有 BREAKING CHANGE footer版本号只走了 patch。根因破坏性变更依赖人在提交时自觉声明没有任何机制兜底refactor类型默认不携带 breaking 语义。解决commitlint 增加自定义规则refactor类型强制要求 scope且改目录/接口的提交一律要求BREAKING CHANGEfooter 人在回路确认。依赖该路径的自动化04 号的 CI job同步修正。教训!和 footer 不是格式洁癖是下游自动化的生命线——漏一次故障要到下游 CI 挂了才暴露。4. beta 通道的版本号先于 main现象预发通道发过v1.1.0-beta.1后来 feat 合进 main 正式发版时semantic-release 推出了v1.1.0——第一反应是版本号重复了差点手工干预。根因不了解 prerelease 语义——prerelease 与正式版的推导是隔离的v1.1.0-beta.1与v1.1.0是两个不同的版本。解决不干预信任流水线。正式发版v1.1.0正确覆盖了 beta 预告的语义。教训人不要碰版本号——理解不了流水线行为时先查文档而不是上手改。五、效果发版从半年一憋到随过随发指标接入前手写时代接入后自动化单次发版人工耗时40-60 分钟核对手写打 tag0CI 内 3 分钟无人值守版本发布间隔最长半年积压发不出有 feat/fix 即发周内完成CHANGELOG 覆盖率抽查约 60%凭记忆漏记100%由 commit 生成不靠记性版本号错误人工判断出现过跳号推导制零错误历史可追溯性git log -S考古CHANGELOG 直接定位改造后的发版流程变成合 PR → push → CI 自动分析 commit → 自动出 tag/CHANGELOG/Release → 我在手机上收到完成通知。人从发版流程里完全退场只在 breaking change 时被拉回确认。六、扩展与边界两个联动方向许可证合规检查可以作为一个前置 job 挂进这条流水线02 号文章的 DCO 校验就在 lint 阶段依赖升级 PR04 号 Dependabot与发版流水线的共存规则——升级 PR 的 commit 统一走chore(deps)前缀不污染版本号安全补丁单独标fix(deps)。适用边界说清楚这套全自动机制的前提是提交纪律由工具强制。如果团队里 commit message 无法统一历史原因、人员流动semantic-release 会把脏历史如实反映成混乱的版本号——那种场景建议先从 release-please 的人工确认模式起步纪律养好了再切全自动。七、总结这次改造下来值得记住的就四句话。commit message 是流水线的 API——写给六个月后的自己更是写给机器。首次接入先打基线 tag规范只管未来不为旧历史焦虑。tag、Release、push 三种权限一次配齐这是我用一次 403 换来的教训。!和 footer 不是格式洁癖漏一次故障要到下游 CI 挂了才暴露。现在我的仓库每次发版我只做一件事看通知。你上次发版花了多久评论区报个数。八、常见问题Q1单人仓库有必要上这套吗感觉是团队才需要的东西。恰恰相反单人仓库收益最大。团队发版好歹有人分担单人仓库的 CHANGELOG 全靠半年后的自己回忆——而回忆是最不可靠的。我接入的动机就是git log -S考古自己半年前的改动考了半小时没考到。这套配置装完之后发版这件事从我的待办清单里消失了。Q2commitlint 会不会太烦写个 commit 还要过校验。头两天确实烦第三天开始就是肌肉记忆了。真正被拦截的往往是你本来就该写清楚的fix后面到底修了什么、feat!有没有把破坏性影响写进 footer。我的经验是把subject-min-length设成 8——低于 8 个字的 subject 几乎必然没信息量。如果团队抵触强烈可以先只开 warning 不开 error观察两周拦截记录再收紧。Q3CHANGELOG 全由机器生成会不会可读性很差取决于 commit 写得好不好机器只是忠实的转录员。我的做法是subject 一句话讲清改了什么body 里补充为什么改和影响范围。semantic-release 生成的 CHANGELOG 按版本分组、按 type 归类feat/fix 分区展示——比手写的还整齐因为它不会漏、也不会偷懒。你的仓库上次发版是什么时候如果超过一个月了这篇的配置加起来 20 分钟能跑通值得试一次。真实性声明本文配置来自本仓库实际运行的流水线commitlint/lefthook/semantic-release/CI yml 均为在用版本发版耗时与 CHANGELOG 覆盖率数据来自 git 历史与 CI 日志统计四个踩坑为真实故障复盘。semantic-release 版本 v24.x行为如随版本变化以官方文档为准。参考资源Conventional Commits 规范semantic-release 官方文档SemVer 2.0.0commitlint 参考配置专栏导航上一篇开源许可证怎么选决策全过程下一篇用 AI 机器人治理开源仓库三件套落地(即将发布)专栏首页码动四季·秋季征稿系列如果本文对你有帮助欢迎点赞、收藏、转发。有任何问题或建议请在评论区留言交流。行文仓促定有不足之处欢迎各位朋友在评论区批评指正不胜感激。
返回列表