最近领导开始要求使用ai来提高效率,正好我们部门平时一直没有人力进行详细的代码审核,因此这次就打算在CI/CD中集成ai进行代码审核,看看效果如何,能否解放这一部分的人力。
首先看下完成后的效果,如图所示,当有人提交merge request(mr)申请合并代码时,会触发gitlab的pipeline流水线,执行ai代码审核的程序,然后将ai代码审核通过评论的方式输出在这个mr下面。目前ai审核的结果只是用于代码审核的参考,不作为硬性标准。

准备:
- gitlab,我们公司的版本比较老,是gitlab 9,新版本的gitlab的CI/CD脚本语法会不太一样。
- 一台机器,安装了docker,用于部署gitlab 触发流水线时执行的ai代码审核程序
- 阿里云百炼的秘钥,这里使用的ai审核工具是阿里云的qwen code,可以参考安装配置与使用Qwen Code命令行工具-大模型服务平台百炼-阿里云 官方文档,使用的ai模型是qwen3-coder-plus。
- openresty,这个非必须,因为触发mr时的请求是固定格式的,需要写个接口进行数据转换,来触发后面的ai审核逻辑,这里也可以用java写一个controller进行处理。
必备知识:
首先简单介绍下gitlab的CI/CD流程和gitlab runner的机制。
CI/CD流程:
我们公司的代码分支管理策略采用github flow的管理方式,只有一个主干分支,开发人员都从这个分支切代码到feature分支进行开发,开发完成后需要提交mr 申请合并到主干分支,主干分支只有核心开发有权限提交,mr审核通过后会合并到主干分支,同时触发gitlab的CI/CD流程,自动执行代码的编译测试,完成后自动打包发布到开发环境。
gitlab runner机制:
这个是gitlab提供的用于CI/CD的组件,可以在项目代码的根目录下新增一个.gitlab-ci.yml文件,这里面可以写脚本代码,在某些条件满足的情况下由gitlab触发执行这个脚本代码,不过在gitlab 9 这个老版本上只有当代码被push时才会触发,不过可以在脚本中写条件,限制只有主干分支push时触发测试打包,其他开发人员的feature分支push时可以只执行代码测试。
而gitlab runner就是执行这个脚本的程序,他需要你提前在一台机器上部署这个程序,然后配置完成后就会注册到gitlab上并保持长连接,这样gitlab发现代码提交需要触发自动集成时,就能找到这个程序并触发自动集成代码。
ai代码审核配置
接下来进入主题,ai代码审核的配置。这个主要可以分为三步
- 开发提交mr时触发回调
- 在回调中执行ai代码审核的代码
- 将ai代码审核的结果通过评论的方式发送到指定的mr下
第一步,开发提交mr时触发回调
这个在高版本的gitlab中可以直接触发gitlab runner执行 .gitlab-ci.yml中的脚本代码,但是在gitlab 9这个老版本中没有这个功能,于是只能采用麻烦一点的方式,如图所示,可以在settings -> Integrations下,增加一个Merge Requests Events事件的webhook,他会在有mr事件时触发回调自定义服务器的接口。这里我也贴一下mr事件回调的数据格式。


json
{
"object_kind": "merge_request",
"user": {
"name": "Administrator",
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
"object_attributes": {
"id": 99,
"target_branch": "master",
"source_branch": "ms-viewport",
"source_project_id": 14,
"author_id": 51,
"assignee_id": 6,
"title": "MS-Viewport",
"created_at": "2013-12-03T17:23:34Z",
"updated_at": "2013-12-03T17:23:34Z",
"st_commits": null,
"st_diffs": null,
"milestone_id": null,
"state": "opened",
"merge_status": "unchecked",
"target_project_id": 14,
"iid": 1,
"description": "",
"source":{
"name":"Awesome Project",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/awesome_space/awesome_project",
"avatar_url":null,
"git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
"git_http_url":"http://example.com/awesome_space/awesome_project.git",
"namespace":"Awesome Space",
"visibility_level":20,
"path_with_namespace":"awesome_space/awesome_project",
"default_branch":"master",
"homepage":"http://example.com/awesome_space/awesome_project",
"url":"http://example.com/awesome_space/awesome_project.git",
"ssh_url":"git@example.com:awesome_space/awesome_project.git",
"http_url":"http://example.com/awesome_space/awesome_project.git"
},
"target": {
"name":"Awesome Project",
"description":"Aut reprehenderit ut est.",
"web_url":"http://example.com/awesome_space/awesome_project",
"avatar_url":null,
"git_ssh_url":"git@example.com:awesome_space/awesome_project.git",
"git_http_url":"http://example.com/awesome_space/awesome_project.git",
"namespace":"Awesome Space",
"visibility_level":20,
"path_with_namespace":"awesome_space/awesome_project",
"default_branch":"master",
"homepage":"http://example.com/awesome_space/awesome_project",
"url":"http://example.com/awesome_space/awesome_project.git",
"ssh_url":"git@example.com:awesome_space/awesome_project.git",
"http_url":"http://example.com/awesome_space/awesome_project.git"
},
"last_commit": {
"id": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"message": "fixed readme",
"timestamp": "2012-01-03T23:36:29+02:00",
"url": "http://example.com/awesome_space/awesome_project/commits/da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"author": {
"name": "GitLab dev user",
"email": "gitlabdev@dv6700.(none)"
}
},
"work_in_progress": false,
"url": "http://example.com/diaspora/merge_requests/1",
"action": "open",
"assignee": {
"name": "User1",
"username": "user1",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
}
}
}
第二步,在回调中执行ai代码审核的代码
这里我们选择在上一步的自定义回调中通过调用http请求的方式,触发gitlab的trigger,让gitlab执行.gitlab-ci.yml中的脚本代码。 trigger的配置方式为在Settings -> CI/CD Pipelines菜单栏下,通过Add trigger的方式添加一个trigger,这样我们就可以在mr回调的处理代码中通过http调用触发gitlab的pipeline,执行.gitlab-ci.yml中的代码。不过这一步的具体执行还要分成三个部分。

3.1 编写回调接口代码
mr触发的事件和主动触发gitlab trigger的http请求参数并不一样,这里需要我们在自定义代码中做下转换,我这里是使用openresty的lua代码简单做了下处理,这里也贴下代码用于参考。
主要逻辑是从mr请求体中过滤出几个变量,然后转成trigger请求的变量,用于后续脚本使用。解析的变量有target_branch被合并的目标分支,source_branch申请合并的源头分支,source_project_id合并的项目id,mr_iid合并请求id,过滤出这几个变量后,就可以调用http请求触发trigger,让gitlab执行pipeline。
这里有几个地方比较重要,触发trigger需要指定项目id,trigger的token(这个可以在gitlab上创建trigger的地方看到),变量ref的值,这个值会决定在哪个分支上触发gitlab的pipeline流程,这里需要指定为mr的源分支,方便后续脚本查询mr的代码修改。
ini
location /gitlab/ci/relay {
access_by_lua '
ngx.req.read_body()
local param_args_str = ngx.req.get_body_data()
ngx.log(ngx.INFO, "<param_args_str>" .. tostring(param_args_str))
local cjson = require "cjson"
local param_args = cjson.decode(param_args_str)
if param_args and param_args.object_attributes and param_args.object_attributes.last_commit and string.find(param_args.object_attributes.state, "opened") then
local target_branch = tostring(param_args.object_attributes.target_branch)
local source_branch = tostring(param_args.object_attributes.source_branch)
local commit_id = tostring(param_args.object_attributes.last_commit.id)
local source_project_id = tostring(param_args.object_attributes.source_project_id)
local mr_iid = tostring(param_args.object_attributes.iid)
local url = string.format("https://xxx.xxx.com.cn/api/v4/projects/%s/trigger/pipeline",source_project_id)
local body = string.format("ref=%s&token=xxxxxxxxxxxxx&variables[CI_MERGE_REQUEST_SOURCE_BRANCH_NAME]=%s&variables[CI_MERGE_REQUEST_TARGET_BRANCH_NAME]=%s&variables[CI_COMMIT_ID]=%s&variables[CI_PROJECT_ID]=%s&variables[CI_MR_IID]=%s",source_branch,source_branch,target_branch,commit_id,source_project_id,mr_iid)
ngx.log(ngx.INFO,"trigger ai review")
ngx.log(ngx.INFO,string.format("url is %s,body is %s",url,body))
local httpc = require("resty.http").new()
local res, err = httpc:request_uri(url, {
method = "POST",
body = body,
headers = {["Content-Type"]="application/x-www-form-urlencoded"}
})
if not res or err then
ngx.log(ngx.INFO, "<err>" .. err)
end
ngx.log(ngx.INFO, "<info>" .. tostring(res.body))
end
ngx.say("ok")
return ngx.exit(ngx.HTTP_OK)
';
}
3.2 编写.gitlab-ci.yml文件
这里列出示例代码,主要是通过git diff来查询mr的代码修改,然后通过执行qwen这个命令,将代码修改和提示词传入,让ai进行代码审核,最后将审核结果通过评论的方式发送到mr下。
bash
stages:
- ai-review
ai_code_review:
stage: ai-review
image: qwen-code:1.1.0
script:
- |
if [ "${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-}" = "" ]; then
echo "is not merge request ,skip ai review"
exit 0
fi
echo "Target branch: $CI_MERGE_REQUEST_TARGET_BRANCH_NAME"
echo "Source branch: $CI_MERGE_REQUEST_SOURCE_BRANCH_NAME"
echo "CI_PROJECT_ID: $CI_PROJECT_ID"
echo "CI_MR_IID: $CI_MR_IID"
echo "start ai review11"
echo "OPENAI_MODEL:$OPENAI_MODEL"
git diff origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME}...HEAD > /tmp/git-diff-output.txt
cat /tmp/git-diff-output.txt
result=$(cat /tmp/git-diff-output.txt | qwen -p "Review these changes for bugs, security issues, and code quality,and use Chinese to output the reply." --output-format json)
echo "$result"
response=$(echo "$result" | jq -r '.response')
curl --request POST --header "PRIVATE-TOKEN: ${COMMENT_KEY}" --data-urlencode "body=${response}" "https://xxxxxxx.com.cn/api/v4/projects/${CI_PROJECT_ID}/merge_requests/${CI_MR_IID}/notes"
3.3 制作自定义镜像
我们需要制作一个docker镜像,主要功能是安装qwen工具,支持脚本中执行qwen调用ai代码审核,因为gitlab runner执行.gitlab-ci.yml脚本时是通过制定镜像来执行的,这里就需要把自定义的镜像填到yml文件的image字段中,提供docker环境。
制作镜像也很简单,这里提供下dockerfile文件,其实就是在node基础镜像的基础上安装下qwen和jq程序。
bash
FROM docker.1ms.run/library/node:22-bookworm
RUN npm install -g @qwen-code/qwen-code
ADD jq-linux-amd64 /usr/bin/jq
RUN chmod +x /usr/bin/jq
3.4 在gitlab中增加变量
因为qwen调用需要三个参数,其中包括秘钥信息,这里可以通过gitlab的 settings -> CI/CD Pipelines下面的Secret Variables功能来实现,通过Add new variable添加变量OPENAI_API_KEY,OPENAI_BASE_URL,OPENAI_MODEL,这样.gitlab-ci.yml脚本中启动镜像时会自动注入这些变量作为环境变量,就可以直接执行qwen命令调用ai。

第三步,将ai代码审核的结果通过评论的方式发送到指定的mr下
这个其实就是在.gitlab-ci.yml脚本中,将ai代码审核的结果通过gitlab的api新增一个评论,评论的内容就是ai审核的结果。需要注意的是,发送评论的api需要一个secret key,这个可以通过在gitlab的user settings界面生成,然后也是在gitlab中设置为一个变量,在.gitlab-ci.yml的脚本中通过变量引用进来。

至此,ai代码审核就完成了,还是比较简单的,赶紧试一下吧