node+jenkins 实战前端发布平台——自动化部署

8,960 阅读11分钟

声明:文章为稀土掘金技术社区首发签约文章,14天内禁止转载,14天后未获授权禁止转载,侵权必究!

哈喽!这篇文章来到了整个前端发布平台的核心——自动化部署的实现。本文将分享通过后端跟jenkins之间交互,完成前端项目的自动化部署的业务功能。笔者手把手“教”学,大家跟着走~let‘go!

系列文章:

  1. 总览前端自动化部署流程,如何实现前端发布平台?文章链接
  2. 前端发布平台 node server 实战!文章链接
  3. 前端发布平台 jenkins 实战!如何实现前端自动化部署?
  4. 前端发布平台全栈实战(前后端开发完整篇)!开发一个前端发布平台。文章链接
  5. websocket 全栈实战,实现唯一构建实例 + 日志同步文章链接

本文是第三篇「前端发布平台 jenkins 实战!如何实现前端自动化部署?」的实战记录分享。主要通过分享 node serverjenkins 之间的交互,实现核心的自动化部署功能。

主要分享以下几点:

  • 后端配置 jenkinsjenkins job
  • 后端发起构建,调用 jenkins job 执行
  • 后端获取 jenkins 的构建日志
  • 后端发起停止构建
  • jenkins job 在各执行阶段如何通知 node server

本文用到的 npm 包:

  • npm jenkins。根据 jenkins openApi 包装了基础的功能。 npm 地址

快速看源码

注意⚠️:代码提交的时候,笔者把 jenkins 的域名、token给去掉了,如果是要在本地跑起来并成功调用构建的话,需要在 /jenkins/index.js 中配置自己的 jenkins 配置:

const config = {
  user: '',
  token: '',
  instance: '',
  assignedNode: ''
}

const jenkins = createJenkins({
  baseUrl: `http://${config.user}:${config.token}@${config.instance}`,
  promisify: true,
})

一、动态配置 Jenkins job

回顾一下专栏第一篇文章,笔者在那简单介绍了 freestyle job 的配置,但是那时候笔者是在 jenkins 的可视化界面中操作的。试想一下,如果我们要实现发布平台,那这份配置一定是要通过平台侧完成的,总不能我们在发布平台构建前还要跑去 jenkins 中配置 job 吧?

1. 如何配置?

首先一起分析一下动态配置 job 需要平台侧实现什么?笔者在 job 的配置面板中截了个图,很直观的可以看出,其实整个job的配置模板是固定的。我们仅仅是往表单的一些配置项中填入信息而已,所以我们可以把模板搞出来,然后挖空来填一些动态的内容(如构建分支、是否监听 push event、构建完成后执行xxx)。 image.png

文章的开头有提到本文会使用 npm jenkins 包,所以笔者决定通过它的文档来介绍「如何动态配置 jenkins job ?

  1. 首先看创建 job 的 apiimage.png
  2. 再看更新 job 的 api
    image.png

大家都能看出,笔者在2张图中都圈出一个关键点:XML 。显然,如果我们需要在后端去 生成动态配置 job ,这个 XML 都是一个关键的存在,那它是一个什么东西来的呢?其实就是我们在可视化界面配置完之后的配置产物。那接下来,我们就去找找这个 XML 到底在哪里。

当我们把图形配置界面拉到最底部,我们可以看到有一个 REST API 的外链,我们戳进去。 image.png

这个时候会出现一篇小作文,介绍 jenkins api 的~ image.png

我们在其中找到标题: Fetch/Update config.xml,再点击其内容中的外链: this URL image.png

我们可以拿到一份完整的 XML 配置文件: image.png

没错!就是这一份 XML 。我们可以根据我们的业务需求,在图形界面中编辑一份固定的构建模板,然后导出成一份 XML 模板,放到我们的 Koa 项目中。以后仅需要把 XML 中的动态配置内容(如分支、构建shell脚本)替换成变量即可实现整个 job config 的动态生成、更新。这时候再回想一下上一篇文章中实现构建的配置保存数据库,是不是就打通了呢?

完成一个 XML 配置模板需要注意什么:

  1. 构建步骤image.png
  2. 构建后操作image.png

总的来说,需要我们根据自己的业务需求实现的比较关键的就是 构建步骤构建后操作 这2个步骤。我们在图形配置界面中配置好n个构建步骤、构建后操作后,就可以导出成一份 XML 模板了。当然,我们很可能不知道图形界面的某个块对应 XML 中的哪个部分,这时候可以通过不断的修改图形界面的配置看看应用在 XML 文件的哪些位置,这样就可以把对应的位置定位出来了。不要觉得这一步很麻烦,这是个一劳永逸的事情,模板搞出来之后,以后再也不用改动了~!

2. 实战 node server 配置 job

上文已经用大篇幅来介绍整个 XML 的由来和配置方式,紧接着笔者通过实战,实现通过 node server 动态配置 jenkins job

首先,先确定模板。笔者这里直接采用最原始的模板,在其中配置一下构建命令:

pnpm install
pnpm run build  

跟上文介绍的一样,这里笔者把最原始的配置信息保存,然后得到他的 XML 文件,并在其中搜索刚才配置的 pnpm xxx 等命令: image.png

接下来,笔者将拷下这份配置到 Koa 项目中进行一系列的操作。

  • 在项目中新建 jenkins 的文件夹,将得到 XML 文件拷贝到其中。
  • 对需要动态配置的地方进行抽离(如构建命令这种),使用变量去替换

由于整个 XML 的内容很多,这里笔者就不直接贴全代码了,感兴趣的可以到 github 上的源码进行查看。这里笔者直接贴出伪代码和图片:

const getXML = () => {
  const buildShell = 'xxx xxx xxx'
  return `xxx ${buildShell} xxx`
}
export default getXML()

image.png

到这里,所有动态配置的前置工作就做完了。剩下就是我们要把之前我们存在 mongoDb 中的构建配置跟这份 XML 文件进行结合,然后通过 npm jenkins 这个库提供的 API 去完成对 jenkins freestyle job 的动态配置了。

  1. 首先回到数据库的配置: (为了方便大家看数据,特地装了个mongo的可视化的工具!) image.png

  2. 读取上述的配置,配置 jenkins job 。又因为笔者这里打算在每次构建前去替换最新的 job ,所以需要在构建前执行这个函数;又因为后续构建需要从前端发起,所以这里直接实现一个 build 的接口

接下来,我们直接实战!关于接口创建、实现等一系列的 node server 基础实战在专栏第二篇文章中已经有详细介绍,这里就不展开了,直接从实现开始。

如图所示,现在已经新建了一个 build 的接口了,接下来就将其实现!
image.png

进入代码阶段之前,为了帮助大家理清接下来要实现的核心功能点,笔者粗略画个图:
配置job-构建.png

  • 前端发起构建,提交 配置id 给后端(下文会讲,期待!)
  • 后端根据 配置id 查询配置
  • 动态替换 XML 模板中的变量配置
  • 调用 jenkins api 实现 job 的更新
  • 调用 jenkins api 实现 job 执行构建任务

如下代码,就是 build接口 核心代码的实现:

export async function build (ctx, next) {
  const requestBody = ctx.request.body
  // 前端会提交上一个配置 id,用来查询对应的配置,替换 job 用
  const { id } = requestBody

  try {
    // 这里写死 job 名称,实际业务中根据自己的规范去动态定义 job 名称即可
    const jobName = 'test-config-job'
    // 通过 id 查询数据库中的配置数据(调用 mongoose 的 findJobById)
    const config = await jobConfig.findJobById(id)
    // 配置 job 的函数(后面会展开)
    await jenkins.configJob(jobName, config)
    // 调用 job 的构建(后面会展开)
    await jenkins.build(jobName)

    ctx.state.apiResponse = {
      code: RESPONSE_CODE.SUC,
      data: null
    }
  } catch (e) {
    ctx.state.apiResponse = {
      code: RESPONSE_CODE.ERR,
      msg: '构建失败'
    }
  }

  next()
}

从上面贴的代码很清楚看到整个构建过程的:找到配置 -> 配置 job -> 调用构建。其中 jenkins.configJobjenkins.build都是笔者自己封装的属于 jenkinsservice 层,放在 /jenkins 的目录中,其实内部的实现都非常简单,就是去调用各种 open api 而已~接着往下看!

首先看看 configJob 的具体实现:

// jenkins.configJob 函数

/**
 * 参数说明
 * jobName:生成的 job 的名称(供构建时使用)
 * config:保存在数据库中的配置信息
**/
export async function configJob (jobName, config) {
  // 判断 job 是否存在(jenkins 那个包提供的封装,使用起来很方便)
  const isExist = await jenkins.job.exists(jobName)
  // 获取 XML 用于配置 jenkins job
  const jksConfig = getXML(config.buildCommand)
  if (!isExist) {
    // 不存在就创建新的 job
    return jenkins.job.create(jobName, jksConfig)
  }
  // 更新已存在的 job ,保证本次构建是最新的配置
  return jenkins.job.config(jobName, jksConfig)
}

再看看 build 的具体实现:

jenkins.build

/**
 * 参数说明
 * jobName:通过 job 名称调用整个 job 执行构建
**/
export async function build (jobName) {
  // 一行代码直接跑构建!!!
  const buildId = await jenkins.job.build(jobName)
}

代码实现后,我们直接执行下看看效果。笔者这里边打代码边调试的时候不小心把 job 创建完了~由图可以看到,以 test-config-job 为名的 job 已经可以在 jenkins 中看到了!

image.png

紧接着,笔者在 postman 中模拟了一个前端请求(id那些都是在数据库中 copy 出来的~先写死!) image.png

接下来,笔者激动的点了点构建: image.png

完美,成功返回并且成功调用起了 jenkins job 的构建。你以为这就完了吗?emmm,确实完了,但是我们还需要再完善一些,毕竟发布平台的核心不能只是点击构建就算了的,好歹你也要让用户知道当前的构建进度吧?好歹你也要让用户知道自己的项目构建有没有bug吧?我们接着往下看,看看如何让用户知道当前的构建进度、是否构建成功?

二、获取 Jenkins 构建日志

1. 如何获取?

其实我们做发布平台,需要让用户了解当前的构建进度和情况,只需要把 jenkins 的日志输出“转发”出来就可以了,日志里面已经有很完善的信息提供了。jenkins的日志界面如图所示:

image.png

当然,实现这一步也不会很复杂,毕竟 npm jenkins 这个库已经帮我们封装了一层,我们只需要直接调用即可~那我们先了解一下,看看文档的 demo 是如何使用的。 image.png

首先,我们得知了两个比较关键的参数:

  1. job name。跟上文提到的一样,就是 job 名称(我们用来发起构建的那个)
  2. build number。可以理解为本次 job 的构建序号,只有拿到这个build number,我们才能查到当前的构建日志。 至于 typedelay 这种,根据自己的业务场景去配置即可,问题不大~

根据 demo 的用法展示,我们可以发现:只需要按照这种“事件”的使用方式去监听 dataerrorend 这几个钩子,就能追踪到整个 job 的执行链路,执行进度。只要把这些数据拿到手,并且最终打通前端(这个会在下一篇文章中讲到),就能把整个构建进度在前端展示了,用户能清楚的知道整个构建的过程、结果~那么接下来,我们直接 copy 搞起!

2. 实战获取构建日志

首先明确本文需要实现的程度,由于这一块最后是要输送到前端的(前端的实现会放到下一篇文章中),所以在本文仅仅以实现 服务端 获取到 jenkins日志 为目的。所以这里,以 node server打印构建日志 即为成功!

上文中提到的 jenkins.build.logstream 方法中,需要两个核心参数去获取本次 jenkins job 的构建日志。当然, jobName 是我们事先就知道的了(因为我们通过 jobName 去触发 job 构建的),所以,如何获取 buildNumber 成为我们目前的首要任务。

当然,这一步可以在一个 issue 中找到答案,感兴趣的可以点击进去详细查看。 image.png

笔者在这里简单概括成2点:

  1. 触发构建时,jenkins 会创建一个队列项,但不会立即分配 buildNumber
  2. 需要轮训队列,直到分配的 buildNumber 被获取

按照 issue 中的解决方案,可以将其封装成 Promise 的方式,以成功获取到 buildNumber

function waitForBuildNumber(buildId) {
  // 返回一个 Promise
  return new Promise(function (resolve, reject) {
    // 开启定时器做轮训
    const timer = setInterval(async function () {
      try {
        // 观察当然队列项
        const item = await jenkins.queue.item(buildId)
        if (item.executable) {
          // 得到 buildNumber 后,将结果 resolve 出去
          resolve(item.executable.number)
          // 清除定时器
          clearInterval(timer)
        } else if (item.cancelled) {
          // 构建取消,清除定时器
          clearInterval(timer)
          // reject Promise
          reject()
        }
      } catch (e) {
        reject(e)
      }
    }, 500)
  })
}

实现完获取 buildNumber 的函数后,验证一下是否成功。我们对 build 函数增加两行代码,再用 postman 请求后看看效果~

export async function build (jobName) {
  const buildId = await jenkins.job.build(jobName)
  const buildNumber = await waitForBuildNumber(buildId)
  console.log(buildNumber); // 打印 buildNumber
}

成功发起 jenkins 的构建: image.png 控制台成功打印出 buildNumber=3image.png

注意⚠️:如果这里大家也要用到 node 的调试工具 ,记得在启动命中加入参数: --inspect ,详细说明可以戳一下 node文档

大功告成!绕了大半圈终于获取成功 buildNumber 了。接下来就是最后一步了,将 jobNamebuildNumber 传进 logStream 函数里,获取构建日志!let's go

const logStream = jenkins.build.logStream(jobName, buildNumber, "text", 2000)
// 这一段就是在文档中拷回来的代码,完全不需要自己出手,爽!
logStream.on('data', function(text) {
  console.log(text); // 打印 text 验证日志获取情况
});

logStream.on('error', function(err) {
  console.log('error', err);
});

logStream.on('end', function() {
  console.log('end');
});

接下来,我们再通过 postmanbuild 接口做一次请求: image.png

如上图所示,构建日志成功在 devtool 的 控制台 中打印,这也就以为着我们成功获取到 jenkins 的构建进度的信息了。

三、练兵时间

相信看到这里的你,已经对整个 node serverjenkins 的交互搞得明明白白了,自己手撸一个发布平台肯定不在话下。其实一个完善的发布平台,需要实现的远远不止笔者在本文实现的功能点。但是,如果掌握了本文的实现,那去完善一个发布平台也是轻轻松松的事。不信我们一起试一试。

回顾文章开头,笔者还有两点是没有手把手实现的:

  1. 后端发起停止构建
  2. jenkins job 在各执行阶段如何通知 node server

首先看第一个怎么实现。停止构建,不就是 node server 提供一个接口,在实现这个接口的时候,通过调用 npm jenkinsapi 完成停止构建!直接在文档中找到 stop 的函数,接进去就完事了~ image.png

其次,再看看 jenkins job 在各执行阶段如何通知 node server。为什么会有这样的需求场景呢?这就非常的业务导向了,笔者也不好穷尽的去举例,就举个好理解的例子吧。比如你希望在 node server 中检测 npm 装包的用时,那就需要 jenkins装包前、后 通知到 服务端。这种便是 jenkins 主动跟 我们的服务端交互了。大家可以试想一下怎么实现?

大家可以回想一下,我们在服务端是如何通知 jenkins 构建、获取 buildNumber 的?是不是通过使用 npm jenkins 的接口呢?那其实不就是在调用 jenkinsopenApi 吗?好了,笔者也不卖关子了,那就是在 jenkins 中,通过 curl 的方式,来调用我们服务端的接口以完成交互。如果觉得不好理解,没关系,笔者来段小 demo ,一下就懂了:

image.png

是不是非常简单~我们可以直接 shell 脚本里写几个 curl 来访问自己服务端的接口以实现跟jenkins 交互 ,想写几个写几个!

写在最后

到这里,本文的主要内容就结束了。简单回顾一下,本文主要讲解 node server 如何跟 jenkins 交互,实现服务端触发 jenkins 构建并且获取 jenkins 构建状态。总的来说,技术上并不是特别困难,可能更耗时的事是去搭建环境,装这装那的。笔者在这里想提一句,光看不够,如果想真正的掌握,还是得自己实战,敲一敲代码,无论是 demo 级别的,还是要投产到团队中使用的都可以~

接着下一篇文章,笔者将要讲述通过 vue3 + Element-plus + Koa 的全栈实战,实现一个前端发布平台的前端界面和配置的 crud 功能。主要内容有:

  1. 前端后端数据分页展示实现
  2. 全栈增删改数据实现

大家敬请期待!鸭!