起因:
公司低代码平台用的是amis,由于内部有些依赖包固定写死了,并且每个包之间会有一些依赖关系,依赖版本号写死的,每次发包都要人工保证版本保持一致,带来很多没有必要的工作量,于是打算用个脚本处理这个问题,实现完全自动化。也是受到了同事的启发,同事用阿里工作流写了个脚本处理了手动更改完版本后续打包发布的流程,而每次手动改版本确实也会带来一些错误的产生,于是我来处理一些前置的操作。
原则
能用机器实现就不用人工参与,能用线上服务就不用本机
现存解决方案不用的原因
lerna version,尝试过,大概能实现根据依赖包更改版本的问题,但是公司内部定义的包的规则有些特殊,正式包后面也会跟一个后缀,类似3.0.0-xxx,使用lerna会将这个后缀去掉,暂时还没发现lerna有自定义后缀的功能
总结了自己写脚本的一些优势:
- 自己控制灵活一些,版本号和tag生成都更灵活
- 不过于依赖框架,比如别的项目不基于lerna,基本可以复用,改动不大
脚本功能包括:
- 根据当前分支判定哪个版本,alpha(开发),beta(测试),exp(正式)
- 判断有哪些文件更改了,识别出要发哪些包
- 获取包的最新版本 ,调整版本号
- 更改包以及依赖包的版本号
- 将最后更改过的包汇总出来,写入到一个文件中
- 提交变更的文件
- 创建tag
核心
node利用git,npm指令实现一些基本流程操作,通过阿里云工作流构建,接入钉钉机器人的消息推送
实现细节分析
-
添加执行指令
package.json添加执行脚本,我使用的ts,所以要用ts-node执行,"uc": "ts-node ./scripts/updateVersionAndCommit.ts" -
收集执行脚本的指令,
因为流水线执行的时候会传一些参数控制执行流程, 参数形式类似--new=true --pkgs=amis,amis-core这种形式 通过process.argv来获取指令的参数 获取到一个参数数组,内容如下
[
'项目根目录/node_modules/.bin/ts-node',
'项目根目录/scripts/updateVersionAndCommit.ts',
'--new'//这里就是后面追加的参数
]
所以先要获取到都有哪些参数,截取掉前面两个元素const args = process.argv.slice(2);
接下来用正则匹配出具体指令是什么,后续就是取值做一些逻辑判断,给process.env中添加一些标记,以便后续真正执行的时候做判断
const cmdRegexp = /^--\w+/; // 匹配 -- 开头的参数
const match = cmdRegexp.exec(args[i]);
-
获取当前分支
git指令是:git branch --show-current
开启子进程执行git命令:后续执行git命令都是使用的execSync这个方法执行的,同步执行,虽然感觉不太好,后续可以改成异步的方式,网上有类似实现库,比如之前看到的一个zx,google出的,实现十分简洁
const { execSync } = require('child_process');
const branch = execSync(`git branch --show-current`, { encoding: 'utf8' }).trim();
这里需要按照utf-8做编码转换,否则输出的是一堆乱码
-
找到需要更改的包
首先如果参数指定了要打哪些包,就不找了,直接用,否则就要根据提交的sha1值来获取到当前分支都变更了哪些文件。
这个sha1值可能来自于指令,如果没有指定则需要找最近的tag对应的sha1,这个tag有特殊意义,每次自动打包之后会写一个ci-auto开头的tag,这样下次打包的时候就可以推断上回打包到这次都有哪些文件变化了
获取最近一次的tag,对应的指令如下execSync(`git rev-list --tags='ci-auto-*' --max-count=1`);其中
rev-list这个指令是按照时间倒序列出提交对象,--tags='ci-auto-*'指定了以ci-auto开头的一些tag,最后max-count限制只返回一个
获取当前节点的sha1:git rev-parse HEAD通过diff指令,找到变更文件的文件名,
GIT_PAGER=cat git diff --name-only ${lastSha1} ${currentSha1}注意:需要加个
GIT_PAGER=cat否则git diff会直接进入vim模式,而不会把结果输出由于返回的文件路径有些有可能不是
packages包内的,直接过滤一下不是包名开头的路径就好了,用个set去重
const getChangePkgs = (fileNames: string[]): string[] => {
const set = new Set<string>()
fileNames.forEach(file => {
pkgs.forEach(pkg => {
if (new RegExp(`^packages/${pkg}/`).test(file)) {
set.add(pkg);
}
}
)
})
return Array.from<string>(set);
}
-
更新依赖包版本
因为不同子包之间有相互依赖关系,所以事先定义好依赖关系数组,按照从低到高排序
//按照依赖顺序,从低到高
const pkgs = ['amis-core', 'amis-formula', 'amis-theme-editor-helper', 'amis-editor-core', 'amis-editor', 'amis-ui', 'amis'];
然后将传过来的数组按照上面这个数组顺序排序,得到正确的依赖顺序
//首先按照依赖顺序排一下依赖包
const sortedDeps = depsPkgs.sort((prev, after) => {
return pkgs.indexOf(prev) - pkgs.indexOf(after);
})
之后在按照顺序更新每个依赖包
sortedDeps.forEach(pkg => {
updateDeps(pkg);
})
下面是核心更新依赖的方法,
主要思路:
- 如果没有指定包的版本,则自动获取最新包的版本,然后自增
- 遍历需要更新的依赖包数组,如果当前包就是指定的包,版本不同则更新
- 如果当前这个包的依赖项有匹配到指定包名称,并且版本不同,则更新到指定包的版本
- 指定的包更新了,当前包的版本也要更新一个版本
- 将更新了版本的包名和版本收集到一个map中,遍历map,递归更新其余的包版本,此处终止条件就是当前的包版本和指定包相等并且子依赖没有指定版本的包
个人感觉这种寻找相互依赖的找法还是比较优雅的,无须关心到底哪个包依赖了哪个包,循环依赖的问题暂时没测,感觉应该不存在
循环依赖场景:
比如依赖关系如下:
amis-ui依赖amis,amis又依赖amis-ui, 指定更新俩包[amis-ui,amis]
- 当前包
amis-ui, 指定包amis-ui, 先更新amis-ui,从1.0 —> 1.1, 收集到数组{ amis-ui:1.1}- 遍历收集数组,只有
amis-ui并且版本已经变更,所以递归结束- 当前包
amis,指定包amis-ui,发现amis-ui依赖版本,更新, 更新当前包 从2.0—>2.1 收集到数组{amis-ui:1.1 ,amis:2.1}- 然后遍历收集数组[amis-ui,amis]
- 当收集包
amis-ui遍历发现依赖项中有amis则替换- 当收集包
amis遍历发现有依赖项中是amis-ui的则替换 递归出口:包的版本相同,并且依赖项的包版本也不变
更新依赖包代码实现:
//在所有子包中更新对应的依赖版本
const updateDeps = (pkg: string, version?: string) => {
const deps = pkgs;
if (!version) {
version = plusVersion(getLatestVersion(pkg), versionType);
}
deps.forEach(dep => {
const jsonData = JSON.parse(fs.readFileSync(`${rootPath}/packages/${dep}/package.json`, 'utf8'));
// console.log(jsonData.name);
//更改自己
if (dep === pkg && version && version !== jsonData.version) {
jsonData.version = version;
collectPubDeps[jsonData.name] = version;
fs.writeFileSync(`${rootPath}/packages/${dep}/package.json`, JSON.stringify(jsonData, null, 2));
}
//更改依赖
if (jsonData.dependencies[pkg] && jsonData.dependencies[pkg] != version) {
const newPkgVersion = plusVersion(getLatestVersion(dep), versionType);
jsonData.dependencies[pkg] = version;
jsonData.version = newPkgVersion
collectPubDeps[dep] = newPkgVersion;
fs.writeFileSync(`${rootPath}/packages/${dep}/package.json`, JSON.stringify(jsonData, null, 2));
if (collectPubDeps) {
for (let dep in collectPubDeps) {
updateDeps(dep, collectPubDeps[dep]);
}
}
}
})
}
-
获取最新的版本号
团队内部有个约定,测试分支发包都是beta版本,开发分支发包都是alpha版本,发布分支发包都是正式版本
所以要根据所在的分支获取对应版本包的最新版
暂时通过npm view versions获取到所有的版本的包,然后进行过滤,暂时没找到如何直接通过npm的指令直接过滤掉某些规则的包。
实现如下:
const getVersions = (pkg: string) => {
const versions: string[] = JSON.parse(execSync(`npm view '${pkg}' versions --json --registry=公司内部源`, { encoding: 'utf8' }));
const filter = versions.filter(v => {
return v.includes(versionType)//VersionType就是,beta,alpha之类的字符串
})
return filter;
}
获取到包对应的所有版本之后,要从中选出版本号最大的那个,
因此,就需要对数组进行排序,
首先要对版本进行处理,去掉-beta这种字符方便比较,3.0.0-beta.1-->3.0.0.1
const getMaxVersion = (versions: string[]): string => {
const sorted = versions.sort((a: string, b: string) => {
const version1 = a.replace(/-\w+/g, '')
const version2 = b.replace(/-\w+/g, '')
return compareVersion(version1, version2);
});
return sorted[sorted.length - 1]
}
此时还会用到leetcode经常出现的一道题,比较版本号 leetcode.cn/problems/co… 官方双指针解法:
const compareVersion = (version1: string, version2: string) => {
const v1 = version1.split('.');
const v2 = version2.split('.');
for (let i = 0; i < v1.length || i < v2.length; ++i) {
let x = 0, y = 0;
if (i < v1.length) {
x = parseInt(v1[i]);
}
if (i < v2.length) {
y = parseInt(v2[i]);
}
if (x > y) {
return 1;
}
if (x < y) {
return -1;
}
}
return 0;
}
-
根据需要版本号增加1
这俩要分两种情况,
- 一个是更改第三位版本号,
3.0.1-->3.0.2 - 一个是更
-bata后面的版本号,beta.0-->beta.1
通过正则匹配的方式实现自增替换 更新第三位正式版本实现方式如下:
matchRegex = /(\d+)(?=\-)/; //这个正则代表的意思是数字后面必须带一个“-” 并且不匹配上这个“-”
const newVersion = version.replace(matchRegex, (match) => {
//这里的match是匹配到的数字,将它直接增1
return `${parseInt(match) + 1}`;
});
如果是普通beta版本自增的情况
matchRegex = /(\d+)(?!.*\d)/; //表示数字之后不能再有任何别的数字
const newVersion = version.replace(matchRegex, (match) => {
return `${parseInt(match) + 1}`;
});
-
打tag
目的就是每次打包之后自动生成一个ci-auto开头的tag,下次方便从该tag查找变化 执行的指令如下:
execSync(`git tag -a ${tag} -m "ci自动生成打包"`);
execSync(`git push origin ${tag}`);
-
获取changeLog
git log HEAD ^{sha1} 代表从某个提交点之后到当前的提交记录,获取到之后分割为数组,过滤出有效的提交记录
实现如下
const msg = execSync(`git log --pretty=format:%s HEAD ^${fromSha1}`, { encoding: 'utf8' }).trim();
const msgArr = msg.split('\n').filter((m: string) => m.startsWith('fix') || m.startsWith('feat') || m.startsWith('chore'));
-
发送钉钉机器人消息
添加机器人
首先在群里添加一个机器人,然后安全设置需要至少选择一种,自定义关键字还是加签,或者ip地址,为了方便起见,直接选择关键字触发, 如果是加签需要再装一下加密的依赖库,相对麻烦,参考文档:open.dingtalk.com/document/ro…
流水线配置机器人
之后新建一个流水线,在流水线配置好机器人的变量
调用机器人hook链接发送消息
最后直接在脚本获取到变量,然后通过ajax进行调用就行了,注意发送的消息中要包含关键字
const { ROBOT_HOOK } = process.env;
还能获取到一些辅助的其他信息如:PIPELINE_NAME, BUILD_EXECUTOR, BUILD_MESSAGE, DATETIME, BUILD_REMARK
如果自定义信息可能会用到
注意:现在机器人的消息支持的markdown语法很有限,不支持比较复杂的,比如table
仅支持markdown的子集
目前支持的markdown格式如下:
标题
# 一级标题
## 二级标题
### 三级标题
#### 四级标题
##### 五级标题
###### 六级标题
引用
> A man who stands for nothing will fall for anything.
文字加粗、斜体
**bold**
*italic*
链接
[this is a link](http://name.com)
图片(建议不要超过20张)

无序列表
- item1
- item2
有序列表
1. item1
2. item2
参考文档:open.dingtalk.com/document/co…
总流程图
graph TD
A([获取分支])-->B[根据分支判断使用版本号]
B-->C{是否指定依赖包?}
C--是-->D[更新对应依赖版本]
C--否-->E{是否指定sha1}
E--是-->G[当前sha1与指定sha1做比对 找到变更文件]
E--否-->H[拉取最近的tag获取其sha1]
H-->G
G-->I[根据变更的文件判断哪些依赖有变更]
I-->D
D-->F[将收集的依赖包数组写入到文件]
F-->L{是否有更改的文件}
L--是-->J[提交变更的文件]
L--否-->M
J-->M[获取changeLog]
M-->K[打tag]
K-->P[发送机器人消息]
P-->O([结束])
待完善功能:
- 版本控制增加marjor minor patch区分
- 指定包版本支持精确指定固定版本而不是只指定包,例如可以指定amis=3.0.1
- 根据分支打对应分支的tag,每次拉取对应分支的最新tag标记
- 变更记录写入到文档里
相关链接:
- 正则测试:regex101.com/
- git官方文档:git-scm.com/book/zh/v2
- zx指令库: github.com/google/zx