有赞零售移动CI/CD实践
有赞技术 有赞coder
一、背景
随着有赞零售业务的蓬勃发展,为了尽早交付有价值的应用满足客户需求,我们采用了敏捷开发的模式,快速拥抱变化的同时保持竞争优势。从 2019 年起,零售客户端的发版周期更改为每周一次,这对移动端的持续集成与交付提出更高的要求。如何根据现有的团队规模,在有限的资源下,快速搭建稳定可靠的持续集成与交付系统,我们有了自己的实践与思考。
二、问题与挑战
对于一个业务需求的开发或者迭代,移动开发同学除了编写代码,还需要经历打包、测试,如果 QA 同学反馈问题,则需要进行修复,然后再次经历打包、测试,直至测试通过,才能交付产品。整个过程中,开发同学需要手动提交 MR,手动触发打包构建以及实时监控打包流程的状态。如此下去,开发同学手头上的工作会被频繁打断,去跟踪处理这些流程的衔接,必然会严重影响开发专注度,降低开发生产力。
那么,随着业务的复杂,项目的壮大,团队人员的增多,这类问题会愈发严重。而且,在编码过程中,一些通过文档规定或是意识上达成的共识,实际执行起来会变得越来越困难,这些都是不可持续的。因此,从代码提交到最终打包交付便成了保证代码与产品质量,十分重要的一个环节。
三、自动化流程体系
持续集成与交付系统是一块大又杂的领域,我们这次主要介绍一下从代码提交到打包交付的一套自动化流程体系,解决上述提到的问题。由于有赞零售的代码是托管在 GitLab 平台上的,因此我们采用基于 GitLab CI 搭建持续集成平台,当然我们也使用 Jenkins 配合做一些辅助性的工作。
3.1 流程与架构
3.1.1 流程
我们先从一个开发工程师的角度,来整体感受一下现在的开发及交付的流程。开发工程师首先从 dev 分支 checkout 出自己的 feature 分支进行迭代,在迭代过程中不断地向自己的 feature 分支提交代码,提交代码过程中会有本地检查这一道保障,当需求开发完成,在 feature 分支上构建出对应的阶段提测包并提交 QA 进行测试,测试通过后,提交 MR 准备合并入 dev 分支,通过编译检查和 Code Review 后,才能允许合并至 dev 分支。在 dev 分支上的代码是可靠的,会有静态检查再次进行保障,而且也会有 QA 进行回归测试,直至所有 bug 修复完毕,交付最终的产品进行验收。
3.1.2 架构
这套体系主要分为 5 个部分,下文会具体介绍一些细节:
- CI(持续集成):GitLab、Jenkins
- CD(持续交付):MBD、APUB、移动助手 App
- 检查:编译检查、本地检查、静态检查
- Code Review:Git Hooks、GitLab CI
- 消息与闭包:企业微信消息通知、企业微信群机器人、企业邮件、检查报告、JIRA…
3.2 打包与分发
持续集成与交付的目的是快速迭代,并且交付稳定可靠的产品。从移动端的角度来看,我们可以理解为快速构建出不同分支、版本,且稳定可靠的应用包。并且,能够将应用包快速分发到 QA、PM、UED 等各个业务方同学手上。
3.2.1 打包
有赞零售的打包接入了有赞的移动构建集成平台 MBD (Mobile Build),MBD 提供了便利的打包操作,支持 iOS、Android、Weex 等多个构建集成平台,可以选择任意分支、版本进行构建,并且提供了可远程调用的 API 方便持续集成。当然,如果技术团队还没有类似的构建集成平台,或者现阶段去开发一个平台的效益并没有那么高,也可以选择通过 Jenkins 搭建 iOS 或 Android 的自动化打包构建任务,实现成本较低。
简单介绍一下 MBD 的使用流程,首先需要添加组件,填入工程的一些重要信息(如 Bundle ID、SSH 地址…),再编写构建脚本(如运行 pod、fastlane…),根据组件创建出对应的集成单。我们后续的打包流程,主要就是通过已经创建的好的集成单,输入对应的版本号、分支名等信息,触发集成单的构建,最后处理构建反馈的结果,构建产物就是可供下载的应用包。
3.2.2 分发
为了减少 QA 和开发之间的低效沟通以及优化 App 包的分发流程,我们急需一个平台来统一管理分发公司内部的 App 包,于是有赞移动应用发布平台 APUB (App Publish) 应运而生。与 MBD 无缝衔接,可以实现从构建到发布、热修复、交付一系列流程的打通,并且提供了有赞移动助手 App 这个入口,方便下载各个业务线的 App 包。
有赞移动助手 App 提供了许多好用的功能:
- 正式版、测试版应用的下载
- 内网网关、开发环境的切换
- 应用包的基础信息(打包人、包版本、包体积、构建分支…)
想要了解有赞移动基础设施建设相关内容,可以详见文章:有赞移动基础设施建设的实践和思考
3.3 检查体系
不过,谈到持续集成与交付,我们不能忽略了一个关键点:出包的可靠性。如何保证出包的可靠性呢?答案:检查。
3.3.1 编译检查
编译检查可以认为是最重要的守门员,编译检查能否通过,直接决定了打包能否成功。我们的第一反应是可以通过修改编译脚本,在每次打包前加入编译检查,来确保出包的可靠性。然而,这其中潜藏着一个很严重的流程性问题。原本出包是一个箭在弦上的事情,但是,因为在出包前加入了编译检查,使整个流程受这个前置条件的影响。想象这样一个场景,一旦编译检查出问题,我们就得通过日志定位问题,然后找相关工程师进行修复,修复完毕,再触发打包,可能又检查出新的问题,反反复复,使得出包变得非常的低效。
为了解决上述的问题,我们调整了检查策略。在一些可靠的分支,如 dev、release 进行 MR 的时候,通过 GitLab Runner 触发编译检查的 Pipeline,只有检查通过,相关的代码才能够被允许合入对应的分支。
为了能让大家有个简单的概念,介绍一下几个名词:
- GitLab Runner:GitLab CI 提供注册 CI 服务器的接口,执行构建任务的一个服务,即 Pipeline 运行的具体环境,能够运行 Pipeline 并将结果发送回 GitLab,通常是和仓库托管的服务区分开来,部署在不同的机器上
- Pipeline:一次 Pipeline 其实相当于一次构建任务,里面可以包含 CI 不同阶段的不同任务,我们的编译检查就是运行在这个流程中,触发的条件也很多,我们选择 MR 的时候触发编译检查的 Pipeline
至于 GitLab Runner 如何搭建就不再赘述了,可以查看官方文档。我们可以来感受一下 GitLab CI 的架构设计:
我们对于编译检查进行了一些提速优化,使得平均时间稳定在 5 分钟左右:
- 全量编译和差量编译的区分
- 可忽略编译的文件,进行白名单配置
- 缓存外部依赖文件…
3.3.2 本地检查
由于资源有限,目前的 GitLab Runner 是单机状态,并没有进行多机并发处理。所以,如果一股脑全部依赖编译检查,一旦同一时间段内 Pipeline 数量变多,很可能就会处于排队等待中,MR 的环节也会因此变得十分的冗长。除了优化编译检查,我们可以把部分检查的时机再提前。
本地检查,具体一点可以叫做本地代码提交检查。本地代码提交检查可以有效的保证代码提交质量。除了 Lint 这类的代码风格统一的检查,在业务上,最重要的是对跨模块的代码修改做了一定的限制,还可以检查一些关键的配置文件是否被不小心修改。
举个小例子,比如 A 同学没有修改 RetailStock 模块的权限,但是 A 同学在 RetailStock 下的 YZStockBundle.h 文件增加了一行注释,准备提交代码。会在 git commit 这个时机,进行本地代码提交检查,发现了 A 同学修改了 RetailStock 模块的代码对其进行提醒,并且提供了一个 Code Review 的 URL 链接通过 git diff 展现代码修改的内容,如果真的由于业务需要,可以由 RetailStock 模块的相关负责人 Review 后同意修改。
搭建本地代码提交检查,需要使用 Git Hooks,我们这边 hook 了 commit-msg ,当然你也可以 hook prepare-commit-msg、 pre-commit 等其他 Git 钩子将脚本进行更精细的拆分。
简单介绍一下,如何配置本地代码提交检查。
- deploy_git_hooks.sh:部署脚本,方便小伙伴一键配置
- commit_msg_analyzer.sh:本地代码提交检查的入口脚本
工程目录结构:
├── .git
│ ├── hooks
│ │ ├── commit-msg
│ │ ├── commit-msg.sample
└── scripts
└── git-hooks
├── commit_msg_analyzer.sh
└── deploy_git_hooks.sh
deploy_git_hooks.sh:
#!/bin/bash -l
current_dir=$(cd $(dirname $0) pwd)
scripts_dir=$(dirname $current_dir)
project_dir=$(dirname $scripts_dir)
git_dir="$project_dir/.git"
git_hooks_dir="$git_dir/hooks"
commit_msg="$git_hooks_dir/commit-msg"
analyzer="$current_dir/commit_msg_analyzer.sh"
if [ ! -d $git_dir ]; then
echo ".git not exist"
exit 1
fi
if [ -f $commit_msg ]; then
echo "commit-msg already exist"
exit 1
else
if [ -f $analyzer ]; then
chmod +x $analyzer
ln -sf $analyzer $commit_msg
echo "deploy success"
else
echo "commit_msg_analyzer.sh not exist"
exit 1
fi
fi
commit_msg_analyzer.sh:
#!/bin/bash -l
# 校验提交说明是否标准
# 检查代码风格
# 检查是否存在关键配置文件的修改
# 检查是否存在跨模块的修改
# ...
3.3.3 静态检查
静态检查可以在编码规范,代码缺陷,性能等问题上提前预知,从而保证项目的交付质量。现阶段自助研发一套静态检查工具,投入产出比并不高,但是我们可以借助三方框架快速搭建起来,并且定义了一些错误的过滤规则,让我们更加聚焦迫切需要解决的问题。iOS 侧我们选择了 Clang 支持度最好的 scan-build 作为首选,以及精度最高的 Infer 作为配合使用。Android 侧首选 Android Lint,其内容涵盖了大部分 Android 的检测内容,并且使用 FindBugs 作为 Android Lint 在 Java 语言层上的补充。
- iOS:scan-build + Infer
- Android:Android Lint + FindBugs
对于可靠的分支,比如 dev 分支,我们选择定时触发,如每天晚上触发。当然,开发者也可以选择主动触发,选择对应的分支执行静态检查。静态检查报告会通过企业微信消息通知的方式,发送给执行者。定时触发的静态检查,检查出错误后,除了生成报告,而且会根据错误找到相应的模块负责人,创建 JIRA Issue。
3.4 Code Review
Code Review 的重要性是毋庸置疑的,这里提到的 Code Review 并不是指项目开发完成后组织的 Group Code Review,主要是针对持续集成与交付过程中,需要或者说必须要进行的一环。我们前面讲到的本地代码提交检查后,发现有跨模块代码的修改,就是需要相关模块的负责人进行 Code Review 的。
对于那些可靠的分支进行 MR 的时候,则必须要经过两个同学 Review 后确认没有问题,才能允许进行合入操作。Review 时,可以对需要改进的代码进行评论。从进行 Review 的同学角度来说,不仅能够看到新需求的逻辑与问题,还可以碰撞不同的架构思想。从被 Review 的同学角度来说,则可以发现一些由于思维定式或者粗心造成的问题,也可以采纳一些好的建议,让代码更加健壮且优雅。
通过 GitLab CI 的 Merge Request 机制,可以很方便快捷的搭建这套体系。每个 MR 只有通过了 Pipeline 并且所有 Reviewer 的评论都得到解决,最终由拥有 MR 权限的同学进行 Merge 操作。
简单介绍一下,GitLab Merge Request 几个好用的功能:
- 不仅支持对整个 MR 进行评论,而且支持对每行代码进行评论,并且评论后会自动将其标注为待解决的状态
- 在提交 MR 的时候支持配置目标 Approvers 以及 Reviewers,在对项目进行配置的时候也可以配置至少需要哪几个同学同意才能进行 Merge
- 可以设置只有 Pipeline 执行通过才允许进行 Merge,这里的 Pipeline 就是指前面提到的编译检查,只有检查通过了,有合并权限的 Reviewer 才被允许点击 Merge 按钮进行合入操作
日常实践中,我们发现了直接通过 MR 进行 Code Review 的一个痛点。比如在一个稍微大一点的项目开发中,动辄就是几百个 Changes,几千个 Additions 或 Deletions。这样不仅 Reviewer 需要耗费大量的时间去理解逻辑和审查代码,而且往往也很难发现一些隐藏很深的问题,简而言之,就是使得这次的 Code Review 非常的耗时且低效。
我们也在积极探索和实践更好的 Code Review 的形式,有个简单思路就是将最后 MR 进行前置分解,通过调整 GitLab Flow 把 Code Review 放到每个分解的 MR 中。
3.5 消息与闭环
持续集成与交付过程中,消息与闭环也是非常重要的一环。能够减少沟通成本,能够在自动化的流程中,让使用者更加无感知,不用时不时的去跟踪处理流程的衔接。监控的成本大大降低,正常情况下感知流程进行的节点即可,重点只需要关注异常流程,因为这个时候需要人工介入处理。
目前,主要依赖的消息通知方式:
- 企业微信的消息通知
- 企业微信的群机器人
- 企业邮件
闭环方式中,最值得一提的是搜集出包的变更集。应用包在提测期间,也经常会有一些 bug 产生,修复后需要重新打包,如何比较 2 个提测包之间的差异及变更?这里,给出 2 个解决方案:
3.5.1 方案一
根据上述的流程体系可知,目前可靠分支的代码合并都是需要通过 MR 的方式。我们可以搜集 2 个提测包之间所有的 MR 提交,并规定好 MR 的提交模板,变更内容就是其中的必填项之一,然后过滤出提交信息中的变更内容,由群机器人进行通知。
这是我们目前采取的方案,对现有的流程以及小伙伴们的操作习惯改变最小。小伙伴们提交 MR 的时候,只需要根据提供的 MR 模板填入对应信息即可。还有一个好处,变更内容更像是 MR 申请者对本次改动点的总结,能够更好的概括此次 MR 涉及的改动点。而且,我们规定在提测期间的每个 MR 都是需要有对应的 JIRA Issue 链接,方便 QA 追踪及回归。
3.5.2 方案二
首先,需要规范 git commit,这一步可以通过一些开源的工具解决,比如:commitizen。最终通过 git log 的方式,过滤出 2 个包之间新增的所有 git commit。
这样做的好处是,能够通过 git log 获取到所有代码提交的改动信息。坏处是,需要规范 git commit 流程的学习成本,其次是庞大的 git commit 信息是否真的有必要?大量的 git commit 信息不仅冗杂,而且不能很好的区分到底哪些是为了修复某个问题而产生的提交。
当然,也可以解决这个问题,就是前面提到的需要严格规范每一次的 git commit。简而言之,严格执行规范本身就是不可持续的,可能对小伙伴们的操作习惯改变也较大,因此快速落地的成本也会比较大。但是,规范 git commit 仍然是一个很好举措,它的价值远远不只是为了搜集变更集,也是开发过程中排查问题的一个好习惯,是后期需要真正落地的一个规范。
流程中,还有一些比较重要的闭环方式:
- 核心用例自动化报告
- 静态检查报告
- JIRA Issue
四、思考与展望
在持续集成与交付系统实践过程中,并没有一套所谓最好的或者是标准的解决方案。每个公司或者团队所处的阶段不同,提供的资源不同,采用的方案也会不一样。但是,如果你目前正在或者将要做相关实践时,一定要衡量好投入产出比,确保整个流程尽可能的简化,尽可能的减少使用者的学习成本,将其系统化、高效化以及自动化。
当前,有赞零售业务仍然处于快速增长阶段,移动端在持续集成与交付这块才有了一点雏形。未来我们的建设主要会集中在 2 个方向,更加健壮的代码管控体系和更加完备的自动化测试流程。如上文所述,随着业务越来越复杂,涉及的角色越来越多,代码集成的管控需要更加严格,而严格的代码集成管控将增加团队成员每次提代码的痛苦。如何做到差异化的检查和准入,将是未来持续集成的建设方向。
目前,除了移动端发版核心用例 UI 自动化,还有大量的测试 Case 都需要 QA 人工保障。我们希望未来能够尽可能地采用自动化为主的方式进行测试,覆盖大部分的场景,而让 QA 可以集中精力测试新功能或非常边界异常的场景验证。