都2024年了,你还在手动打tag嘛

479 阅读7分钟

前言

每次发版都需要基于发版分支去打一个新的tag。本着折腾()的原则,我准备写一个基于node.js的脚本程序,来代替手工打新tag。 开卷!因为真正的脚本代码涉及了一定的公司业务,所以本文的实现进行了一定程度的简化。

需求分析

在需求分析之前,我们先进行一些约定,方便大家更好的理解文章。以1.0.0.0这个tag为例子,1.0.0部分我们称之为大版本号,剩余的.0既是小版本。

开始分析需求,假设当前系统已有1.0.0.01.0.0.1两个tag。那么我们需要打的新tag号就是1.0.0.2

详细的步骤拆解:

  1. 读取package.json文件中的version属性,获取大版本号。
  2. 根据大版本号,去检索gittag列表,计算出新tag的小版本号。
  3. 创建新tag,并且推送到git远程仓库。
  4. 以上这一系列操作只需要通过一个脚本文件来触发执行。

很好就4步,我们接下来开始实现功能。

功能实现

项目初始化

第一步,搭建一下项目,新建名字为smart-tag的文件夹。在文件夹内打开cmd,执行npm init命令,然后一路回车即可。

image.png

第二步,在文件夹内新建两个文件夹

  • bin文件夹,在新建一个index.js文件,这是我们日后的执行文件。
  • scripts文件夹,用于放置后续的功能函数。

现在我们得到了一个如下所示的目录结构

image.png

执行文件

我们先在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文件的脚本(下图中有三个脚本,适用于不同的执行环境)。

imagepng

如下图则表示链接建立成功了。

imagepng

接下来可以通过tag++来执行bin/index.js,在终端中输入tag++可以看到如下结果

imagepng

解析命令行

我们已经有了执行文件,但是目前的执行文件还不能接受命令行输入参数。这里我们就需要使用到commander库来解析命令行,接收命令行参数,接下来我们安装commander。

npm install commander // 安装commander

接下来简单讲一下我们将使用到的两个函数versionoption,这里就不过多展开其他相关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]

  1. remoteName是指git远程仓库的名称。我们可以通过git remote命令获得
  2. 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…