Jenkins允许用户完成所有这些操作的一个关键方法就是使用流水线共享库(pipeline share library)。共享流水线库是由存储在代码仓库中的代码组成的,该代码仓库由Jenkins自动下载并可供流水线使用。

1、需求引入

随着 DevOps 理念在公司越来越多的实践,Jenkins 等工具的应用场景越来越多,当我们在执行完成某个流水线任务后,常常需要关注的是这个任务为什么执行,执行成功与否等等。

于是就需要在执行完流水线后进行一定程度的消息推送,在现今的工作流中消息推送无外乎分为两大类:邮件和企业沟通协作软件,相比之下,我们可能更多的会去关注和使用沟通软件来发送消息而不是通过邮件的方式。而常用的企业沟通协作软件有以下几类:腾讯系的企业微信、阿里系的钉钉、字节跳动的飞书等等,当然有能力的企业也会自己研发这类软件。

本文示例以钉钉为例,通过流水线共享库实现自定义消息通知器。

2、钉钉机器人

钉钉的群机器人是钉钉群的高级扩展功能。群机器人可以将第三方服务的信息聚合到群聊中,实现自动化的信息同步。例如:通过聚合GitHub,GitLab等源码管理服务,实现源码更新同步;通过聚合Trello,JIRA等项目协调服务,实现项目信息同步。不仅如此,群机器人支持Webhook协议的自定义接入,支持更多可能性。

自定义钉钉机器人支持以下类型消息类型数据格式的推送,更多定义方法可参考官方的接口文档:

  • text类型
  • markdown类型
  • 整体跳转ActionCard类型
  • 独立跳转ActionCard类型
  • FeedCard类型

钉钉机器人在2019年的下半年进行过升级,在新增机器人时,需要选择一种安全条件(自定义关键词、加签、ip地址或ip地址段)来保障自定义机器人的安全。可以理解为即使机器人的token泄漏,如果不知道设置的安全条件是什么,还是无法盗用的。

3、Jenkins 消息推送插件

这里要提到的是在 Jenkins 插件列表中有一个钉钉插件。

简单对此插件做了下分析:截止目前此插件在2020年1月份有相应代码提交,并且发布了2.0版本,从jenkins的插件官网中可以看到此版本的插件在在消息中支持了更多内容,效果如下,但是此插件目前还暂不支持流水线中使用




Android 发送自定义通知_共享库


在此之前的上一版本提交记录已经是2018年了,此插件使用方法类似,推送的消息效果如下


Android 发送自定义通知_自定义_02


此版本支持在流水线中使用,相应内容如下

dingTalk accessToken: "xxx",imageUrl: "xxx",jenkinsUrl: "https://127.0.0.1:8080",message: "项目构建成功",notifyPeople: "155xxxx5533"

如上所示,在流水线脚本中配置钉钉机器人token、图片路径、jenkins地址、消息内容、要提醒的人手机号码即可,可以发现,此消息还是有局限性,不够友好。

因此在没有编写插件能力的情况下,我们可以通过更为灵活的自定义流水线共享库的形式,并且按照钉钉机器人的官方接口文档,自定义一个消息推送通知器。

4、自定义通知器的实现

4.1、内容定义

无论 Jenkins 任务的构建触发原因是使用者手动构建或通过代码推送的自动触发,往往关注此消息的人群是开发者们。因此通过一段时间的需求调研以及综合各方的建议,最终将消息推送的内容中包含了以下信息:

  • 应用名称
  • 构建结果
  • 当前版本
  • 构建发起
  • 持续时间
  • 构建日志
  • 更新记录(包含用户提交的短日志,用户名称,提交时间)

每次构建结果通知中包含了以上就基本完备。

4.2、共享库创建

本文不过多介绍共享库具体的创建与在pipeline流水线中的引用方法,整体来说,共享库的代码目录结构如下

(root)+- src                     # Groovy source files|   +- org|       +- foo|           +- Bar.groovy  # for org.foo.Bar class+- vars|   +- foo.groovy          # for global 'foo' variable|   +- foo.txt             # help for 'foo' variable+- resources               # resource files (external libraries only)|   +- org|       +- foo|           +- bar.json    # static helper data for org.foo.Bar

官方描述:

  • src目录应该看起来像标准的Java源目录结构。当执行流水线时,该目录被添加到类路径下。
  • vars目录定义可从流水线访问的全局变量的脚本。每个 .groovy文件的基名应该是一个Groovy (~ Java)标识符, 通常是camelCased。匹配.txt,如果存在, 可以包含文档, 通过系统的配置标记格式化从处理 (所以可能是HTML, Markdown等,虽然txt扩展是必需的)。

这些目录中的Groovy源文件 在脚本化流水线中的CPS transformation一样。

resources目录允许从外部库中使用 libraryResource 步骤来加载有关的非 Groovy 文件。目前,内部库不支持该特性。

根目录下的其他目录被保留下来以便于将来的增强。

4.3、方法的具体实现

定义共享库中src/org/devops目录为共享库方法的主目录,在这个目录下创建一个名为dingmes.groovy的文件作为钉钉消息推送方法的代码文件。

构建一个消息通知器的主要思路:

  • 消息指标内容从哪来
  • 消息模板如何定义
  • 消息怎么发送,发到哪里

4.3.1、消息来源

首先,消息内容从哪来,上面提到的需要在消息中体现的每个指标的可取的获取方式


Android 发送自定义通知_手机txt拆分器_03


  • 分析:
    本文中的共享库用于 Jenkins+k8s 自动化ci测试环境,因此某些指标的定义方法为:
    应用名称自定义,用变量给出,在pipeline前文定义全局变量,在这里传入变量即可
    当前版本自定义,以代码分支+commitid作为docker镜像的tag,在pipeline前文中实现或亦通过共享库实现,在这里传入变量即可
    更新记录根据全局变量获取,在这里通过代码实现

较为复杂的是如何解读 currentBuild.changeSet 这个全局变量,通过 Jenkins 上的全局变量列表文档查看如下


Android 发送自定义通知_手机txt拆分器_04


点击其中的链接查看官方文档


Android 发送自定义通知_自定义_05


通过进一步查看官方文档得知,currentBuild.changeSet返回的是一个集合,这个集合中包含了提交日志,commitid,作者id,作者全称,时间戳等信息,具体对象相关属性如下

currentBuild.changeSets{    items[{        msg //提交注释        commitId //提交hash值        author{ //提交用户相关信息            id            fullName        }        timestamp        affectedFiles[{ //受影响的文件列表            editType{                name            }            path: "path"        }]        affectedPaths[// 受影响的目录,是个Collection            "path-a","path-b"        ]    }]}

因此,可以通过循环遍历得出我们需要的相关属性值,通过groovy脚本定义方法并返回相应字符串,其中为了更优化,需要对提交日志做一下长度限制,对时间戳进行格式化,这两个功能需要不断调试。其中changeString变量的赋值格式定义为markdown的无序列表,最终方法如下

def getChangeString() {    def changeString = ""    def MAX_MSG_LEN = 20    def changeLogSets = currentBuild.changeSets    for (int i = 0; i < changeLogSets.size(); i++) {        def entries = changeLogSets[i].items        for (int j = 0; j < entries.length; j++) {            def entry = entries[j]            truncatedMsg = entry.msg.take(MAX_MSG_LEN)            commitTime = new Date(entry.timestamp).format("yyyy-MM-dd HH:mm:ss")            changeString += " - ${truncatedMsg} [${entry.author} ${commitTime}]"        }    }    if (!changeString) {        changeString = " - No new changes"    }    return (changeString)}

4.3.2、消息模板定义

消息中的相关字段都获取到了,下一步需要做的就是定义一个消息模板,如果使用邮件发送通知,同样的也需要定义一个模板。

这里使用更为友好的markdown格式来发送通知,钉钉机器人接口接收的消息是json格式,具体内容可以通过查看官方文档,为了避免换行出错,手动指定换行符,最终的json格式数据和markdown格式模板如下

{    "msgtype":"markdown",    "markdown":{        "title":"项目构建信息",        "text":"### 构建信息>- 应用名称: **${AppName}**- 构建结果: **${Status} ${CatchInfo}**- 当前版本: **${ImageTag}**- 构建发起: **${env.BUILD_USER}**- 持续时间: **${currentBuild.durationString}**- 构建日志: [点击查看详情](${env.BUILD_URL}console)#### 更新记录: ${ChangeLog}"    },    "at":{        "atMobiles":[            "155xxxx5533"        ],        "isAtAll":false    }}

4.3.3、消息发送方法

在流水线中按照消息模板渲染好的消息发送给钉钉的接口地址,可以实现的方法包括但不限于以下几种:

  • 通过执行shell命令发送,例如curl命令指定参数即可,最为简单,但不够友好
  • 通过pipeline语法和插件实现,例如使用HTTP Request插件,在Jenkins pipeline中发送HTTP请求给钉钉接口。
  • 通过调用其他脚本发送,例如python脚本,较复杂,不推荐。

综上比较,选择一种友好且不复杂的方案,即通过pipeline语法和插件实现

首先在插件安装中安装好HTTP Request插件,打开语法片段生成器查看对应语法


Android 发送自定义通知_共享库_06


虽然参数有些多,但是只有url是必需的,其他参数都是可选的。这里我们传入请求内容以及url,并省去其他不必要的参数,如下

httpRequest acceptType: 'APPLICATION_JSON_UTF8',        consoleLogResponseBody: false,        contentType: 'APPLICATION_JSON_UTF8',        httpMode: 'POST',        ignoreSslErrors: true,        requestBody: ReqBody,        responseHandle: 'NONE',        url: "${DingTalkHook}",        quiet: true

4.3.4、最终方法

综上所述,在调用此共享库方法时传入应用名称变量AppName、应用版本(镜像tag)变量ImageTag、构建状态变量Status、以及在pipeline前文中实现的异常信息捕捉变量CatchInfo,并结合前面实现的方法内容,最终方法dingmes.groovy内容如下

/* dingmes.groovy   ##################################################   # Created by SSgeek                              #   #                                                #   # A Part of the Project jenkins-library          #   ##################################################*/package org.devopsdef getChangeString() {    def changeString = ""    def MAX_MSG_LEN = 20    def changeLogSets = currentBuild.changeSets    for (int i = 0; i < changeLogSets.size(); i++) {        def entries = changeLogSets[i].items        for (int j = 0; j < entries.length; j++) {            def entry = entries[j]            truncatedMsg = entry.msg.take(MAX_MSG_LEN)            commitTime = new Date(entry.timestamp).format("yyyy-MM-dd HH:mm:ss")            changeString += " - ${truncatedMsg} [${entry.author} ${commitTime}]"        }    }    if (!changeString) {        changeString = " - No new changes"    }    return (changeString)}def HttpReq(AppName,ImageTag=' ',Status,CatchInfo=' '){    wrap([$class: 'BuildUser']){        def DingTalkHook = "https://oapi.dingtalk.com/robot/send?access_token=67449753547bfcb8e2ee6088fdebaf2cdc7228787201fca83406fc449ffaf92"        def ChangeLog = getChangeString()        def ReqBody = """{            "msgtype": "markdown",            "markdown": {                "title": "项目构建信息",                "text": "### 构建信息>- 应用名称: **${AppName}**- 构建结果: **${Status} ${CatchInfo}**- 当前版本: **${ImageTag}**- 构建发起: **${env.BUILD_USER}**- 持续时间: **${currentBuild.durationString}**- 构建日志: [点击查看详情](${env.BUILD_URL}console)#### 更新记录: ${ChangeLog}"            },            "at": {                "atMobiles": [                    "155xxxx5533"                ],                "isAtAll": false                }            }"""        // println(currentBuild.description)        // println(currentBuild.changeSets)        httpRequest acceptType: 'APPLICATION_JSON_UTF8',                consoleLogResponseBody: false,                contentType: 'APPLICATION_JSON_UTF8',                httpMode: 'POST',                ignoreSslErrors: true,                requestBody: ReqBody,                responseHandle: 'NONE',                url: "${DingTalkHook}",                quiet: true    }}

4.4、方法调用

此消息通知的方法通常在pipeline的post部分调用,如下所示

post{    success{        script{            tools.PrintMes("========pipeline executed successfully========",'green')            dingmes.HttpReq(AppName,ImageTag,"构建成功 ✅")        }    }    failure{        script{            tools.PrintMes("========pipeline execution failed========",'red')            dingmes.HttpReq(AppName,ImageTag,"构建失败 ❌",CatchInfo)        }    }    unstable{        script{            tools.PrintMes("========pipeline execution unstable========",'red')            dingmes.HttpReq(AppName,ImageTag,"构建失败 ❌","不稳定异常")        }    }    aborted{        script{            tools.PrintMes("========pipeline execution aborted========",'blue')            dingmes.HttpReq(AppName,ImageTag,"构建失败 ❌","暂停或中断")        }    }}

4.5、最终效果

测试代码提交,执行流水线,最终的消息通知效果如下图


Android 发送自定义通知_手机txt拆分器_07


5、总结
至此,本文记录通过自定义 Jenkins Pipeline 流水线共享库方法,实现了较为灵活的自定义钉钉机器人消息通知。如果是使用企信等其他软件,与此实现思路相近。

参考:https://jenkins.io/doc/book/pipeline/shared-libraries/