本文是 Code Review 系列的第三篇,前两篇可点击下方传送门查看
这个系列计划写3篇(本文是第3篇)
-
中篇:结合 GitLab 的 webhooks、merge request 以及钉钉群 GitLab 消息机器人,实现 code review 的半自动化;
-
下篇:笔者基于 nodeJs 开发的全自动消息通知服务(已开源):结合 GitLab 的 webhooks 和钉钉群自定义机器人,将评论,merge request 等消息及时推送到钉钉群并@对应同学继续后续流程,让整个 code review 更加丝滑;
---------------------------------------我是长长的分割线-----------------------------------
GitLab 上的所有操作,之所以能够把消息推送到对应钉钉群,本质上是借助于 Webhooks 和钉钉群消息机器人的能力,其流程如下图所示
- GitLab 上发生操作,比如 Create Merge Request,会触发 GitLab 自身对应事件的“回调”(自己歪歪的名字);
- 在该回调里,发现对应的事件配置了 Webhooks 相关服务,则会将当前事件的所有信息通过
POST方式发送给对应的服务,也就是 Webhooks 里配置的URL所对应的服务,上图里就是钉钉群”GitLab机器人“服务(自己歪歪的名字); - 钉钉群”GitLab机器人“服务接受到 GitLab 的请求之后,对该请求进行处理,其逻辑是根据不同的“GitLab 事件类型” 推送不同的消息到对应的钉钉群(每个钉钉群机器人的服务
URL都是不一样的);
上述流程中,给钉钉群推送什么样格式的消息是由钉钉群"GitLab机器人"服务来做的,那么如果要自定义推送到钉钉群的消息内容,只需要替换掉钉钉群”GitLab机器人“的默认服务即可,流程图如下所示
区别有两处
- GitLab 的消息推送到自建 Nodejs 服务,由该服务自定义业务逻辑,比如将 GitLab 消息体中的@格式转换为钉钉群消息的@格式;
- 自建Nodejs服务将处理后的数据推送给钉钉群”自定义机器人“服务,这里之所以要用钉钉群”自定义机器人“服务替换钉钉群”GitLab机器人“服务,就是为了实现推送到钉钉群消息的完全自定义;
接下来,就跟着笔者来一步一步搭建一个完整的”自动化“ code review服务吧。
第1步:创建钉钉群自定义机器人
首先,我们创建一个钉钉群自定义机器人,该机器人会对外暴露一个 webhook(本质就是一个 URL 提供服务),通过该 webhook,可以往钉钉群推送任何自定义消息,具体操作路径是:钉钉群设置->群助手->添加机器人->选择自定义机器人->下一步……直至最终创建完成->拷贝webhook url。不熟悉该步骤的同学可参考上一篇有类似步骤或者官方文档,和上一篇中添加机器人唯一不同如下图所示,添加机器人时选择自定义机器人即可。
这里需要额外关注的是,钉钉群自定义机器人接受消息的格式,官方文档有详细描述,笔者采用了 markdown 格式,如下示例
{
"msgtype": "markdown",
"markdown": {
"title":"杭州天气",
"text": "#### 杭州天气 @150XXXXXXXX \n > 9度,西北风1级,空气良89,相对温度73%\n > \n > ###### 10点20分发布 [天气](https://www.dingtalk.com) \n"
},
"at": {
"atMobiles": [
"150XXXXXXXX"
],
"atUserIds": [
"user123"
],
"isAtAll": false
}
}
对应字段的含义如下
- msgtype 表明数据格式,这里选择 markdown;
- title 是通信的标题,可自定义;
- text 是推送到钉钉群的消息主体;
- at 对象里说明要@的用户信息,有两种@方式:1. atMobiles 通过手机号识别用户;2. atUserIds 通过钉钉的用户ID识别用户。
至此钉钉群自定义机器人就创建好了,想要测试也很简单,任意发送一个 POST 请求,请求 URL 就是创建的自定义机器人的 webhook URL,参数直接拷贝如上示例数据即可。
第2步:搭建 Nodejs 服务
Nodejs 服务作为 GitLab 和 钉钉群之间的桥梁,负责接受 GitLab 的请求,对数据进行二次加工,再推送到钉钉群,其核心逻辑也很简单
- 对外暴露一个服务接口,接受 GitLab 的
POST请求; - 处理完 GitLab 的请求之后,发起一个
POST请求,将数据推送给钉钉群;
接下来就分步解释一下 Nodejs 服务的核心代码实现(完整代码已在 GitHub 上开源,如果觉得有帮助记得 star 哦)
2.1 入口文件
该文件包含内容如下
- 暴露一个接口,名称为
/code-review; - 根据 GitLab 的事件类型,将请求分发到对应的服务,比如 merge request 由其对应的服务来处理;
源码
import Koa from "koa";
import bodyParser from 'koa-bodyparser';
import Router from 'koa-router';
import fs from 'fs';
import actionComment from './actions/comment';
import actionMergeRequest from './actions/merge_request';
import actionPush from './actions/push';
import actionTagPush from './actions/tag_push';
import actionIssue from './actions/issue';
import actionBuild from './actions/build';
import actionPipeline from './actions/pipeline';
import actionWiki from './actions/wiki';
interface ICtx {
request: any;
body?: any;
}
const app = new Koa();
const router = new Router();
// DingTalk group code review url
const defaultDingtalkUrl = 'https://oapi.dingtalk.com/robot/send?access_token=ae02abd824a4c628b97dd95a2ce3f2a67303cccfe38b6c3dd2aee2c6efb8c169';
router.post('/code-review', async (ctx: ICtx) => {
const { dingTalkUrl } = ctx.request.query || {};
const gitlabDataFromWebHook = { dingTalkUrl: dingTalkUrl || defaultDingtalkUrl, ...ctx.request.body, };
fs.writeFile('log.txt', JSON.stringify(gitlabDataFromWebHook), () => {
// 1. 简单的日志记录
});
const {
object_kind, // 2. GitLab 的事件类型
} = gitlabDataFromWebHook || {};
// 3. 根据不同事件类型,分发给不同的服务
// 不同的服务本质上都是类似的,对 GitLab 数据进行深加工,然后发送特定格式的数据到钉钉群
switch (object_kind) {
case 'merge_request':
// 4. 接下来就以 merge request 事件为例来分享一下处理逻辑
actionMergeRequest(gitlabDataFromWebHook);
break;
case 'note':
actionComment(gitlabDataFromWebHook);
break;
case 'push':
actionPush(gitlabDataFromWebHook);
break;
case 'tag_push':
actionTagPush(gitlabDataFromWebHook);
break;
case 'issue':
actionIssue(gitlabDataFromWebHook);
break;
case 'build':
actionBuild(gitlabDataFromWebHook);
break;
case 'pipeline':
actionPipeline(gitlabDataFromWebHook);
break;
case 'wiki_page':
actionWiki(gitlabDataFromWebHook);
break;
default:
// nothing
}
ctx.body = "hello code review";
});
app
.use(bodyParser())
.use(router.routes())
.use(router.allowedMethods());
app.listen(50001);
- 使用 koa 和 koa-router 启动一个 Nodejs 服务,对 koa 不熟悉的同学可前往其官网查看相关文档,也是很简单的;
- 依赖 Nodejs 内置模块 fs 进行日志写入,这个也是 Nodejs 的基础模块,对 Nodejs 不熟悉的同学可前往其官网查案相关文档;
2.2 特定 GitLab 事件处理逻辑
源码
import request from '../utils/request'; // 1. request 是对 axios 的封装
import userList from '../config/user_list'; // 2. user_list 是一个对象,键为 GitLab 开发同学的用户名,值为其在钉钉群的手机号
export default function mergeRequest(gitlabDataFromWebHook: any) {
const {
dingTalkUrl,
object_kind,
user: {
name: applyerName,
username: applyerUsername,
},
assignee: {
name: assigneeName,
username: assignUsername,
},
object_attributes: {
action,
url: gitActionUrl,
title,
description,
source_branch,
target_branch,
state: actionState,
},
project: {
name: projectName,
},
} = gitlabDataFromWebHook;
// 3. 如果该 merge request 没有选择 assignee, 则@该 merge request 提交人,通知她/他要选择 assignee(就是负责 review 的同学)
// 4. 只有当 merge request 是opened状态才@对应同学,其他状态不处理
// 5. 这里 @ 的实现其实就是把对应的 GitLab 用户转换为钉钉群用户,底层依赖 user_list 文件
// user_list 文件是一个以 GitLab 用户名为键,以钉钉群用户手机号为值的对象
// 那转换就简单了,其实就是用户信息的简单替换
let assigneeStr = '';
if (actionState === 'opened') {
assigneeStr = `@${userList[assigneeName] || userList[assignUsername]}`;
// if no assignee then @ the applyer him/her self
if (!userList[assigneeName] && !userList[assignUsername]) {
assigneeStr = `@${userList[applyerName] || userList[applyerUsername]} 要找谁帮你merge代码呢?记得选择Assignee哦`
}
}
if (actionState === 'merged') {
const {
object_attributes: {
// last_commit is exist this case
last_commit: {
author: {
email: applyerEmail,
},
},
},
} = gitlabDataFromWebHook
const lastCommitUsername = applyerEmail.replace('@mistong.com', '');
if (lastCommitUsername !== applyerName) {
// only @ user if this actions is not dispatched by him/her self
assigneeStr = `@${userList[lastCommitUsername] || '佚名'}, you can go on now`;
}
}
// 6. 将所有数据拼装为钉钉群机器人要求的格式
const text = `${applyerName} ${action} the ${object_kind} from ${source_branch} to ${target_branch} ${assigneeStr} \n `
+ `> [${title}](${gitActionUrl}) \n `
+ `> ###### [${description}](${gitActionUrl}) \n `
+ ` > ###### Status: ${actionState} \n `
+ `> Repository: ${projectName} \n `
+ `> ###### [${gitActionUrl}](${gitActionUrl})`;
// 7. 向钉钉群发送消息
request(dingTalkUrl, text);
}
user_list 代码简单示意如下
const userList: any = { zhangsan: '135xxxxxxxx',};
export default userList;
request 代码示意如下
import axios from 'axios';
import userList from '../config/user_list';
import dingTalkCustomPrefix from '../config/dingtalk_custom_prefix';
export default function request(dingTalkUrl: string, text: string) {
axios.post(
dingTalkUrl,
{
"msgtype": "markdown",
"at": {
// TODO: this could be optimized with real @ user
"atMobiles": Object.values(userList), // @ need to config 2 place, this is first place
"atUserIds": [],
"isAtAll": false
},
"markdown": {
"title": dingTalkCustomPrefix,
// // @ need to config 2 place, this is second place
"text": `#### ${dingTalkCustomPrefix}: ${text}`,
},
}
).then((res: any) => {
console.log(res.data);
});
}
关于@钉钉群用户,需要设置2个地方(本文以用户手机号为媒介来实现)
-
atMobiles 字段要将需要@的用户手机号传进去
-
text 正文里也必须有 @目标用户手机号 的文本
假设我们要 @ 一个手机号为 19999999999 的用户,必须的数据格式如下所示
{
"msgtype": "markdown",
"at": {
"atMobiles": [19999999999]
},
"markdown": {
"text": `@19999999999 同学你好,我要圈你了`,
},
}
当然也可以使用群用户的userId来实现@功能,这个找公司的管理员要到权限即可,笔者采用了更简单的方式,直接采集群用户的手机号。
需要注意的点
- Nodejs 服务必须部署在一个公网可以访问的服务器上,这样 GitLab 才能访问到该服务,笔者是部署在阿里云 ECS 上的;
- 另外,关于 GitLab 各种不同事件推送的数据格式,可参见其官网有详细说明,笔者的 Nodejs 服务也对各种不同的 GitLab 推送数据格式进行了本地 mock, 方便本地开发调试,具体路径为
src/events_mock_data;
其余部分的代码比较简单,这里就不展开了,对 Typescript 不了解的同学可点击传送门,Typescript 是 JavaScript 的超集,新增了类型定义,入门也是很容易的。
基于 Nodejs 的服务部署好了之后,接下来,只需完成最后一步即可。
第3步:配置 GitLab 的 webhooks
具体配置的完整流程可参见上一篇,这里只示意一下核心部分。假设部署的 Nodejs 完整服务地址是 http://12.84.77.36:50001/code-review ,将该 url 添加到 GitLab 项目的 webhooks 即可,如下示意
至此,一个完整的流程就完成了,在 GitLab 上创建一个 merge request,对应的钉钉群就会收到消息了,@群用户的能力也有了,剩下的,就是根据自己的团队需求,自定义@逻辑即可。
完整流程图如下