Jenkins通知脚本-飞书卡片通知
目的
使用飞书卡片消息类型将Jenkins任务构建开始和结束时状态进行飞书群通知
关键点
- Jenkins构建任务中增加2个过程
-
- Build Steps -> Execute shell (添加在主构建脚本(npm/mvn相关脚本)之前,目的是:编译等主要构建过程之前进行通知)
-
- 构建后操作-> Post build task(添加在构建后操作,目的是:编译等主要构建过程之后进行通知)
- 飞书卡片的制作,必须先在飞书后台自建应用内新建自己的卡片,并且卡片的字段和卡片ID要和脚本内容匹配;
效果截图
- 构建开始
- 构建结束
源码
飞书卡片源码
- 请使用“消息卡片制作工具”进行制作,下述源码会根据卡片组件自动生成;
{
"config": {
"wide_screen_mode": true
},
"elements": [
{
"tag": "hr"
},
{
"tag": "markdown",
"content": "**任务名称:** **<font >${JOB_NAME}</font>**"
},
{
"tag": "markdown",
"content": "此次构建启动时间:${BUILD_START_TIME}"
},
{
"tag": "markdown",
"content": "构建状态:**<font color=${font_color}>${build_status}</font>**"
},
{
"tag": "hr"
},
{
"tag": "markdown",
"content": "**仓库分支信息如下:**"
},
{
"tag": "column_set",
"flex_mode": "none",
"background_style": "grey",
"columns": [
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "column_set",
"flex_mode": "none",
"background_style": "grey",
"columns": [
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "**地址**"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "**分支**"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "**commitId**"
}
]
}
]
}
]
}
],
"horizontal_spacing": "default"
},
{
"tag": "column_set",
"flex_mode": "none",
"background_style": "default",
"columns": [
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "${repo_url}"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "${repo_branch_name}"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "${repo_branch_commit_id}"
}
]
}
],
"_varloop": "${last_build_info}",
"horizontal_spacing": "default"
},
{
"tag": "hr"
},
{
"tag": "markdown",
"content": "**${last_changes_info_text}**"
},
{
"tag": "column_set",
"flex_mode": "none",
"background_style": "grey",
"columns": [
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "**提交ID**"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "**提交人**"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "**提交内容**"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "**提交时间**"
}
]
}
]
},
{
"tag": "column_set",
"flex_mode": "none",
"background_style": "default",
"columns": [
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "${commitId}"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "${authorEmail}"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "${comment}"
}
]
},
{
"tag": "column",
"width": "weighted",
"weight": 1,
"vertical_align": "top",
"elements": [
{
"tag": "markdown",
"content": "${date}"
}
]
}
],
"_varloop": "${last_changes_info}"
},
{
"tag": "hr"
},
{
"tag": "markdown",
"content": "**😁构建人:**<at email=${user_id}></at>"
},
{
"tag": "markdown",
"content": "**😁知情人:**${insider_users_email}"
},
{
"tag": "action",
"actions": [
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "任务链接"
},
"type": "primary",
"multi_url": {
"url": "${JOB_URL}",
"pc_url": "",
"android_url": "",
"ios_url": ""
}
},
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "构建链接"
},
"type": "primary",
"multi_url": {
"url": "${THIS_BUILD_URL}",
"pc_url": "",
"android_url": "",
"ios_url": ""
}
},
{
"tag": "button",
"text": {
"tag": "plain_text",
"content": "Jenkins变更记录"
},
"type": "primary",
"multi_url": {
"url": "${THIS_BUILD_CHANGES}",
"pc_url": "",
"android_url": "",
"ios_url": ""
}
}
]
},
{
"tag": "hr"
},
{
"tag": "note",
"elements": [
{
"tag": "img",
"img_key": "img_v2_xxx",
"alt": {
"tag": "plain_text",
"content": ""
}
},
{
"tag": "plain_text",
"content": "${short_description}"
}
]
}
],
"card_link": {
"url": "",
"pc_url": "",
"android_url": "",
"ios_url": ""
},
"header": {
"title": {
"content": "Jenkins任务构建通知:",
"tag": "plain_text"
},
"template": "wathet"
}
}
脚本源码
- Build Steps -> Execute shell 和 构建后操作-> Post build task 存放一样的shell脚本
#/bin/bash
feishu_chat_id1="oc_xxx"
feishu_chat_list=($feishu_chat_id1)
function send_msg() {
for ((i = 0; i < ${#feishu_chat_list[@]}; i++)); do
feishu_chat_id=${feishu_chat_list[$i]}
/root/anaconda3/envs/py36/bin/python /jenkins_tools/jenkins_builds_info_notify_feishu.py \
"${feishu_chat_id}" -job_name "${JOB_NAME}" -job_url "${JOB_URL}" -build_name "${BUILD_ID}" -branch "${GIT_BRANCH}"
done
}
send_msg
- Python脚本(该脚本Python版本:3.6.8),存放在Jenkins服务器上,让shell脚本调用。
import argparse
import json
import time
import requests
import validators
parser = argparse.ArgumentParser(description='Jenkins 发送消息到飞书',
epilog="执行示例>>> python ${feishu_chat_id} -job_name ${JOB_NAME} -job_url ${JOB_URL} "
"-branch ${GIT_BRANCH} -build_name ${BUILD_NUMBER} ")
parser.add_argument('feishu_chat_id', help='机器人webhookURL') # 必填
parser.add_argument('-job_name', '--JOB_NAME', help='作业Name', ) # 选填
parser.add_argument('-job_url', '--JOB_URL', help='作业URL', required=True, ) # 必填
parser.add_argument('-branch', '--GIT_BRANCH', help='git分支', default='') # 选填
parser.add_argument('-build_name', '--BUILD_DISPLAY_NAME', help='编译Name') # 选填
feishu_chat_id = parser.parse_args().feishu_chat_id
JOB_NAME = parser.parse_args().JOB_NAME
JOB_URL = parser.parse_args().JOB_URL
GIT_BRANCH = parser.parse_args().GIT_BRANCH
BUILD_DISPLAY_NAME = parser.parse_args().BUILD_DISPLAY_NAME
BUILD_URL = JOB_URL + '/lastBuild'
def feishu_tools(send_data, app_id, app_secret):
get_tenant_access_token_url = "https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal"
payload = json.dumps({
"app_id": app_id,
"app_secret": app_secret
})
headers = {
'Content-Type': 'application/json'
}
response = requests.request("POST", get_tenant_access_token_url, headers=headers, data=payload)
tenant_access_token = json.loads(response.text)['tenant_access_token']
url = "https://open.feishu.cn/open-apis/message/v4/send/"
headers = {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + str(tenant_access_token)
}
response = requests.request("POST", url, headers=headers, data=send_data)
return response.text
def feishu_notify(jenkins_auth, app_id, app_secret, card_id):
result = requests.get(f'{BUILD_URL}/api/json', auth=jenkins_auth).json()
print(json.dumps(result))
last_build_info = []
last_changes_info = []
insider_users_email = ""
for _ in result['actions']:
if _ != {}:
if _['_class'] == "hudson.model.CauseAction":
for c in _['causes']:
if c['_class'] == "hudson.model.Cause$UserIdCause":
short_description = c['shortDescription']
user_id = c['userId']
# user_name = c['userName']
elif _['_class'] == "hudson.plugins.git.util.BuildData":
cur_build_info = {
"repo_url": _.get('remoteUrls')[0],
"repo_branch_name": _['lastBuiltRevision'].get('branch')[0].get('name'),
"repo_branch_commit_id": _['lastBuiltRevision'].get('branch')[0].get('SHA1'),
}
last_build_info.append(cur_build_info)
elif _['_class'] == "hudson.model.ParametersAction":
for p in _['parameters']:
if "insider_users_email" == p['name']:
insider_users_email = list(p['value'].split(" "))
print(insider_users_email)
insider_users_email_formats = ""
for insider_user_email in insider_users_email:
insider_users_email_format = "<at email={}></at> "
if validators.email(insider_user_email):
insider_users_email_formats += insider_users_email_format.format(insider_user_email)
else:
print(validators.email(insider_user_email))
changesItems = result['changeSet']['items']
if len(changesItems) > 0:
for c in changesItems:
last_change_info = dict()
last_change_info['commitId'] = c['commitId']
# last_change_info['affectedPaths'] = '\n'.join(c['affectedPaths'])
last_change_info['authorEmail'] = c['authorEmail']
last_change_info['comment'] = c['comment']
last_change_info['date'] = c['date']
last_changes_info.append(last_change_info)
last_changes_info = list(reversed(last_changes_info))
print(last_changes_info)
BUILD_START_TIME = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(result['timestamp'] / 1000))
build_status = "START" if result['result'] is None else result['result']
estimated_duration = result['estimatedDuration']
estimated_duration = int(estimated_duration) // 1000
minutes, seconds = divmod(estimated_duration, 60)
hours, minutes = divmod(minutes, 60)
build_duration = f'{hours}小时:{minutes}分:{seconds}秒'
BUILD_ID = result['id']
THIS_BUILD_URL = result['url']
THIS_BUILD_CHANGES = THIS_BUILD_URL + '/changes'
# # ============ 设置文字颜色 ============
if "SUCCESS" == build_status: # 成功
font_color = "green"
elif "FAILURE" == build_status: # 失败
font_color = "red"
elif "ABORTED" == build_status: # 终止
font_color = "grey"
else:
font_color = "default"
card = {
"type": "template",
"data": {
"template_id": card_id,
"template_variable":
{
"JOB_NAME": JOB_NAME,
"JOB_URL": JOB_URL,
"BUILD_START_TIME": BUILD_START_TIME,
"build_duration": build_duration,
"BUILD_ID": BUILD_ID,
"THIS_BUILD_URL": THIS_BUILD_URL,
"THIS_BUILD_CHANGES": THIS_BUILD_CHANGES,
"build_status": build_status,
"short_description": short_description,
"font_color": font_color,
"last_build_info": last_build_info,
"user_id": user_id,
"insider_users_email": insider_users_email_formats if insider_users_email_formats is not "" else "*<font color='green'>暂未分配知情人,可携带该构建url联系运维永久添加!当然,也可以在构建的时候,insider_users_email参数下自行覆盖填写知情人邮箱!以空格分隔!</font>*",
"last_changes_info": last_changes_info,
"last_changes_info_text": "变更记录详情如下:" if len(last_changes_info) > 0 else "此次变更无变更信息"
}
}
}
send_data = json.dumps({
"chat_id": feishu_chat_id,
"msg_type": "interactive",
"card": card
})
res = feishu_tools(send_data=send_data, app_id=app_id, app_secret=app_secret)
print(res)
if __name__ == '__main__':
jenkins_auth = ('yuhongchao', 'xxx')
app_id = "cli_xxx"
app_secret = "xxx"
feishu_notify(jenkins_auth=jenkins_auth, app_id=app_id,
app_secret=app_secret, card_id="ctp_xxx")
后话
- 如果构建任务使用“This project is parameterized”,那么,可在脚本接收到的parameters字典列表字段,基础上完善其他通知逻辑。
- 除了本文使用的“脚本执行”方式,还有一种可以使用通知插件实现,这种通知插件的方式更加优雅,配合Python drf框架提供的接口即可实现整个逻辑。但是对Jenkins的版本有要求,我司的生产环境Jenkins版本过低,不想升级,所以无法使用这个通知插件+Python Drf框架来实现;有兴趣的朋友可以尝试一下,我这边在测试环境验证过,比较简单也比较优雅。
- 飞书后台消息卡片截图
- Jenkins字符参数配合知情人通知逻辑