Jenkins Pipeline脚本在美团餐饮SaaS的实践
温馨提示:这篇文章已超过378天没有更新,请注意相关的内容是否还可用!
我们在成都有很多后端、前端和测试岗位正在招人,欢迎投递简历:。建议学习一下Jenkins。构建 Jenkins 的方式也有很多种,现在比较常用的是自由式软件项目。针对这种情况,我们采用Pipeline构建方法来解决。当然,如果项目集成了React Native,还需要构建JsBundle。Native修改后,JsBundle可能不会更新。如果在构建Native的时候一起构建JsBundle,会造成很大的资源浪费。本文就是分享一个使用Pipeline解决此类问题的经验。事实上,Jenkins 提供了一种更优雅的方式来管理 Pipeline 脚本。配置项目Pipeline时,选择Pipeline script from SCM,如下图:。这样,当Jenkins启动作业时,会先去仓库拉取脚本,然后运行脚本。
本文作者来自美团成都研发中心(是的,我们正在成都建设研发中心)。 我们在成都有很多后端、前端和测试岗位正在招人,欢迎投递简历:。
背景
在日常开发中,我们经常有发布需求,遇到各种环境,比如:线上环境(Online)、模拟环境(Staging)、开发环境(Dev)等,最简单的方法就是手动搭建并上传服务器,但是这个方法太麻烦了。 使用持续集成可以完美解决这个问题。 建议学习一下Jenkins。
构建 Jenkins 的方式也有很多种,现在比较常用的是自由式软件项目(一种构建 Jenkins 的方式,它结合了 SCM 和构建系统来构建你的项目,甚至可以构建软件以外的系统)。 对于单个项目的简单构建来说,这种方法已经足够了,但是对于多个相似且不同的项目,就很难满足要求了,否则需要大量的作业来支撑,这是存在的,一个小小的改动,就可以了当很多工作需要修改时,维护起来很困难。 我们的团队以前也遇到过这个问题。
目前我们团队主要负责开发和维护多个Android项目,每个项目都需要构建。 每个构建过程都非常相似,但也存在一定的差异。 例如,构建流程大致如下:
总体流程大致相同,但也存在一些差异。 例如,有些构建可能没有单元测试,有些构建不需要触发自动化测试,构建结果通知的负责人也不同。 在自由式软件项目的正常构建中,每个项目都会创建一个作业来处理该过程(可能调用其他作业)。
这种处理方式也是可以的,但是必须考虑到可能会有新的进程接入(比如二次签名),并且构建过程中可能会出现bug等各种问题。 无论哪种情况,一旦修改了主构建流程,每个项目的作业都需要修改和测试,势必会浪费大量时间。 针对这种情况,我们采用Pipeline构建方法来解决。
当然,如果项目集成了React Native,还需要构建JsBundle。 Native修改后,JsBundle可能不会更新。 如果在构建Native的时候一起构建JsBundle,会造成很大的资源浪费。 而且直接把JsBundle这样的大文件放到Native Git仓库中也不是特别合适。 本文就是分享一个使用Pipeline解决此类问题的经验。
管道简介
管道也是施工管道。 对于程序员来说,最好的解释是:用代码来控制项目的构建、测试、部署等。 使用它的好处有很多,包括但不限于:
舞台视图
使用管道构建
新建Pipeline项目,编写Pipeline构建脚本,如下图所示:
对于单个项目来说,使用这样的管道来构建可以满足大部分需求,但它也存在很多缺陷,包括:
将管道编写为代码
既然有缺陷,我们就需要寻找更好的方法。 事实上,Jenkins 提供了一种更优雅的方式来管理 Pipeline 脚本。 配置项目Pipeline时,选择Pipeline script from SCM,如下图:
这样,当Jenkins启动作业时,会先去仓库拉取脚本,然后运行脚本。 脚本中会一步步执行我们规定的构建方法和流程。 构建的脚本可以由多人维护,也可以进行审查以避免错误。 即使上面已经打好了基础,但是针对多个项目的时候,还是有一些事情要做,不可能完全相同。 以下为施工结构图:
这样我们的构建数据源就分为三个部分:作业UI界面、仓库通用的pipeline脚本、项目下的特殊配置。 我们分别来看一下。
作业UI界面(参数化构造)
配置作业时,选择参数化构建流程,传入项目仓库地址、分支、构建通知程序等。还可以添加更多参数,这些参数的特点是可能需要频繁修改,比如灵活选择要构建的代码分支。
项目配置
在project项目中,放置这个项目的配置,一般是项目固定的、不经常修改的参数,比如项目名称,如下图所示:
注入构建信息
当QA提出bug时,我们需要判断是哪个build,或者知道commitId,以便于定位。 因此,在构建时,可以将构建信息注入到APK中。
1. 将属性注入gradle.properties
# 应用程序的后端环境
APP_ENV=测试版
# CI打包的数量,方便判断测试的版本。 如果不是通过CI打包,则默认为0
CI_BUILD_NUMBER=0
# CI打包的时间,方便判断测试的版本,如果不是CI打包的,默认为0
CI_BUILD_TIMESTAMP=0
2.在build.gradle中设置buildConfigField
#使用gradle.properties中注入的值
buildConfigField "String", "APP_ENV", "\"${APP_ENV}\""
buildConfigField "字符串", "CI_BUILD_NUMBER", "\"${CI_BUILD_NUMBER}\""
buildConfigField "String", "CI_BUILD_TIMESTAMP", "\"${CI_BUILD_TIMESTAMP}\""
buildConfigField "String", "GIT_COMMIT_ID", "\"${getCommitId()}\""
//获取当前Git的commitId
字符串 getCommitId() {
尝试 {
def commitId = 'git rev-parse HEAD'.execute().text.trim()
返回commitId;
} catch (异常 e) {
e.printStackTrace();
3. 显示构建信息
在App中找到合适的位置,比如开发者选项,显示刚才的信息。 当 QA 提出错误时,要求他们携带此信息
mCIIdtv.setText(String.format("CI 版本号:%s", BuildConfig.CI_BUILD_NUMBER));
mCITimetv.setText(String.format("CI 构建时间: %s", BuildConfig.CI_BUILD_TIMESTAMP));
mCommitIdtv.setText(String.format("Git CommitId:%s", BuildConfig.GIT_COMMIT_ID));
仓库通用Pipeline脚本
通用脚本是一个抽象的构造过程。 与项目相关的所有内容都需要定义为变量,然后从变量中读取。 不要把它写死在通用脚本中:
节点{
尝试 {
stage('check out code'){//从git仓库中查看代码
git 分支:“${BRANCH}”,凭证 ID:'xxxxx-xxxx-xxxx-xxxx-xxxxxxx',url:“${REPO_URL}”
加载项目配置();
阶段('编译'){
//这里是构建,可以调用job输入或者项目配置的参数,如:
echo "项目名称${APP_CHINESE_NAME}"
// 可以判断
if (Boolean.valueOf("${IS_USE_CODE_CHECK}")) {
echo“需要静态代码检查”
} 别的 {
echo“不需要静态代码检查”
stage('archive'){//此demo Android项目,实际使用时请根据自己的产品确定
def apk = getShEchoResult ("查找./lineup/build/outputs/apk -name '*.apk'")
def artifactsDir="artifacts"//存放工件的文件夹
sh "mkdir ${artifactsDir}"
sh "mv ${apk} ${artifactsDir}"
archiveArtifacts“${artifactsDir}/*”
stage('通知负责人'){
emailext body: "构建项目: ${BUILD_URL}\r\n构建完成", 主题: '构建结果通知【成功]', to: "${EMAIL}"
} 捕获 (e) {
emailext body: "构建项目: ${BUILD_URL}\r\n构建失败,\r\n错误消息: ${e.toString()}", subject: '构建结果通知【失败】', to: "$ {EMAIL }”
} 最后 {
// 清空工作区
cleanWs notFailBuild:true
// 获取shell命令的输出
def getShEchoResult(cmd) {
def getShEchoResultCmd = "ECHO_RESULT=`${cmd}`\necho \${ECHO_RESULT}"
返回 sh (
脚本:getShEchoResultCmd,
返回标准输出: true
)。修剪()
//加载项目中的配置文件
def loadProjectConfig(){
def jenkinsConfigFile="./jenkins.groovy"
if (fileExists("${jenkinsConfigFile}")) {
加载“${jenkinsConfigFile}”
echo "找到打包参数文件${jenkinsConfigFile},加载成功"
} 别的 {
echo "${jenkinsConfigFile}不存在,请在项目${jenkinsConfigFile}中配置打包参数"
sh“1号出口”
轻轻双击“使用参数构建”->“开始构建”,然后等待几分钟即可收到电子邮件。
其他建筑结构
以上只是针对我们目前遇到的问题的一个很好的解决方案。 可能并不完全适用于所有场景,但是可以根据上面的结构进行调整,比如:
当遇到React Native时
当React Native引入项目时,由于技术栈的原因,React Native页面由前端团队开发,但容器和原生组件由Android团队维护,构建流程也发生了一些变化。
方案对比
前端团队开发页面,构建后生成JsBundle。 Android团队拿到前端构建的JsBundle,将其打包在一起,生成最终产品。 在我们的开发过程中,JsBundle修改后,Native不一定需要修改,并且JsBundle也不一定需要每次构建Native时都重新构建。 而且这两部分是由两个团队负责的,而且是独立发布的,构建的时候应该独立构建,不应该合并在一起。
为了综合比较,我们选择采用单独构建的方式来实现。
单独建造
因为版本需要分开发布,所以JsBundle的构建和Native的构建应该分开,使用两个不同的作业来完成,这也方便两个团队各自操作,避免相互影响。 JsBundle的构建也可以参考上面提到的Pipeline的构建方法,这里不再赘述。
独立搭建之后,如何组合在一起呢? 我们是这样想的:JsBundle构建完成后,将版本存储在一个地方,供Native在构建时下载所需版本的JsBundle。 大致流程如下:
这个过程有两个核心,一是构建好的JsBundle的归档存储,二是Native构建时的下载。
JsBundle归档存储
这里我们选择MSS(美团存储服务)。 上传文件到MSS可以使用s3cmd,但毕竟不是每个Slave都安装在上面,通用性不强。 为了保证稳定性和可靠性,这里基于MSS SDK写一个小工具就足够了,比较简单,几行代码就可以完成。
私有静态字符串TenantId =“mss_TenantId =”;
私有静态 AmazonS3 s3Client;
公共静态无效主(字符串[] args)抛出IOException {
if (args == null || args.length != 3) {
System.out.println("请输入:inputFile,bucketName,objectName");
返回;
s3Client = AmazonS3ClientProvider。 创建AmazonS3Conn();
uploadObject(args[0], args[1], args[2]);
公共静态无效 uploadObject(字符串输入文件,字符串存储桶名称,字符串对象名称){
尝试 {
文件 文件 = 新文件(inputFile);
if (!file.exists()) {
System.out.println("文件不存在:" + file.getPath());
返回;
s3Client.putObject(new PutObjectRequest(bucketName, objectName, file));
System.out.printf("成功上传%s到MSS:%s/v1/%s/%s/%se", inputFile, AmazonS3ClientProvider.url, TenantId, bucketName, objectName);
} catch (AmazonServiceException ase) {
System.out.println("捕获了一个 AmazonServiceException,其中 " +
“表示您的请求已实现” +
“发送至 Amazon S3,但因错误响应而被拒绝”+
“因为某些原因。”);
System.out.println("错误消息:" + ase.getMessage());
System.out.println("HTTP 状态代码:" + ase.getStatusCode());
System.out.println("AWS 错误代码:" + ase.getErrorCode());
System.out.println("错误类型:" + ase.getErrorType());
System.out.println("请求ID:" + ase.getRequestId());
} catch (AmazonClientException ace) {
System.out.println("捕获了一个 AmazonClientException,其中 " +
"表示客户端遇到" +
“尝试时出现内部错误” +
“与 S3 通信,”+
“例如无法访问网络。”);
System.out.println("错误消息:" + ace.getMessage());
我们直接在Pipeline中构建之后,就可以调用这个工具了。
当然,JsBundles也是分类型的,在调试过程中可能需要随时更新。 这些JsBundle不需要永久保存,一段时间后可以删除。 删除时参考MSS生命周期管理。 因此,我们在构建JsBundle的工作中添加一个参数来区分。
//根据TYPE,上传到不同的bucket
def Bucket =“rn-bundle-prod”
if ("${TYPE}" == "dev") {
bucket = "rn-bundle-dev" //具有生命周期管理,一段时间后会自动删除
echo "开始上传JsBundle到MSS"
//jar地址需要替换成自己的
sh“curl -s -S -L -o upload.jar”
sh "java -jar upload.jar ${archiveZip} ${bucket} ${PROJECT}/${targetZip}"
echo "上传 JsBundle 到 MSS:${archiveZip}"
Native构建时下载JsBundle
为了实现构建时的自动下载,我们编写了一个Gradle插件。
首先在build.gradle中配置JsBundle信息:
类路径 'com.zjiecode:rn-bundle-gradle-plugin:0.0.1'
将插件应用到所需的模块:
应用插件:'mt-rn-bundle-download'
在build.gradle中配置JsBundle信息:
RN下载配置 {
//远程文件目录,因为有多种类型,所以这里可以填写多种。
路径=[
'',
”
version = "1"//版本号,这里使用JsBundle的BUILD_NUMBER
fileName = 'xxxx.android.bundle-%s.zip' //远程文件的文件名,%s会填上面的版本
outFile = 'xxxx/src/main/assets/JsBundle/xxxx.android.bundle.zip' // 下载的存放路径,相对于项目根目录
插件会在打包的任务前面插入一个下载的任务,该任务会读取上述配置信息,并在打包阶段检查该版本的JsBundle是否已经存在。 如果不存在,就会去存档的JsBundle中,下载我们需要的JsBundle。 当然,这里的版本可以使用上面介绍的注入构建信息的方法,通过作业参数注入。 这样Jenkins在构建Native的时候就可以动态填写需要JsBundle的版本了。
我们已经把这个Gradle插件放到了github仓库中,大家可以在此基础上进行修改,当然也欢迎PR。 地址:
总结
我们将一个构建分为几个部分,好处如下:
当然,Pipeline也有一些缺点,比如:
当项目集成React Native和Pipeline时,我们可以将JsBundle构建产品上传到MSS存档。 构建Native时,可以动态下载。
关于作者
张杰,美团点评高级Android工程师,2017年加入餐饮平台成都研发中心,主要负责餐饮平台B端应用开发。
王浩,美团点评高级Android工程师,2017年加入餐饮平台成都研发中心,主要负责餐饮平台B端应用开发。