node+jenkins 实战前端发布平台——自动化部署
哈喽!这篇文章来到了整个前端发布平台的核心——自动化部署的实现。本文将分享通过后端跟jenkins之间交互,完成前端项目的自动化部署的业务功能。笔者手把手“教”学,大家跟着走~let‘go!
系列文章:
- 总览前端自动化部署流程,如何实现前端发布平台?文章链接
- 前端发布平台
node server
实战!文章链接 - 前端发布平台
jenkins
实战!如何实现前端自动化部署? - 前端发布平台全栈实战(前后端开发完整篇)!开发一个前端发布平台。文章链接
websocket
全栈实战,实现唯一构建实例 + 日志同步。文章链接
本文是第三篇「前端发布平台 jenkins
实战!如何实现前端自动化部署?」的实战记录分享。主要通过分享 node server
跟 jenkins
之间的交互,实现核心的自动化部署功能。
主要分享以下几点:
- 后端配置 jenkins 的
jenkins 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)。
文章的开头有提到本文会使用 npm jenkins 包,所以笔者决定通过它的文档来介绍「如何动态配置 jenkins job ?」
- 首先看创建 job 的
api
: - 再看更新 job 的
api
:
大家都能看出,笔者在2张图中都圈出一个关键点:XML
。显然,如果我们需要在后端去 生成、动态配置 job ,这个 XML
都是一个关键的存在,那它是一个什么东西来的呢?其实就是我们在可视化界面配置完之后的配置产物。那接下来,我们就去找找这个 XML
到底在哪里。
当我们把图形配置界面拉到最底部,我们可以看到有一个 REST API
的外链,我们戳进去。
这个时候会出现一篇小作文,介绍 jenkins api 的~
我们在其中找到标题: Fetch/Update config.xml,再点击其内容中的外链: this URL
我们可以拿到一份完整的 XML
配置文件:
没错!就是这一份 XML
。我们可以根据我们的业务需求,在图形界面中编辑一份固定的构建模板,然后导出成一份 XML
模板,放到我们的 Koa 项目中。以后仅需要把 XML
中的动态配置内容(如分支、构建shell脚本)替换成变量即可实现整个 job config 的动态生成、更新。这时候再回想一下上一篇文章中实现构建的配置保存数据库,是不是就打通了呢?
完成一个 XML
配置模板需要注意什么:
- 构建步骤。
- 构建后操作。
总的来说,需要我们根据自己的业务需求实现的比较关键的就是 构建步骤、构建后操作 这2个步骤。我们在图形配置界面中配置好n个构建步骤、构建后操作后,就可以导出成一份 XML
模板了。当然,我们很可能不知道图形界面的某个块对应 XML
中的哪个部分,这时候可以通过不断的修改图形界面的配置看看应用在 XML
文件的哪些位置,这样就可以把对应的位置定位出来了。不要觉得这一步很麻烦,这是个一劳永逸的事情,模板搞出来之后,以后再也不用改动了~!
2. 实战 node server 配置 job
上文已经用大篇幅来介绍整个 XML
的由来和配置方式,紧接着笔者通过实战,实现通过 node server 动态配置 jenkins job
。
首先,先确定模板。笔者这里直接采用最原始的模板,在其中配置一下构建命令:
pnpm install
pnpm run build
跟上文介绍的一样,这里笔者把最原始的配置信息保存,然后得到他的 XML 文件,并在其中搜索刚才配置的 pnpm xxx
等命令:
接下来,笔者将拷下这份配置到 Koa 项目中进行一系列的操作。
- 在项目中新建 jenkins 的文件夹,将得到
XML
文件拷贝到其中。 - 对需要动态配置的地方进行抽离(如构建命令这种),使用变量去替换。
由于整个 XML
的内容很多,这里笔者就不直接贴全代码了,感兴趣的可以到 github 上的源码进行查看。这里笔者直接贴出伪代码和图片:
const getXML = () => {
const buildShell = 'xxx xxx xxx'
return `xxx ${buildShell} xxx`
}
export default getXML()
到这里,所有动态配置的前置工作就做完了。剩下就是我们要把之前我们存在 mongoDb
中的构建配置跟这份 XML
文件进行结合,然后通过 npm jenkins
这个库提供的 API
去完成对 jenkins freestyle job
的动态配置了。
-
首先回到数据库的配置: (为了方便大家看数据,特地装了个mongo的可视化的工具!)
-
读取上述的配置,配置
jenkins job
。又因为笔者这里打算在每次构建前去替换最新的job
,所以需要在构建前执行这个函数;又因为后续构建需要从前端发起,所以这里直接实现一个build
的接口。
接下来,我们直接实战!关于接口创建、实现等一系列的 node server
基础实战在专栏第二篇文章中已经有详细介绍,这里就不展开了,直接从实现开始。
如图所示,现在已经新建了一个 build
的接口了,接下来就将其实现!
进入代码阶段之前,为了帮助大家理清接下来要实现的核心功能点,笔者粗略画个图:
- 前端发起构建,提交
配置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.configJob
、jenkins.build
都是笔者自己封装的属于 jenkins 的 service
层,放在 /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 中看到了!
紧接着,笔者在 postman 中模拟了一个前端请求(id那些都是在数据库中 copy 出来的~先写死!)
接下来,笔者激动的点了点构建:
完美,成功返回并且成功调用起了 jenkins job 的构建。你以为这就完了吗?emmm,确实完了,但是我们还需要再完善一些,毕竟发布平台的核心不能只是点击构建就算了的,好歹你也要让用户知道当前的构建进度吧?好歹你也要让用户知道自己的项目构建有没有bug吧?我们接着往下看,看看如何让用户知道当前的构建进度、是否构建成功?
二、获取 Jenkins 构建日志
1. 如何获取?
其实我们做发布平台,需要让用户了解当前的构建进度和情况,只需要把 jenkins 的日志输出“转发”出来就可以了,日志里面已经有很完善的信息提供了。jenkins的日志界面如图所示:
当然,实现这一步也不会很复杂,毕竟 npm jenkins 这个库已经帮我们封装了一层,我们只需要直接调用即可~那我们先了解一下,看看文档的 demo 是如何使用的。
首先,我们得知了两个比较关键的参数:
job name
。跟上文提到的一样,就是 job 名称(我们用来发起构建的那个)build number
。可以理解为本次 job 的构建序号,只有拿到这个build number,我们才能查到当前的构建日志。 至于type
、delay
这种,根据自己的业务场景去配置即可,问题不大~
根据 demo 的用法展示,我们可以发现:只需要按照这种“事件”的使用方式去监听 data
、 error
、 end
这几个钩子,就能追踪到整个 job 的执行链路,执行进度。只要把这些数据拿到手,并且最终打通前端(这个会在下一篇文章中讲到),就能把整个构建进度在前端展示了,用户能清楚的知道整个构建的过程、结果~那么接下来,我们直接 copy
搞起!
2. 实战获取构建日志
首先明确本文需要实现的程度,由于这一块最后是要输送到前端的(前端的实现会放到下一篇文章中),所以在本文仅仅以实现 服务端 获取到 jenkins日志 为目的。所以这里,以 node server
端打印到 构建日志 即为成功!
上文中提到的 jenkins.build.logstream
方法中,需要两个核心参数去获取本次 jenkins job
的构建日志。当然, jobName
是我们事先就知道的了(因为我们通过 jobName
去触发 job
构建的),所以,如何获取 buildNumber
成为我们目前的首要任务。
当然,这一步可以在一个 issue 中找到答案,感兴趣的可以点击进去详细查看。
笔者在这里简单概括成2点:
- 触发构建时,jenkins 会创建一个队列项,但不会立即分配
buildNumber
- 需要轮训队列,直到分配的
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 的构建:
控制台成功打印出
buildNumber=3
:
注意⚠️:如果这里大家也要用到 node 的调试工具 ,记得在启动命中加入参数: --inspect
,详细说明可以戳一下 node文档~
大功告成!绕了大半圈终于获取成功 buildNumber
了。接下来就是最后一步了,将 jobName
和 buildNumber
传进 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');
});
接下来,我们再通过 postman 对 build
接口做一次请求:
如上图所示,构建日志成功在 devtool 的 控制台 中打印,这也就以为着我们成功获取到 jenkins 的构建进度的信息了。
三、练兵时间
相信看到这里的你,已经对整个 node server 跟 jenkins 的交互搞得明明白白了,自己手撸一个发布平台肯定不在话下。其实一个完善的发布平台,需要实现的远远不止笔者在本文实现的功能点。但是,如果掌握了本文的实现,那去完善一个发布平台也是轻轻松松的事。不信我们一起试一试。
回顾文章开头,笔者还有两点是没有手把手实现的:
- 后端发起停止构建
jenkins job
在各执行阶段如何通知node server
首先看第一个怎么实现。停止构建,不就是 node server
提供一个接口,在实现这个接口的时候,通过调用 npm jenkins 的 api
完成停止构建!直接在文档中找到 stop
的函数,接进去就完事了~
其次,再看看 jenkins job
在各执行阶段如何通知 node server
。为什么会有这样的需求场景呢?这就非常的业务导向了,笔者也不好穷尽的去举例,就举个好理解的例子吧。比如你希望在 node server
中检测 npm 装包的用时,那就需要 jenkins 在 装包前、后 通知到 服务端。这种便是 jenkins 主动跟 我们的服务端交互了。大家可以试想一下怎么实现?
大家可以回想一下,我们在服务端是如何通知 jenkins 构建、获取 buildNumber
的?是不是通过使用 npm jenkins 的接口呢?那其实不就是在调用 jenkins 的 openApi
吗?好了,笔者也不卖关子了,那就是在 jenkins 中,通过 curl 的方式,来调用我们服务端的接口以完成交互。如果觉得不好理解,没关系,笔者来段小 demo ,一下就懂了:
是不是非常简单~我们可以直接 shell
脚本里写几个 curl
来访问自己服务端的接口以实现跟jenkins 交互 ,想写几个写几个!
写在最后
到这里,本文的主要内容就结束了。简单回顾一下,本文主要讲解 node server
如何跟 jenkins 交互,实现服务端触发 jenkins 构建并且获取 jenkins 构建状态。总的来说,技术上并不是特别困难,可能更耗时的事是去搭建环境,装这装那的。笔者在这里想提一句,光看不够,如果想真正的掌握,还是得自己实战,敲一敲代码,无论是 demo 级别的,还是要投产到团队中使用的都可以~
接着下一篇文章,笔者将要讲述通过 vue3
+ Element-plus
+ Koa
的全栈实战,实现一个前端发布平台的前端界面和配置的 crud
功能。主要内容有:
- 前端后端数据分页展示实现
- 全栈增删改数据实现
大家敬请期待!鸭!
转载自:https://juejin.cn/post/7158274890317955108