Jenkins Pipeline脚本在美团餐饮SaaS的实践

我们在成都有很多后端、前端和测试岗位正在招人,欢迎投递简历:。建议学习一下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端应用开发。

免责声明

1、本网站属于个人的非赢利性网站,转载的文章遵循原作者的版权声明。 2、本网站转载文章仅为传播更多信息之目的,凡在本网站出现的信息,均仅供参考。本网站将尽力确保所 提供信息的准确性及可靠性,但不保证信息的正确性和完整性,且不对因信息的不正确或遗漏导致的任何 损失或损害承担责任。 3、任何透过本网站网页而链接及得到的资讯、产品及服务,本网站概不负责,亦不负任何法律责任。 4、本网站所刊发、转载的文章,其版权均归原作者所有,如其他媒体、网站或个人从本网下载使用,请在 转载有关文章时务必尊重该文章的著作权,保留本网注明的“稿件来源”,并白负版权等法律责任。
阅读剩余
THE END