实现gitlab PR自动流程处理机器人

公司团队是使用gitlab来管理源代码的,一直以来当提交了一个PR后,需要手动在内部IM群里贴出PR链接和摘要,然后@目标同事来帮忙review代码,之后就不时地手动刷新页面查看是否有足够的人review完了。这种方式比较原始费时,仔细想想其实大部分流程是可以自动化的。

一些做的好的IM可以实现自定义机器人,例如飞书(lark),如果机器人可以知道有人发了PR,同时又知道具体有哪些人需要review PR,那么就可以自动在IM群里把这个PR的信息发出来,同时@对方。然后当机器人知道PR通过了后就可以给PR作者私聊发通知,就可以开开心心合并代码了。整理下具体流程:

1
提交PR -> 群里发通知+@对方 -> PR通过 -> 私聊发送通知 -> 合并PR

全程只有第一步和最后一个需要PR作者操作,中间的步骤都可以由机器人完成。

如何知道有人发了PR

这里的标题有一点误导,我们真正想知道的不是有没有人提交PR,而是那些需要被人reviewPR。我们可以约定只有在PR的评论里@了同事,才认为这个PR需要review. 恰好gitlab提供了一种webhook机制,可以在发生了特定的事件时请求指定的apiwebhook的设置路径是项目仓库 -> Settings -> Integrations,如图:

webhook

其中的URL是自定义的,我们可以将它指向我们的一个后端apigitlab会往这个api发送一个POST请求,携带事件的详细信息。Trigger我们设置为Comments,表示每次有人在PR里发表了评论都会触发api

在设置了webhook后,点击它的Edit按钮,可以看到在什么时候触发了webhook:

webhook-list

webhook-call-list

还可以看到每次触发时传递的参数:

webhook-call-detail

里面的信息非常丰富,每次评论的内容也在其中,还可以根据已有的信息再次调用gitlab openapi来获取更多信息。

以上,每次有人在PR里发表评论了,我们的后端api都会收到一个请求,接下来的问题是如何判断评论里是否含有有效的@.

提取@对象

首先评论的具体内容是放在了请求参数的object_attributes.note属性中,它是一个字符串例如@xiaoming @zhangsan 来看看我的PR啊~。什么是有效的@目标呢? 就是@的对象确实是同事的名字,而不是随便写的,例如@123就不是有效的@。 我们把这件事拆分一下,首先无脑提取所有的@对象,可以用以下代码:

1
2
3
4
5
6
7
const atReg = /@[^\s]+/g

// 获取单条note中@的列表
function getNoteAtList ( note ) {
return ( note.match( atReg ) || [] )
.map( at => at.slice( 1 ) ); //去除前面的@符号
}

它会返回形如['xiaoming', 'zhangsan']的数组,接下来就是判断每个元素是否有效的目标。这里可能不同公司的业务不一样,我的思路是判断目标是否为gitlab里的注册用户,可以用gitlabusers api,可以查询用户名匹配指定字符串的所有用户。我们利用这个api看返回的数组是否为空,不为空取第一个。则整个过滤逻辑如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 过滤@列表,只留下真实存在的gitlab用户, privateToken为gitlab openapi需要的鉴权token
async function filterAtList ( atList = [], privateToken ) {
const distinctList = [ ...new Set( atList ) ]; // 去重
const result = await Promise.all( distinctList.map( username => getGitlabUser( username, privateToken ) ) );
return result.filter( Boolean ); // 去除空值
}

// 根据username在查找User信息
async function getGitlabUser ( userName, privateToken ) {
const users = await getUser( {
userName,
privateToken,
} )

if ( !users || !users.length )
{
return null;
}
const target = users[ 0 ];
return {
username: userName,
name: target.name,
id: target.id,
};
};

async function getUser ( params ) {
const { userName, privateToken } = params;
const request = await getGitlabRequest( privateToken ); // 基于axios进行的封装,设置了baseURL及鉴权header
try
{
const result = await request.get( `/users`, {
params: {
username: userName,
}
} );

return result.data || [];
} catch ( e )
{
console.error( 'get_gitlab_user_info', e );
return [];
}
}

调用gitlab openapi需要鉴权的private_token,获取步骤: 进入gitlab -> 右上角个人头像 -> Settings -> Access Tokens,然后就可以增加一个token了,注意tokenscopes全选.

private_token

以上,这样我们就拿到了所有有效的@目标,接下来就是如何在IM群里利用机器人@他们。

机器人自动@目标

这个基本就是调用各IM工具的open api了,举一个飞书的例子:

1
2
3
4
5
6
7
8
9
const atText = atList.map( at => `<at user_id="${ at.userId }">@${ at.name }</at>` );
const textMsgConfig = {
open_chat_id, // 群id
msg_type: 'text',
content: {
text: atText,
}
}
await request.post( '/send/message/', textMsgConfig ); // 在群里发送通知

只需提供@目标的userIdname即可,可以同时@多人。同时需要额外再提供群的id,这样机器人才知道该把消息往哪里发送。

置于怎么申请IM机器人,这个也是不同IM不一样,这里不赘述。

以上,我们就能做到当在PR里@了一些目标同事后,机器人自动在群里@同事了。

通知merge PR

接下来要解决的问题是当有足够多的人觉得PR是ok的,实时通知PR作者来merge PR。这里涉及到2个问题:

  1. 怎么判断一个PR是ok的
  2. 怎么判断有足够多的人觉得PR是ok的

第一个问题可以约定一个暗号:当在PR中评论了LGTM(Look Good To Me)即认为PR是ok的,当然也可以用其他暗号。

第二个问题的核心是: 判断一个PR的所有评论中是否有足够的指定字符串。首先需要获取所有评论,这个依然可以通过gitlab openapi来获取,然后就依次对每条评论的内容做字符串匹配即可。核心代码示范:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 此PR所有的评论
const discussions = await getPrDiscussions( {
pid,
mrId,
privateToken
} );

const lgtmList = getLgtmList( discussions, author ) || []; // 回复了LGTM的评论列表
// 如果还没有到阈值则不回复消息
if ( lgtmList.length < lgtmThreshold )
{
return;
}

// 文本消息
const nameList = lgtmList.map( note => note.author.name );
const lgtmSummary = `${ nameList.slice(0,3).join( '、' ) }${ nameList.length > 3 ? `等${nameList.length}人` : '' }对你的PR回复了LGTM, 你可以Merge此PR了`;
const textMsgConfig = {
email, // PR作者的邮箱
msg_type: 'text',
content: {
text: lgtmSummary,
}
}
await request.post( '/send/message/', textMsgConfig ); // 发送私聊通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 筛选出非PR作者写的note, discussion 与 note是一对多的关系
function getNonAuthorNotes ( discus, authorId ) {
return ( discus.notes || [] ).filter( note =>
`${ note.author.id }` !== `${ authorId }` && // 非作者的PR
!note.system && // 不是系统发的
!note.resolved // 没有resolve
);
}

// 获取回复了LGTM的评论列表
function getLgtmList ( discussions, author ) {
const distinctMap = {}; // 去重,同一个人的多次lgtm只算一次

// 获取所有非作者的评论
const noteList = discussions.reduce( ( noteList, discus ) => noteList.concat( getNonAuthorNotes( discus, author.id ) ), [] );

return noteList
.filter( note => note.body.trim().toUpperCase() === 'LGTM' ) // 过滤出评论了LGTM的
.filter( note => { // 去重
const { id } = note.author;
if ( !distinctMap[ id ] )
{
distinctMap[ id ] = 1;
return true;
}
return false;
} )
}

以上我们就可以及时给PR作者发送merge通知了。

merge PR

最后一件锦上添花的事情,当收到merge通知后,当然可以手动打开PR连接然后点击merge按钮,不过这样显得不够极致,能不能让作者直接在IM工具内点击某个按钮或链接,然后由机器人自动帮助其合并呢?这样就更方便好用了!

当然这么做的前提是IM工具支持富文本类型消息,或者可交互的消息,只要最终可以往指定链接发送GETPOST请求即可。

gitlab提供了直接merge PRopenapi,不过其只支持PUT类型请求,同时需要携带PR的关键信息。如果IM工具不支持在消息内再发送PUT类型请求,那么只能做一次中转:先发送一个普通的get/post请求到我们自己的某个后端api,然后在这个api内再发送最后的PUT请求。核心代码示范如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 发送支持交互的消息
const interacriveMsgConfig = {
// ... 其他相关配置
method: "post",
url: 'xxx', // 中转后端api
parameter: {
pid,
mrId,
privateToken,
squash, // 是否squash commit
removeSource, // 是否删除源分支
},
}

// 发送交互类型消息
await request.post( '/send/message/', {
email,
msg_type: 'interactive',
content: {
card: interacriveMsgConfig,
},
} );

然后在后端api发送PUT请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 合并PR
module.exports = async function ( params, context ) {
const {
pid,
mrId,
privateToken,
squash,
removeSource,
} = params;

const gitlabRequest = await getGitlabRequest( privateToken );

let msg = '';
let status = 200;
try
{
// 发送PUT请求
const result = await gitlabRequest.put( `/projects/${ pid }/merge_requests/${ mrId }/merge`, {
id: pid,
merge_request_iid: mrId,
squash,
should_remove_source_branch: removeSource
} );
console.log( 'SUCCESS merge pr, result => ', result );
msg = `【 SUCCESS 】: PR is merged!`;
} catch ( error )
{
console.error( 'FAIL merge pr, error => ', error.response );
( { status, statusText } = error.response );
msg = `【 FAILED 】: ${ statusText }, ${ errorMap[ status ] }`;
}

console.log( 'merge pr ,final function return => ', { status, msg } );
context.status( status ); // 设置响应状态码
return { status, msg };
}

以上我们就做到了PR的全程自动化! 整个机器人的代码其实就是一个node服务,找个服务器部署起来即可。

完整的源码可以参照github.