目录

Code Review 机器人——架构设计

项目整体设计遵循 「面向抽象编程,而不是面向细节编程」 理念,旨在打造一个组件可插拔、可替换、极易拓展的通用 Code Review 机器人。

一、项目概述

1.1 背景

无论是开源项目还是私有项目,现代成熟软件一般由多人协作完成。在此背景下,Code Review 成为了软件研发中必不可少的一环,极大地保证了代码质量。

保证 Code Review 持续进行不阻塞,分析当前 Pull Request 需要 Reviewer 的进一步操作,还是需要 Committer 修改代码,并及时通知各方,也非常重要。

1.2 项目需求

开发一个 Code Review 机器人,通过 helm 部署,可以通过 API/Webhook 的方式获取 GitHub/GitLab 上的Pull-Request/Merge-Request 动态,然后分析当前 Code Review 是被谁阻塞了,进而将消息发送到飞书群中,艾特相应人员“及时处理”。

1.3 项目设计理念

项目整体设计遵循 「面向抽象编程,而不是面向细节编程」 理念,旨在打造一个组件可插拔、可替换、极易拓展的通用 Code Review 机器人。

项目的核心引擎,将用户、代码仓库、通讯平台、消息发送等常用操作全部抽象成了接口,项目设计初期就支持多代码平台、多通讯平台、多交互方式。

同时,模块化的项目设计,带来了以下优点:

  1. 模块间低耦合,易测试。同时为多人协作奠定良好基础。

  2. 易拓展,想要支持新的平台,只要实现相应的平台接口,调用 注册 函数即可。

  3. 可插拔,可替换。可轻松开启/关闭各平台,替换各模块组件。

  4. 抽象常见概念、常用操作,屏蔽了各平台、各操作之间的差异,使得核心引擎无障碍调用各自定义组件。如任意代码平台的PR信息推送到任意通讯平台指定用户。

二、项目详细方案

2.1 整体架构设计

概念介绍:

  • 代码托管平台(Git):GitHub、GitLab等

  • 通讯平台(Community):飞书、钉钉等,用于通知发送与用户交互

  • 组件注册:调用代码托管平台与通讯平台的注册函数,将实际的 Client (如 GitHub、飞书)注入到核心平台管理引擎。

https://bird-notes.oss-cn-hangzhou.aliyuncs.com/img/cr-bot.png

核心模块概要介绍:

  • 代码托管平台——管理中心:负责管理 GitHub、GitLab 等代码托管平台,用户实现的此类自定义平台可通过「组件注册」注册到管理中心。

  • 通讯平台——管理中心:负责管理 飞书、钉钉等通讯类平台,用户实现的此类自定义平台可通过「组件注册」注册到管理中心。

  • 异构平台用户融合:将一个 Git 用户绑定至多个 Community 用户。并向其他系统组件提供「跨平台用户映射器」。

  • PR阻塞分析引擎:根据「代码托管平台-管理中心」提供的 Git Clients 集群获取最新的仓库、PR 动态,分析出每个 PR 被谁阻塞了,生成「抽象通知消息」。

  • 消息渲染/发送引擎:此模块其实是「通讯平台」的一部分,即每个通讯平台都可根据「抽象通知消息」以不同的方式渲染,并完成消息发送动作。

  • 用户响应记录器:不同的通讯平台都能收到用户对于通知信息的实际响应,其可调用「用户响应-代理」,将响应持久化至数据库中。此模块将作为后续消息发送的依据之一,如,长时间未响应,多次发送并通知管理员。

2.2 系统运行流程

  1. 准备阶段

    1. 程序启动,加载配置,各代码平台,通讯平台注册至核心引擎中。

    2. 从配置中读取跨平台用户绑定关系;从数据库中读取绑定关系;用户通过通讯平台与机器人进行交互绑定关系,并记录至数据库中。

  2. 分析/通知阶段

    1. 「触发器」触发分析/通知流程。

    2. 「PR阻塞分析引擎」获取所有已注册的 Git 类平台及其项目对应的最新 PR 进度,从中分析出受阻原因、阻碍人 Git 账号。并返回抽象通知消息。

    3. 跨平台用户映射器,根据 Git 账号,找出其所有的通讯平台账号。

    4. 各通讯平台,将抽象消息渲染为各平台特定格式消息,发送给指定人。

  3. 通知响应阶段

    1. 用户受到通知,与机器人进行交互

    2. 通讯平台将用户响应发送给「用户响应——代理」,持久化至数据库中

    3. 用户的历史响应将作为下一次通知发送间隔的重要依据

2.3 组件详细设计

2.3.1 代码托管平台——管理中心

https://bird-notes.oss-cn-hangzhou.aliyuncs.com/img/2022-05-27-00-13-32-image.png 如图,管理中心在项目启动时,初始化一个保存了 Git Platform interface的空 map

管理中心内部并不预先定义任何实际的代码托管平台,全部通过 RegisterPlatform 函数将实现了 Git Platform 的实际 Client 注册至其中。

并通过 GetPlatformMapGetPlatformByType 两个函数供其他组件获取其托管的所有 Git 平台。

Git Platform 的接口定义如下(以后会随着对业务的深入了解微调接口):

type Platform interface {
    GetType() PlatformType    // 获取平台类型
    GetUserInfoByID(userID string) (User, error)// 根据用户ID获取用户信息
    GetRepoInfo(repoName string) (Repo, error)  // 根据仓库名获取仓库信息
    ListRepos() []Repo    // 获取配置中定义的属于该代码托管平台的所有仓库信息
    // 列出某仓库的所有 Issue,可设置筛选条件
    ListIssuesByRepo(repo Repo, filter IssueFilter) ([]Issue, error)
    // 列出某仓库的所有 PR(及其评论、Committer、Reviewer等信息),可设置筛选条件
    ListPrsByRepo(repo Repo, filter PrFilter) ([]PullRequest, error)
}

type PlatformType string

上述代码的 UserRepoPR 等也均是接口,不同的代码托管平台实现类应该实现 Platform 接口及 RepoPR 等接口。供「PR阻塞分析引擎」统一调取接口及分析。

以下是 User 等接口定义:

type User interface {
    // 这里的用户ID只是平台唯一标识。可以是Github用户名,也可以是 GitLab 的邮箱
    // 不同的代码平台可自行解释与定义,保持唯一即可
    GetUserID() string    
    // 获取用户的来源于哪个代码托管平台,然后可进一步调用平台的能力接口
    OfPlatform() Platform
}


// 时间属性接口,
// PR、评论等接口都包含了此接口,
// 可用于时间线排序,进而进行阻塞分析
type TimeAt interface {
    CreatedAt() time.Time
    UpdatedAt() time.Time
}


type Issue interface {
    TimeAt

    GetID() int64
    GetNumber() int
    GetTitle() string
    GetBody() string
    GetLabels() []string
    GetAssignees() []User
    ListComments() []Comment
    GetState() IssueState
    GetURL() string
}

type Comment interface {
    TimeAt

    GetID() int64
    GetBody() string
    GetUser() User
    GetURL() string
}


type Commit interface {
    TimeAt

    GetID() int64
    GetMessage() string
    GetURL() string
    GetAuthor() User
    GetCommitter() User
}

type PullRequest interface {
    GetID() int64
    GetNumber() int
    GetState() PrState    // PR 状态,open close 
    GetTitle() string
    GetBody() string
    ListLabels() []string
    GetCommitter() User
    ListAssignees() []User
    GetURL() string

    ListComments() []Comment
}

type Repo interface {
    //GetID() int64
    GetName() string
    GetDescription() string
    GetURL() string
    OfPlatForm() Platform
}

注:项目拟采用 https://github.com/google/go-github 作为 GitHub go sdk。已进行基本信息拉取接口测试。

2.3.2 通讯平台——管理中心

https://bird-notes.oss-cn-hangzhou.aliyuncs.com/img/2022-05-27-00-34-16-image.png

与代码托管平台管理中心类似,通讯平台管理中心管理了各个通讯平台的 Client,注册逻辑也相似。 这里不赘述接口列表了,Community 接口能力示意如下:

https://bird-notes.oss-cn-hangzhou.aliyuncs.com/img/2022-05-27-00-39-53-image.png

  • 用户/群组信息获取类接口:应当能通过 UserID 获取用户详细信息,通过群组ID获取到群组信息,为消息显示、消息发送提供基本信息

  • 消息渲染类/发送类接口:每个平台都有其特有的消息发送格式。如飞书有独特的消息卡片,钉钉有 「Ding」消息,微信有消息推送。平台可以根据「PR阻塞分析引擎」分析后得到的抽象消息,渲染出属于当前平台的独特消息格式,并完成消息的发送。

值得一提的是,上述的接口为通用能力接口,每个平台还将发起一些 WebHook 以监听用户消息与交互动作。

如,若机器人在飞书群内收到了 @cr-bor bind github aFlyBird0 ,飞书 Client 将调用「异构平台用户融合模块」提供的接口,而后用户融合模块会将发送消息的飞书用户绑定至 GitHub 平台的 Git 用户 aFlyBird0 上。

系统还定义了一个可选的 NeedRefresh 接口,系统将定时从管理中心获取所有 Community Client,尝试类型断言此接口,成功的话将调用接口内的函数。各通讯平台可在此接口内实现对群组、用户等信息的刷新。

注:项目拟采用 https://github.com/chyroc/lark 作为 飞书 go sdk。已进行事件订阅/卡片消息发送/互动等接口的基本测试。

2.3.3 异构平台用户融合模块

所有的 PR 阻塞分析都是建立在 Git 类平台上的,直接追溯到的用户也必定是 Git 类用户,所以系统需要一个将 Git 用户绑定至通讯平台的机制。目前设计的是一对多,即一个 Git 账号可在每个通讯平台都绑定一个社交账号。

关于绑定方式,原先采用的是纯配置文件的方式,如下图:

- git:
    github: github-user
- community:
    feishu: feishu-user
    dingtalk: dingtalk-user

模块内通过定义合理的数据结构,即可从配置文件中完成对异构平台用户的融合。

值得一提的是,这里的用户 ID 都只是唯一 ID 而已,实际上可以是用户名、邮箱。各平台可自行选取唯一ID的字段,如 GitHub 可用 UserID,钉钉可用手机号等等。

后面在做可行性分析时发现,可能并不是所有的用户都能方便地提取出唯一 ID。

所以设计了一个「用户绑定接口」,各通讯平台可发起一个 WebHook 监听,自定义用户互动逻辑,如,机器人在飞书群内收到了 @cr-bor bind github aFlyBird0,则可调用此接口增加绑定信息。

https://bird-notes.oss-cn-hangzhou.aliyuncs.com/img/2022-05-27-01-26-33-image.png

Git 仓库与通讯平台的群组的绑定也是类似的,不再详述。 目前的设计是,一个 Git 仓库,在每个通讯平台能且仅能绑定一个群组。

2.3.3 PR阻塞分析引擎

得益于「代码托管平台——管理中心」托管了各类 Git 平台,并通过接口抹平了获取 PR信息、仓库信息的细节差异。此引擎能获取所有需要发送通知的仓库的所有符合条件的 PR 列表,并通过 PR 附带的 Commit、Comment 等信息,通过 TimeAt 接口获取各事件时间线,从而分析出是此 PR 被哪个 Git 用户所阻塞。

而后生成带有 Git 用户、消息类型(PR待Review、Commit待修改等)、消息标题、消息体、详情URL等「抽象消息」。

引擎还会结合「用户响应记录器」记录的历史用户反馈,决定要不要提醒,要不要反复提醒,要不要抄送给管理员等等。

分析引擎有多种触发方式,目前设计了定时任务触发(cron)和机器人交互式触发两种方式。即设置一个分析引擎函数入口,可通过配置文件启用不同的触发方式,可定时轮询,也可由通讯平台的 WebHook 收到交互命令后调用。

https://bird-notes.oss-cn-hangzhou.aliyuncs.com/img/2022-05-27-01-57-52-image.png

注:cron go sdk 库

2.3.4 消息渲染/发送引擎

「消息渲染/发送引擎」其实是各类通讯平台接口能力的一部分。

如前文所述,各个通讯平台都有着属于自己特色的消息发送方式,系统最大程度保证其发挥自身优势。「PR阻塞分析引擎」只是尽可能地给出待发送消息的关键信息,每个平台可以使用自己的渲染方式,对于不同的消息类型(PR待Review、Commit待修改等),使用不同的渲染方式。

更进一步,每个平台对于同一种消息类型,也可以定义多种风格的渲染模板。模板定义可以由 go template 实现;对于复杂的模板,如 飞书的消息卡片,支持各种布局和模组类型,用户可自行编码实现一个消息渲染类进行替换。

消息渲染结束,调用「跨平台用户映射器」,获取该 Git 用户的所有社交平台账号(同时根据 Git 仓库对应的群组进行限定)。

最后调用通讯平台的消息发送能力接口完成消息的发送。

https://bird-notes.oss-cn-hangzhou.aliyuncs.com/img/2022-05-27-02-10-06-image.png

2.3.5 用户响应记录器

通讯平台上的各类用户收到通知消息后,可以点击消息查看详情,也可以通过机器人进行交互。

系统设计了一个统一的用户响应接口,记录了响应者的用户信息、响应类型、对应仓库与PR信息、时间信息等。

通讯平台同样是自行注册一个 WebHook,而后调用相应的响应记录接口即可。

如,用户点击了飞书的「已阅,立即Review」卡片按钮后,飞书 Client 将调用接口记录相应信息。对于其他交互方式较弱的通讯平台,也可以@机器人+发送指令的形式方式进行交互。若下次PR阻塞分析再遇到同用户的同PR,将暂时不提醒。

具体的提醒策略、响应类型等业务细节,在后续开发中再完善和确认。

注:响应记录器和上文的用户绑定关系都需要将数据持久化至数据库中。考虑到尽量减少用户部署成本,以及此系统存储需求并不高,拟采用 sqlite 持久化数据。当然,项目将使用 orm 库进行数据库的操作,所以想替换成其他关系型数据库也是非常简单的。