一个好的项目通常都是多人合作的结果,当你在一个版本迭代后,想要对本次迭代复盘,了解哪些是新增功能点,哪些是项目原有功能的优化,你还在依赖翻阅 gitlab/github 的 history 记录来复盘吗?
2021年了,对这种繁琐且没有统计归类的复盘说 NO!
当前版本发布后,你想要让大家能及时了解到项目迭代内容,收到项目迭代推送,你还在手动组装语句,一个一个发送到你想要通知的 IM 里吗?如果需要通知的 IM 比较多,会有未通知到和阐述不准确的情况;同时阐述的模板不一致,阐述可能也无法具体到哪个项目哪个分支哪个版本;信息自动化时代,我们怎样做到定向精准投送呢?
一份友好地更新日志(CHANGELOG.md),让用户和开发人员可以更好的知道每一个版本有哪些改动,是新增功能点还是项目原有功能的优化;同时在项目复盘时,更新日志提供了直观的复盘依据,方便快速浏览。
有了规范的更新日志,一个月后的你依然记得自己在某个迭代版本做了哪些工作。
规范的更新日志,对大家的 git commit message 做到了统一约束,统一 git commit message 提交方式使项目迭代内容更趋于工程统一化,一目了然。得物前端团队已经产出相应的实时提交约束工具库,约束遵循 Angular 规范,链接指向👉 https://github.com/angular/angular/blob/master/CONTRIBUTING.md#commit
提交约束规范如下:
<type>[optional scope]: <subject>
type具体类别如下:
feat:新功能(feature)
fix:修补bug
docs:文档(documentation)
style: 格式(不影响代码运行的变动)
refactor:重构(即不是新增功能,也不是修改bug的代码变动)
test:增加测试
chore:其他修改, 比如构建流程, 依赖管理
使用示例:
feat: 支付二清商家入驻流程
项目发布后,为了让大家感知项目迭代内容,这时就需要统一规范的发布模板,外加一个能够自动实时通知的机器人帮你干这些累活,通知到你想要发布的IM。基于目前团队使用的 IM 是飞书,接入了飞书机器人,当项目发布后触发机器人,定向发布通知,做到即时通知。
从上述两个出发点,产出了内部工具库 @du/changelog-robot 。该库基于成熟的 conventional-changelog,根据本地 tags 归类生成对应的 CHANGELOG.md;并将更新日志原样输出给飞书机器人,实时通知到对应群组。
二、整体方案架构图
【1.1】*
在用户 npm publish 的过程中,主要涉及 publish 过程中的两个钩子,prepublishOnly 和 postpublish 。
有了相应的钩子,我们就可以针对钩子触发的时间节点,对整个功能做大致分配。项目发布前生成CHANGELOG.md,项目发布后实时通知到对应群组。
如图【1.1】,整体方案分为2大模块,生成 CHANGELOG.md 模块和飞书机器人通知模块,两个模块独立存在,命令使用不会互相影响。
- 生成 CHANGELOG.md 模块:该模块主要在 conventional-changelog 开源包的基础上,解决多人协同开发导致的 CHANGELOG.md 内容紊乱,并依据 npm version xxx 原理新增自动提交 CHANGELOG.md 功能。
- 实时通知模块:该模块主要结合飞书机器人 api,把生成的 CHANGELOG.md 内容原样的通知到对应的飞书群。
三、方案实现
为了在项目发布前自动生成所需的 CHANGELOG.md 文档,并且在项目成功发布后实时自动在飞书群里进行通知,在调研 conventional-changelog 和飞书机器人后,设计了一套解决方案。方案分2个大模块,生成 CHANGELOG.md 模块和飞书机器人通知模块。
怎样生成 CHANGELOG.md
conventional-changelog 是一个成熟的工具包,用于根据模板生成相应的 CHANGELOG.md 。
conventional-changelog 生成文件流,主要依赖 git log ,获取对应 tag 下的所有 commit 信息,具体原理如下:
1、获取当前仓库下的所有 tags
var reverseTags = context.gitSemverTags.slice(0).reverse()
2、形成可读流
var streams = reverseTags.map((to, i) => {
const from = i > 0
? reverseTags[i - 1]
: ''
return commitsRange(from, to)
})
3、commitsRange 方法是形成可读流的关键方法,方法通过 git log,根据你设置的模板生成对应的信息;其中args为数组。
- args[0]: "log"
- args[1]: "--format=%B%n-hash-%n%H%n-gitTags-%n%d%n-committerDate-%n%ci%n-authorName-%n%an%n-authorEmail-%n%ae%n" // git log 模板
- args[2]: "v1.2.5" // 对应的tag号
- args[3]: "--no-merges"
var child = execFile('git', args, {
cwd: execOpts.cwd,
maxBuffer: Infinity
})
经过上面一段代码,实际上是在控制台执行:
git log --format=%B%n-hash-%n%H%n-gitTags-%n%d%n-committerDate-%n%ci%n-authorName-%n%an%n-authorEmail-%n%ae%n 换成你自己的版本号 --no-merges
会得到当前 tag 下的所有提交信息:
1.2.30
-hash-
3c5949d1db635dcfe6fa4dc0331b9003ca8f091c
-gitTags-
(tag: v1.2.30)
-committerDate-
2020-10-25 14:33:03 +0800
-authorName-
chengli
-authorEmail-
xxx@youremail.com
fix: 测试changelog老功能-npm打tag,1.2.30,基于1.2.29打过tag
-hash-
d60c34320bff8fc807e4decd139755bd4b4c07a4
-gitTags-
-committerDate-
2020-10-25 14:32:56 +0800
-authorName-
chengli
-authorEmail-
xxx@youremail.com
....
这些信息形成对应的可读流 streams,如图【2.1.1-1】。
下面为了描述方便 以cc代替conventional-changelog。
cc 是根据git logs,和git commitMsg,自动生成版本信息;所以 cc 想要正常生成版本信息有个重要前提 —— git commitMsg要符合提交规范 && git logs有效。
cc 预设了很多配置项,详细可以看https://github.com/conventional-changelog/conventional- changelog/tree/master/packages/conventional-changelog-core
- cc 首先执行 git log --pretty ,拿到本地所有的git 记录, 所以数据源是git logs。
- 通过thorugh2这个库,创建一个转换流, 将可读流pipe到转换流里。每次往可读流里push commitMsg数 据,自动触发转换流的_transform。如果我们在初始化传入了自定义的transform函数,会执行transform。
- 没有传入使用默认transform函数,默认根据git tag标签对commit 分组 。
- 内部根据semver.valid 校验版本号。可配置具体参数支持提取lerna格式的版本和提交内容,对于不符合格式的commit会忽略。
- cc的模版渲染引擎使用的是handlebar,渲染成md文件格式。
- 将组装好的版本commit信息 再次推送到一个新的转换流里,用handlebar处理成md格式数据。
- cc最后返回一个转换流,只需要配置写流,就可以源源不断的生成changlog数据 。
- http://nodejs.cn/api/stream.html
const changelogStream = conventionalChangelog({
preset: 'angular', // 预设的changelog类型
warn: function (warning) {
console.log('warning; ', warning)
},
append: false, // a+
releaseCount: 0, // 0全部重新生成
transform: function (commit, cb) {
if (typeof commit.gitTags === 'string') {
var match = rtag.exec(commit.gitTags)
rtag.lastIndex = 0
if (match) {
commit.version = match[1] // 版本号需要符合规则 xx.xx.xx这种格式
}
}
if (commit.committerDate) {
commit.committerDate = moment(new Date(commit.committerDate)).format('YYYY-MM-DD HH:mm:ss') //时间格式 字段=内置模版占位符,或者自定义扩展模版
}
return cb(null, commit)
}
})
changelogStream.pipe('./changelog.md')
【2.1.1-1】*
可读流进行一些列的 parse,最终组装成图【2.1.1-2】的数据格式:
const changelogStream = conventionalChangelog({
preset: 'angular', // 预设的changelog类型
warn: function (warning) {
console.log('warning; ', warning)
},
append: false, // a+
releaseCount: 0, // 0全部重新生成
transform: function (commit, cb) {
if (typeof commit.gitTags === 'string') {
var match = rtag.exec(commit.gitTags)
rtag.lastIndex = 0
if (match) {
commit.version = match[1] // 版本号需要符合规则 xx.xx.xx这种格式
}
}
if (commit.committerDate) {
commit.committerDate = moment(new Date(commit.committerDate)).format('YYYY-MM-DD HH:mm:ss') //时间格式 字段=内置模版占位符,或者自定义扩展模版
}
return cb(null, commit)
}
})
changelogStream.pipe('./changelog.md')
【2.1.1-2】*
拿到了数据根据 hbs 模板生成符合 markdown 规范所需的流。
*{{#if scope}} **{{scope}}:**
{{~/if}} {{#if subject}}
{{~subject}}
{{~else}}
{{~header}}
{{~/if}}
{{~!-- commit link --}} {{#if @root.linkReferences~}}
([{{shortHash}}](
{{~#if @root.repository}}
{{~#if @root.host}}
{{~@root.host}}/
{{~/if}}
{{~#if @root.owner}}
{{~@root.owner}}/
{{~/if}}
{{~@root.repository}}
{{~else}}
{{~@root.repoUrl}}
{{~/if}}/
{{~@root.commit}}/{{hash}})) by: {{authorName}} {{committerDate}}
{{~else}}
{{~shortHash}}
{{~/if}}
{{~!-- commit references --}}
{{~#if references~}}
, closes
{{~#each references}} {{#if @root.linkReferences~}}
[
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}#{{this.issue}}](
{{~#if @root.repository}}
{{~#if @root.host}}
{{~@root.host}}/
{{~/if}}
{{~#if this.repository}}
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}
{{~else}}
{{~#if @root.owner}}
{{~@root.owner}}/
{{~/if}}
{{~@root.repository}}
{{~/if}}
{{~else}}
{{~@root.repoUrl}}
{{~/if}}/
{{~@root.issue}}/{{this.issue}})
{{~else}}
{{~#if this.owner}}
{{~this.owner}}/
{{~/if}}
{{~this.repository}}#{{this.issue}}
{{~/if}}{{/each}}
{{~/if}}
剖析了 conventional-changelog 生成 CHANGELOG.md 主要核心代码,但是当前开源的 conventional-changelog 库并不能满足需求,它没有对生成的 CHANGELOG.md 文件做提交处理,对多人协作同一个分支的项目没有很好的同步版本 tags,对于需要 npm publish 的项目,没有对用户手动更改 version 进行校验,这些问题会导致生成的 CHANGELOG.md 内容紊乱,达不到用户想要的效果。
在对相关的工具包做调研后,根据自己需要实现的功能,总结当前的工具包满足需求的程度,基于现有工具包完善功能,并根据用户需求进行迭代优化。
工具包迭代历程:
1、用户手动更改 version 能 publish 成功,但是对应的 commit message 挂载版本号紊乱:
- 第一次提交信息后,手动更新 package.json 的 version,由于没有走正常发布流程,当前发布没有生成 tag,生成的 changelog.md 如下图【2.1.1-3】:
【2.1.1-3】*
- 基于图【2.1.1-3】,第二次提交信息后,手动更新 package.json 的 version ,生成的 changelog.md 如下图【2.1.1-4】,由于手动更新 version,没有 tags 区分记录,所以两个版本的提交信息都融合在了一起,挂在当前 version 上。如果用户每次都手动更新 version,之后所有的 commit message 都会揉到一起挂载在当前 version,直到中间有打tag把提交信息做分界。
【2.1.1-4】*
- 用户通过 npm version xxx,打上 tag,之前未打过 tag 的版本提交内容,全挂在当前 tag 上,当前版本有了 tag。
如图【2.1.1-5】:
【2.1.1-5】*
- 基于上一个打过 tag 的版本,进行正确的版本提交,即 npm version xxx,此时内容会基于 tag 归类展示,是我们想要达到的效果,如图【2.1.1-6】:
【2.1.1-6】*
解决方案:通过上面几个分析,得出结论,必须通过 npm version xxx 打 tag ,commit message 才会正确归类展示。
2、正确生成 changelog.md ,用户执行 npm publish 后,changelog.md 文件存在在本地暂存区,用户需要再次提交,如图【2.1.1-7】:
【2.1.1-7】*
解决方案:类似 npm version xxx 的原理,集成到工具包中,自动帮用户提交信息。
3、同一个分支多人协同开发,本地 tags 不同步,导致相同的 commit message 内容在 changelog.md 中归类混乱,如图【2.1.1-8】:
【2.1.1-8】*
解决方案:生成 changelog.md 之前,拉取远程 tags ,生成后推送本地 tags。
整体架构图
主要依赖 conventional-changelog 开源包的功能,在生成前期对项目版本号和 tags 做校验,并且同步本地和远程。引导用户在发布前生成符合规范的 version,并自动帮用户提交记录内容。
- 整体方案设计如图【2.1.2-1】所示:
【2.1.2-1】*
工作流程图
方案是一个指引,就像人体的骨架,支撑了整个架构。有了整体的设计方案,接下来就需要设计代码流程,实现想要的功能,落地 idea。
- 工作流程如下图【2.1.3-1】
【2.1.3-1】*
完整效果图
整体流程下来,控制台会出现交互式命令,供用户规范生成版本号并打 tag,如图【2.1.4-1】:
【2.1.4-1】*
根据 npm version 规范,选择生成需要的版本号,成功生成 CHANGELOG.md ,如图【2.1.4-2】,生成后的文件会自动帮用户暂存并提交(此处功能参考了 npm version xxx 的逻辑,黑盒处理暂存区内容),如图【2.1.4-3】:
【2.1.4-2】*
【2.1.4-3】*
如何在 npm publish 的时候自动唤起飞书机器人针对群组进行通知,需要我们组合飞书开发者 api,形成一个类,完成一系列的操作。
怎样对接飞书机器人
根据飞书开发者文档,用户可创建自己的机器人,文档指引:飞书开放平台(https://open.feishu.cn/document/uQjL04CN/uYTMuYTMuYTM)。
有了机器人,我们需要按照 CHANGELOG.md 的样式,通知到群组。
需要查阅飞书机器人通知消息api,选择符合要求的api进行展示通知,依据 api 的入参格式,就需要对 conventional-changelog 生成的流进行组合处理,对流组装成符合的格式。
飞书的消息卡片支持部分 markdown格式(https://open.feishu.cn/document/ukTMukTMukTM/uADOwUjLwgDM14CM4ATN)。
拿到飞书机器人的 APP_ID、APP_SECRET、APP_VERIFICATION_TOKEN ,写一个 Robot 类,实现对接功能。同时用户可对通知的群做特定配置。
整体架构图
整体方案架构图如图【2.2.2-2】:
【2.2.2-1】*
工作流程图
配合方案设计图,做如下代码流程设计,如图【2.2.3-1】:
【2.2.3-1】*
完整效果图
postpublish 钩子触发通知后的效果如图:
【2.2.3-1】*
具体配置参考:
以工具包的发布为
{
...
"scripts": {
...
"prepublishOnly": "changelog",
"postpublish": "robot" // 当通知到对应群组,配置"robot [真·B端战友群,xxxx]"
...
},
...
}
目前前端组工具库基本接入 @du/changelog-robot,实现项目发布实时通知,并生成规范 CHANGELOG.md。
- 从 Commit 规范化到发布自定义 CHANGELOG 模版:(https://juejin.cn/post/6844903888072654856#heading-10)
- AngularJS Git Commit Message Conventions:(https://docs.google.com/document/d/1QrDFcIiPjSLDn3EL15IJygNPiHORgU1_OOAqWjiDU5Y/edit#heading=h.uyo6cb12dt6w)
- conventional-changelog:(https://github.com/conventional-changelog/conventional-changelog/blob/master/packages/conventional-changelog-core/README.md)
- inquirer:(https://www.npmjs.com/package/inquirer)
- Promise - Q:(https://www.npmjs.com/package/q)
- read-pkg-up:(https://www.npmjs.com/package/read-pkg-up)
- node api:(http://nodejs.cn/api/)
*文/莉莉橙&fog