前言
每次发版都需要基于发版分支去打一个新的tag
。本着折腾(卷)的原则,我准备写一个基于node.js的脚本程序,来代替手工打新tag。
开卷!因为真正的脚本代码涉及了一定的公司业务,所以本文的实现进行了一定程度的简化。
需求分析
在需求分析之前,我们先进行一些约定,方便大家更好的理解文章。以1.0.0.0
这个tag
为例子,1.0.0
部分我们称之为大版本号,剩余的.0
既是小版本。
开始分析需求,假设当前系统已有1.0.0.0
、 1.0.0.1
两个tag
。那么我们需要打的新tag
号就是1.0.0.2
。
详细的步骤拆解:
- 读取
package.json
文件中的version
属性,获取大版本号。 - 根据大版本号,去检索
git
的tag
列表,计算出新tag
的小版本号。 - 创建新
tag
,并且推送到git
远程仓库。 - 以上这一系列操作只需要通过一个脚本文件来触发执行。
很好就4步,我们接下来开始实现功能。
功能实现
项目初始化
第一步,搭建一下项目,新建名字为smart-tag
的文件夹。在文件夹内打开cmd,执行npm init
命令,然后一路回车即可。
第二步,在文件夹内新建两个文件夹
bin
文件夹,在新建一个index.js文件,这是我们日后的执行文件。scripts
文件夹,用于放置后续的功能函数。
现在我们得到了一个如下所示的目录结构
执行文件
我们先在index.js
文件中写一个简单的测试方法,代码如下。
#!/usr/bin/env node
// 注明是node环境
const main = () => {
console.log('this is main function');
}
main()
// 输出
// this is main function
但是不可能日后每次都通过node来执行这个脚本,我们需要给这个脚本配置一个快捷方式,那么便需要使用到bin
属性。简单解释一下bin
属性的作用。当我们设置好了bin
属性,日后安装这个包的时候会创建一个命令名到本地执行文件的链接,bin
属性的key
就是日后的命令名,value
是调用执行文件的路径位置。
- 如果是全局安装的情况下,会在
npm
的全局包目录下生成连接; - 如果是局部安装的情况下,会在
node_modules
的.bin
目录下生成链接。 以下是package.json
具体配置:
{
"name": "smart-tag",
"version": "1.0.0",
"description": "自动发布新tag",
"main": "index.js",
"scripts": {
"test": "echo "Error: no test specified" && exit 1"
},
"bin": {
"tag++": "./bin/index.js"
},
"author": "zwh",
"license": "ISC"
}
接下来我们来测试一下这个快捷方式好不好使,只需要在工程root目录下执行一下npm link
命令。目的是建立一个全局链接。可以在npm全局包的目录找到这个链接(你可以简单的理解npm link
命令的执行相当于我们全局安装了smart-tag
这个包)。同时生成调用index.js
文件的脚本(下图中有三个脚本,适用于不同的执行环境)。
接下来可以通过tag++
来执行bin/index.js
,在终端中输入tag++
可以看到如下结果
解析命令行
我们已经有了执行文件,但是目前的执行文件还不能接受命令行输入参数。这里我们就需要使用到commander
库来解析命令行,接收命令行参数,接下来我们安装commander。
npm install commander // 安装commander
接下来简单讲一下我们将使用到的两个函数version
、option
,这里就不过多展开其他相关api了,感兴趣的同学可以去详细看看commander。
version
主要用于输出当前程序的版本。
commander
.version('0.0.1', '-v, --version')
.parse(process.argv)
// 执行
tag++ -v
// 输出
0.0.1
option
- 用于配置命令行的自定义参数
option('-n, --name <items1> [items2]', 'name description', 'default value')
该函数接收三个参数,第一个接收的是自定义的命令行参数,<>内包含的是必填参数,[]包含的是可选参数。第二个接受的是参数描述,在使用--help查看会显示这些描述。第三个参数是默认值,可选参数。
// option('-n, --name <items1> [items2]', 'name description', 'default value')
commander
.option('-p, --production', 'tag auto ++')
.parse(process.argv)
使用commander
我们实现一个简单的命令行解析,在bin
文件夹下的index.js文件中写下以下代码:
const { Command } = require("commander"); // 引入commander库
const { autoTag } = require("../scripts/index"); // 引入autoTag
const commander = new Command(); // 初始化命令行实例
commander
.version("0.0.1", "-v, --version") // 输出版本命令
// -p命令,我们后续需要监听处理的命令参数
.option("-p, --production", "tag auto ++")
.parse(process.argv);
const autoTag = (commander) => {
// opts方法是获取当前命令输入了那些参数
const option = commander.opts();
console.log("option ->", option);
};
autoTag(commander);
// 执行
tag++ -p
// 输出
option -> { production: true }
很好,我们已经掌握了解析命令行的能力,接下来我们准备开始打tag。
自动打tag
动态计算tag号
之前需求分析模块分析过,tag
由大版本号和小版本号组合得到:
- 通过读取
package.json
文件的version
属性获得大版本号。 - 需要通过检索
git
中已有的tag
来动态计算获得小版本号。
读取package.json获取大版本号
读取package.json
文件首先需要用到读取文件的api,这里我们使用fs
模块的readFile
函数。
readFile(path, options, callback): void
readFile
函数可以接收三个参数:
- 读取文件完整路径。
- 文件编码格式,允许传入编码格式字符串或者对象多种等类型,我们这里使用utf-8。
- 文件读取结束的回调函数。
我们这里限制一下,tag++
命令必须在项目目录的root目录下执行才有效,使用package.json
文件的完整路径作为命令行的执行路径。(如果你想去除这个限制,可以自己来实现这个功能,通过参数来接受自定义的路径。)
我们可以在scripts
文件夹下面建立第一个文件fileInfo.js
,写下如下代码:
const fs = require('fs')
const getFileVersion = () => {
return new Promise((resolve, reject) => {
// 获取当前执行命令的终端所处路径
const execPath = process.cwd()
// 读取package.json文件
fs.readFile(`${execPath}/package.json`, 'utf-8', (err, data) => {
if (err) {
reject(err)
}
// 读取成功,解析json文件,获取version属性
const json = data ? JSON.parse(data) : { version: void 0 }
resolve({
version: json.version,
})
})
})
}
module.exports = {
getFileVersion
}
这样我们就获得了大版本号。
动态计算小版本号
通过git tag -l 1.0.0.*
命令可以获取包含1.0.0
的所有tag。只需要结合exec
就可以在脚本中执行命令行命令。
const { exec } = require('child_process')
/**
* 获取包含当前所打tag的所有tag号
* @param {string} version 当前所打tag的前置版本
* @returns {string}
*/
const getAllTags = (version) => {
return new Promise((resolve, reject) => {
exec(`git tag -l ${version}.*`, (err, stdout) => {
if (err) {
outputLog('请检查package.json的version', err)
reject(err)
}
resolve(stdout ?? '')
})
})
}
// 所有包含1.0.0的tag号
1.0.0.1
1.0.0.2
1.0.0.3
1.0.0.4
1.0.0.5
1.0.0.6
1.0.0.7
1.0.0.8
通过命令行我们得到了一个tag
的数组,接下来我们对这个数组的每一项进行切割计算,来拿到当前最大的小版本号。
/**
* 获取当前所打tag的最大小版本
* @param {string} version 当前所打tag的前置版本
* @returns {number} 返回最新的tag小版本
*/
const getNewSubVersion = async (version) => {
// 获取当前大版本的所有tag
const tagStr = await getAllTags(version)
// 如果为搜索到,表示还没有
if (!tagStr) return 0
// 计算当前已创建的最大小版本号,并+1
let maxNumber = 0
const list = tagStr?.split('\n')
for (const tag of list) {
if (!tag) continue
const numberList = tag?.split('.')
const number = numberList?.pop()
if (tagNum > number) {
maxNumber = tagNum
}
}
return maxNumber + 1
}
最后我们只需要把计算的大版本号和小版本号组合起来,就可以得到最终要打的tag
号。
创建本地tag号
通过git命令行git tag -a [tag] -m [comment]"
结合之前使用到的exec
模块,便可以很快的完成一个创建tag
的方法。
/**
* 创建本地tag
* @param {string} tag 需要打tag的版本号
*/
const gitTag = async (tag, comment) => {
return new Promise((resolve, reject) => {
const cmd = `git tag -a ${tag} -m "${comment ?? tag}"`
exec(cmd, (error, stdout) => {
if (error) {
outputLog('创建本地tag失败', error)
reject(error)
}
resolve(stdout ?? '')
})
})
}
远程推送tag
推送tag
的git命令是git push [remoteName] [tag]
remoteName
是指git
远程仓库的名称。我们可以通过git remote
命令获得tag
既是之前我们计算的tag
号。
/** 获取远程仓库名称 */
const getRemoteName = async () => {
return new Promise((resolve, reject) => {
const cmd = 'git remote'
exec(cmd, (err, stdout) => {
if (err) {
outputLog('获取远程仓库名称失败', err)
reject(err)
}
resolve(stdout ?? 'origin')
})
})
}
/** 推送tag到远程仓库 */
const pushTag = async (tag) => {
let remoteName = await getRemoteName()
remoteName = remoteName?.replace('\n', '')
return new Promise((resolve, reject) => {
const cmd = `git push ${remoteName} ${tag}`
exec(cmd, (error, stdout) => {
if (error) {
outputLog('推送tag到远程仓库异常', error)
reject(error)
}
outputLog('已提交新tag', `${tag} comment: `)
resolve(stdout ?? '')
})
})
}
尾声
文中如有错误或不严谨的地方,请给予指正,十分感谢。
代码地址
以下是这个自动tag脚本的全部代码,感兴趣的小伙伴可以去看看。github.com/zwh0216/sma…