一、场景
日常一般会通过jira bug面板筛选条件筛选出bug,如:经办人、状态、标签等,但这需要人为主动去筛选搜索,如果有一个定时每日钉钉群、飞书群提醒每个人截止当日bug情况就比较直观,对测试团队成员进度把控也有一定辅助左右。
二、方案设计
群消息,对应人员点击消息链接,跳转具体页面。
这边是通过jira issue 筛选器获取到URL
status = 已解决 AND assignee in (康丽婷) ORDER BY priority DESC, updated DESC
将jira issue页面筛选后的url进行解码,获得具体地址
UrlEncode编码/UrlDecode解码 - 站长工具
三、server RESTful API
(一)钉钉机器人 webhook 对接
1.封装
dingtalk-rebot.js
'use strict'
const assert = require('assert')
const exec = require( 'child_process' ).exec
class DingTalkRobot {
constructor (accessToken) {
assert(accessToken, 'accessToken is necessary!')
this.accessToken = accessToken
}
/**
* text 类型
* text String 必填 文本内容
* isAtAll 选填 是否抄送所有人
*/
sendText(text = '', isAtAll = false) {
this.send({
msgtype: 'text',
text: {
content: text
},
at: {
isAtAll: isAtAll
}
})
}
/**
* link类型
* text String 必填 文本内容
* title String 必填 消息标题
* picUrl String 必填 展示图片
* messageUrl String 必填 点击消息跳转的URL
*/
sendLink(linkObject) {
this.send({
msgtype: 'link',
link: linkObject
})
}
/**
* 发布markdown 消息
* {
* "title":"杭州天气",
* "text": "#### 杭州天气\n"
* }
* @param markdownContent
*/
sendMarkdown(markdownContent) {
const { title = '无题', text = ''} = markdownContent
this.send({
msgtype: 'markdown',
markdown: {
title,
text
}
})
}
send(contentBody) {
exec( `
curl 'https://oapi.dingtalk.com/robot/send?access_token=${ this.accessToken }' \
-H 'Content-Type: application/json' \
-d '${ JSON.stringify(contentBody) }'
`)
}
}
module.exports = DingTalkRobot
2.引入
dingtalk-rebot.js
var DingTalkRobot=require('../dingtalk-rebot')# 引入封装文件dingtalk-rebot.js
let robot = new DingTalkRobot('accesstoken')
robot.sendText('jirabug hello world')
// robot.sendText('hello ding talk', true) // 抄送所有人
robot.sendLink({
text: 'jirabug hello dingtalk',
title: 'hello wold',
picUrl: 'https://placeholdit.imgix.net/~text?txtsize=14&txt=FreeGroup.org+Cool&w=800&h=600',
messageUrl: 'http://baidu.com'
})
accesstoken替换成钉钉机器人webhook url上面的accesstoken
(二)JIRA Client RESTful API
1.获取指定账号下jira bug数据
// With ES5
var JiraApi = require('jira-client');
var jira = '';
var fs = require('fs');
describe('JIRA API', async () => {
beforeEach('Initialize', async () => {
// Initialize
jira = new JiraApi({
protocol: 'https',
host: 'jira.homeking365.com', // jira域名
username: 'jira账号',
password: 'jira密码',
apiVersion: '2',
strictSSL: true
});
});
/**
* ES6
* @description 获取指定账号提交的issue (获取某个账号所提交的所有bug)
* @param username: string
* @param open: boolean issue状态是否打开
* @method getUsersIssues(username: string, open: boolean): *
* @returns object {"startAt":0,"maxResults":50,"total":0,"issues":[]}
*/
it('getUsersIssues', async () => {
try {
var result = await jira.getUsersIssues('liyc', false);
// await console.log(`getUsersIssues: ${JSON.stringify(result.issues)}`);
// 筛选bug
var bugListOpen = []; // 激活
var bugListClose = []; // 已关闭
var bugListResolve = []; // 已解决
var bugListReopen = []; // 重新打开
// bug分类统计
for (const iterator of result.issues) {
switch (iterator.fields.status.name) {
case '激活':
bugListOpen.push(iterator);
break;
case '已关闭':
bugListClose.push(iterator);
break;
case '已解决':
bugListResolve.push(iterator);
break;
default:
bugListReopen.push(iterator);
break;
}
}
// 激活 数量
console.log('激活 bug数量:', bugListOpen.length);
// 已解决 数量
console.log('已解决 bug数量:', bugListResolve.length);
// 已关闭 数量
console.log('已关闭 bug数量:', bugListClose.length);
// bugListReopen 数量
console.log('bugListReopen bug数量:', bugListReopen.length);
} catch (error) {
await console.log(`getUsersIssues error:${error}`);
}
});
});
四、应用配置
(一)钉钉
1.钉钉机器人配置
(1)使用Webhook
(2)选择关键字,输入关键字
(3)获得WebHook
(二)飞书机器人 webhook 对接
1.新建自定义机器人
飞书群设置>添加机器人>自定义机器人>
2.安全设置
自定义关键词,当发送内容含有自定义关键词,才能成功发送钉钉机器人消息到群里!
3.lib库
(1)crypto.js
const crypto = require('crypto');
/**
* 签名
* @param {string} timestamp - 10位时间戳
* @param {string} secret - 密钥
* @returns {string} - 签名
*/
exports.genSign = function (timestamp, secret) {
const key = `${timestamp}\n${secret}`;
const sign = crypto.createHmac('sha256', key).update('').digest('base64');
return sign;
}
(2)index.js
const Robot = require('./robot');
const utils = require('./utils');
module.exports = function (option) {
checkInput(option);
const { webhook, secret } = option;
return new Robot(webhook, secret);
};
/**
* 参数检查
* @param {string} option.webhook
* @param {string?} option.secret
*/
function checkInput(option) {
const { webhook, secret } = option;
if(!utils.exist(webhook) || !utils.isString(webhook)) {
throw new TypeError('webhook is required and expect string');
}
try {
new URL(webhook);
} catch (err) {
throw new TypeError('webhook expect URL');
}
if (utils.exist(secret) && !utils.isString(secret)) {
throw new TypeError('secret expect string');
}
}
(3)robot.js
const crypto = require('./crypto');
const utils = require('./utils');
const simpleClient = require('simple-client');
/**
* 飞书群聊机器人
* @param {string} webhook - webhook地址
* @param {string} secret - 密钥
* @return {Robot}
*/
function Robot(webhook, secret) {
this.webhook = webhook;
this.secret = secret;
}
/**
* 发送文本消息
* @param {string} text 文本
* @return {Promise} - resolve({ statusCode, statusMessage, data }), reject(err);
*/
Robot.prototype.sendText = function (text) {
const data = {
msg_type: 'text',
content: {
text: text
}
};
return this.send(data);
}
/**
* 发送富文本消息
* @param {string} title 标题
* @param {array[]} texts 富文本消息按照飞书格式数组中每个元素是数字,代表一行数据 element: [{ tag, text, ... }]
* @param {string} language - 语言默认中文zh_cn
* @return {Promise} - resolve({ statusCode, statusMessage, data }), reject(err);
*/
Robot.prototype.sendRickText = function (title, texts, language) {
const defaultLanguage = 'zh_cn';
language = utils.exist(language) ? language : defaultLanguage;
const data = {
msg_type: 'post',
content: {
post: {
[language]: {
title: title,
content: texts
}
}
}
};
return this.send(data);
}
/**
* 发送消息
* @param {object} data - 对应飞书消息格式
* @returns {Promise} - resolve({ statusCode, data }), reject(err);
*/
Robot.prototype.send = function(data) {
if (utils.exist(this.secret)) {
const timestamp = utils.genTimeStamp();
const sign = crypto.genSign(timestamp, this.secret);
data.timestamp = timestamp;
data.sign = sign;
}
return simpleClient.post(this.webhook, { body: data });
}
module.exports = Robot;
(4)utils.js
/**
* @method 生成10位时间戳
* @returns string 时间戳
*/
function genTimeStamp() {
let timestamp = Date.now();
return String(timestamp).slice(0, 10);
}
function exist(value) {
return null != value;
}
function isString(value) {
return typeof value === 'string';
}
module.exports = {
genTimeStamp,
exist,
isString,
}
(5)test-feishu-robot.js
// 飞书群机器人webhook
var webhook;
// 引入飞书机器人
var robot;
// setup
before("setup", () => {
// 飞书群机器人webhook
webhook = 'https://open.feishu.cn/open-apis/bot/v2/hook/xxx';
// 引入飞书机器人
robot = require('../../src/feishu/lib/index.js')({
webhook
});
});
// teardown
after("teardown",()=>{
console.log("done")
})
describe('发送消息', async () => {
it('普通文本', async () => {
try {
var data = await robot.sendText('JIRA bug');
console.log(data);
} catch (error) {
console.log(error);
}
});
it('富文本', async () => {
var message = {
title: 'JIRA bug 日常提醒',
texts: [
[
{ tag: 'text', text: '张三:其余标签请' },
{
tag: 'a',
text: '点击链接',
href: 'https://open.feishu.cn/document/ukTMukTMukTM/uMDMxEjLzATMx4yMwETM'
}
]
]
};
try {
var data = robot.sendRickText(message.title, message.texts, 'en_us');
console.log(data);
} catch (error) {
console.log(error);
}
});
});
// 富文本
/**
(async () => {
try {
var data = robot.sendRickText(message.title, message.texts, 'en_us');
console.log(data);
} catch (error) {
console.log(error);
}
})()
*
*/
4.相关文档
(1)旧版
(2)新版
(三)其他应用配置方式
【语雀】
https://jingyan.baidu.com/article/c1a3101ebcc3d19f656deb8c.html
【gitee】
https://gitee.com/help/articles/4135
【github】
https://blog.csdn.net/Q563573095/article/details/79580249
【gitlab】