Jenkins 多分支流水线自动化构建Android实践

Jenkins 多分支流水线自动化构建Android项目打包并上传到fir.im

Jenkins 多分支流水线自动化构建实现了Git仓库下所有的分支管理,只要Git仓库目标分支的代码有更新,就会去自动触发打包上传流程 ,所有的分支只需一个通用的pipeline编写的脚本文件jenkinsFile 加上个性化config.yaml配置文件就可以管理Git分支的自动化构建打包上传步骤,首先说明完成这个流程需要用到哪些工具:

一 .准备工具

1.JDK(8/11) , SDK ,Gradle ,Git 下载并配置环境变量

1628068453(1).jpg

2.jenkin 安装

windows版本的

1628069220(1).jpg

然后直接下一步到安装完成,浏览器打开http://localhost:8080/ 显示jenkins主页。

①配置 Global Tool Configuration 相关项目的 JDK,SDK,Gradle ;配置Gradle环境变量时要注意一个坑,gradle版本要与编译的项目的Gradle版本相同,否则会出现时SDK License 问题;

②安装BlueOcean所有的插件方便图形化界面管理

③安装所有推荐的插件

微信截图_20210804173520.png

④创建项目 ,选择多分支流水线 ,配置git仓库地址和登录凭证,设置构建相关参数等

微信截图_20210804184034.png

⑤配置完运行报错找不到资源,这里有个坑,就是需要手动的修改jenkins全局项目的workspaceDir,因为jenkins安装的路径在系统盘路径超长,运行时会报错找不到资源,这是windows系统最长文件路径有限制,我们需要修改jenkins的config.xml文件里的workspaceDir路径,然后在Manager jenkins 里点击Reload Configuration from Disk应用配置生效就可以了

微信截图_20210804174042.png

1628067998(1).jpg

3.了解PipeLine语法

pipeline

编写jenkinsFile文件,jenkinsFile文件里决定了项目构建的各个步骤,比如Git仓库各个分支在代码提交后是否触发自动化构建时,打包方式,打包完成后执行gradle脚本任务,发送构建结果的邮件通知等

4.Python

安装python环境 安装pip 安装Python requests库, Requests是一常用的http请求库,它使用python语言编写,可以方便地发送http请求,如果还没有安装pip,这个链接 Properly Installing Python 详细介绍了在各种平台下如何安装python以及setuptools,pip,virtualenv等常用的python工具,可以使用下面的命令来进行安装:

pip install requests
复制代码

这些都是为了成功执行python脚本完成自动化上传apk到fir.im 准备的,这里也有个坑,当使用python 命令执行脚本时,jenkins报错找不到资源文件,其实就是jenkins不识别配置的python环境变量,只能把python指定为全路径后才可以。

5.fir.im 账号

获取api_token,调用上传APK接口,fir.im的域名换了注意得用最新的,否则接口报错无法上传apk

微信截图_20210804181044.png

6.插件pipeline utility steps 插件安装

为了在jenkinsFile读取yaml文件配置,可项目中公用一套jenkins文件然后根据config.yaml中的配置构建apk

二.编写JenkinsFile与config.yaml

​ 如下是Android demo项目的JenkinsFile文件的代码:

def loadValuesYaml(x){//读取项目根目录下的config.yaml 配置
  def valuesYaml = readYaml (file: 'config.yaml')
  return valuesYaml[x];
}
pipeline {
      //agent节点   多个构建从节点   有的只配置了Android环境用于执行Android项目构建,有的只能执行iOS项目构建,有的是用于执行Go项目
      //那这么多不同的节点怎么管理及分配呢?
      //那就是通过对节点声明不同的标签label,然后在我们的构建中指定标签,这样Jenkins就会找到有对应标签的节点去执行构建了
      //agent { label 'Android'}
      agent any
      options {//超时了,就会终止这次的构建  options还有其他配置,比如失败后重试整个pipeline的次数:retry(3)
        timeout(time: 1, unit: 'HOURS')
      }
      environment{//一组全局的环境变量键值对  用在stages 使用在“调用方式为${MARKET}”  注意只能在“ ”中识别
         MARKET = loadValuesYaml('market')
         BUILD_TYPE = loadValuesYaml('buildType')
      }
      stages {//这里我们已经有默认的检出代码了  开始执行构建和发布
        //可以根据分支配置构建参数   最好的方式时从一个yaml文件中获取对应的配置文件
         stage('readYaml'){
            steps{
                script{
                 println MARKET
                 println BUILD_TYPE
                }
             }
        }

        stage('Build master APK') {
            when {
                branch 'master'
            }
            steps {
              bat "./gradlew clean assemble${MARKET}${BUILD_TYPE}"
            }
            post {
                failure {
                    echo "Build master APK Failure!"
                }
                success {
                    echo "Build master APK Success!"
                }
            }
        }

        stage('Build dev APK') {
            when {
                branch 'dev-hcc'
            }
            steps {
                bat "./gradlew clean assemble${MARKET}${BUILD_TYPE}"
            }
            post {
                failure {
                    echo "Build dev APK Failure!"
                }
                success {
                    echo "Build dev APK Success!"

                }
            }
        }

        stage('ArchiveAPK') {//存储的apk
            steps {
                archiveArtifacts(artifacts: 'app/build/outputs/apk/**/*.apk', fingerprint: true, onlyIfSuccessful: true)
            }
            post {
                failure {
                    echo "Archive Failure!"
                }
                success {
                    echo "Archive Success!"
                }
            }
        }

        stage('Report') {//显示提交信息
            steps {
                echo getChangeString()
            }
        }

        stage('Publish'){//发布fir.im
          steps{
            bat './gradlew apkToFir'
          }
          post {
             failure {
                echo "Publish Failure!"
             }
             success {
                 echo "Publish Success!"
                 emailext body: 'apk版本有更新', subject: 'apk上传成功啦', to: '137**6*****@163.com'
             }
          }
        }
    }
}
//report 提交日志
@NonCPS
def getChangeString() {
    MAX_MSG_LEN = 100
    def changeString = ""

    echo "Gathering SCM Changes..."
    def changeLogSets = currentBuild.changeSets
    for (int i = 0; i < changeLogSets.size(); i++) {
        def entries = changeLogSets[i].items
        for (int j = 0; j < entries.length; j++) {
            def entry = entries[j]
            truncated_msg = entry.msg.take(MAX_MSG_LEN)
            changeString += "[${entry.author}] ${truncated_msg}\n"
        }
    }

    if (!changeString) {
        changeString = " - No Changes -"
    }
    return changeString
}
复制代码

如下是config.yaml的内容

market: Google
buildType: Debug

复制代码

三.编写模块下的build.gradle 里的Task

​ 放在android{}代码块内

 task apkToFir {
        //dependsOn 'assembleDebug'
        doLast {
            //def upUrl = "http://api.fir.im/apps"
            def upUrl = "http://api.bq04.com/apps"
            def appName = "jenkinsDemo"
            def bundleId = project.android.defaultConfig.applicationId
            def verName = project.android.defaultConfig.versionName
            def apiToken = "d319ac25103******dc4fbf545ad8a7"
            def iconPath = "app/src/main/res/mipmap-hdpi/ic_launcher.png"
            def apkPath = "app/build/outputs/apk/google/debug/app-google-debug.apk"
            def buildNumber = project.android.defaultConfig.versionCode
            def changeLog = "版本更新日志"
            //执行Python脚本
            def pythonPath = "c:\\users\\xinmo\\appdata\\local\\programs\\python\\python39\\python.exe"
            def process = "${pythonPath} upToFir.py ${upUrl} ${appName} ${bundleId} ${verName} ${apiToken} ${iconPath} ${apkPath} ${buildNumber} ${changeLog}".execute()
            println("开始上传至fir")
            //获取Python脚本日志,便于出错调试
            ByteArrayOutputStream result = new ByteArrayOutputStream()
            def inputStream = process.getInputStream()
            byte[] buffer = new byte[1024]
            int length
            while ((length = inputStream.read(buffer)) != -1) {
                result.write(buffer, 0, length)
            }
            println(result.toString("UTF-8"))
            println "上传结束 "
        }
    }
复制代码

四.Python脚本执行上传fir.im任务

​ 以下是upToFir.py的代码:

# coding=utf-8
# encoding = utf-8
import requests
import sys


def upToFir():
    # 打印传递过来的参数数组长度,便于校验
    upUrl = sys.argv[1]
    appName = sys.argv[2]
    bundleId = sys.argv[3]
    verName = sys.argv[4]
    apiToken = sys.argv[5]
    print (apiToken)
    iconPath = sys.argv[6]
    apkPath = sys.argv[7]
    buildNumber = sys.argv[8]
    changeLog = sys.argv[9]
    print(apkPath)
    queryData = {'type': 'android', 'bundle_id': bundleId, 'api_token': apiToken}
    iconDict = {}
    binaryDict = {}
    # 获取上传信息
    try:
        response = requests.post(url=upUrl, data=queryData)
        json = response.json()
        iconDict = (json["cert"]["icon"])
        binaryDict = (json["cert"]["binary"])
    except Exception as e:
        print(e.message)

    # 上传apk
    try:
        file = {'file': open(apkPath, 'rb')}
        param = {"key": binaryDict['key'],
                 'token': binaryDict['token'],
                 "x:name": appName,
                 "x:version": verName,
                 "x:build": buildNumber,
                 "x:changelog": changeLog}
        req = requests.post(url=binaryDict['upload_url'], files=file, data=param, verify=False)
        print(req.status_code)
    except Exception as e:
        print(e.message)

    # 上传logo
    try:
        file = {'file': open(iconPath, 'rb')}
        param = {"key": iconDict['key'],
                 'token': iconDict['token']}
        req = requests.post(url=iconDict['upload_url'], files=file, data=param, verify=False)
        print(req.status_code)
    except Exception as e:
        print(e.message)


if __name__ == '__main__':
    upToFir()

复制代码

经过以上的配置后,当我们提交代码到git仓库时,就具备了构建的条件了,我们需要配置下Scan 多分支流水线 触发器检查时间间隔,只要发现代码有更新,就会触发构建APK并上传到Fir.im了,我们还可以在BlueOcean里看到流水线的相关步骤执行耗时等,如图

微信截图_20210804184706.png

微信截图_20210804185142.png

五.根据config.yaml修改local.properties 文件

我们的项目会把一些配置等放在本地的local.properties,如项目准备编译的模块,渠道,编译特定模块的开关等。为了能够在每个分支都能够修改这个配置文件,首先我们要把这份local.properties 从本地的项目里copy一份到jenkins服务器对应的workSpace项目的根路径下,然后通过JenkinsFile读取config.yaml的参数,再把对应的键值写入到local.properties,

下面会介绍如何实现这个流程:

1.首先我们会使用Pipeline自带的readFile和writeFile

思路是这样的,先通过readFile方法获取到原来文件的内容,返回是一个字符串对象。然后根据换行符把字符串对象给切片,拿到一个list对象,遍历这个list,用if判断,根据Key找到这行,然后改写这行。由于这我们只是在内存改写,所以需要提前定义一个list对象来把改写和没有改写的都给add到新list,然后定义一个空字符串,遍历新list,每次拼接一个list元素都加上换行符。这样得到字符串就是一个完整内容,然后把这个内容利用writeFile方法写回到原来的config文件,实现此需求的editFile.groovy代码如下:

import hudson.model.*;

def setKeyValue(key, value, file_path) {
    // read file, get string object
    file_content_old = readFile file_path
    println file_content_old
    //遍历每一行,判断,然后替换字符串
    lines = file_content_old.tokenize("\n")
    new_lines = []
    lines.each { line ->
        if(line.trim().startsWith(key)) {
            line = key + "=" + value
            new_lines.add(line)
        }else {
            new_lines.add(line)
        }
    }
    // write into file
    file_content_new = ""
    new_lines.each{line ->
        file_content_new += line + "\n"
    }

    writeFile file: file_path, text: file_content_new, encoding: "UTF-8"
}
return this
复制代码

2.local.properties 配置:

sdk.dir=C\:\\Users\\xinmo\\AppData\\Local\\Android\\Sdk
market=google
build.module = chk
build.environment=product
compileSensorsSdk = false
复制代码

3.config.yaml配置:

#google   huawei
market: Google
#Debug   Release
buildType: Debug
#product(正式环境) stage(灰度环境) test(测试环境)
build.environment: test
#声明开发的时候需要编译的模块
#hrxs legendnovel chk teseyanqing xiaoshuodaquan
#english(包含novelCat foxNovel)  Indonesia freenovel popnovel
build.module: foxNovel
#Sdk
compileSensorsSdk: true

复制代码

4.JenkinsFile修改后代码:

def loadValuesYaml(x){
  def valuesYaml = readYaml (file: 'config.yaml')
  return valuesYaml[x];
}
pipeline {
      //agent节点   多个构建从节点   有的只配置了Android环境用于执行Android项目构建,有的只能执行iOS项目构建,有的是用于执行Go项目
      //那这么多不同的节点怎么管理及分配呢?
      //那就是通过对节点声明不同的标签label,然后在我们的构建中指定标签,这样Jenkins就会找到有对应标签的节点去执行构建了
      //agent { label 'Android'}
      agent any
      options {//超时了,就会终止这次的构建  options还有其他配置,比如失败后重试整个pipeline的次数:retry(3)
        timeout(time: 1, unit: 'HOURS')
      }
      environment{//一组全局的环境变量键值对  用在stages 使用在“调用方式为${MARKET}”  注意只能在“ ”中识别
         MARKET = loadValuesYaml('market')
         BUILD_TYPE = loadValuesYaml('buildType')
         BUILD_ENVIRONMENT = loadValuesYaml('build.environment')
         BUILD_MODULE = loadValuesYaml('build.module')
         COMPILE_SENSORS_SDK = loadValuesYaml('compileSensorsSdk')
      }
      stages {//这里我们已经有默认的检出代码了  开始执行构建和发布
        //可以根据分支配置构建参数   最好的方式时从一个yaml文件中获取对应的配置文件
         stage('readYaml'){
            steps{
                script{
                 println MARKET
                 println BUILD_TYPE
                }
             }
        }

        stage('set local properties'){
          steps{
              script{
                 	   editFile = load env.WORKSPACE + "/editFile.groovy"
                 	   config_file = env.WORKSPACE + "/local.properties"
                 	   try{
                 	       editFile.setKeyValue("market", "${MARKET}", config_file)
                 	       editFile.setKeyValue("build.module", "${BUILD_MODULE}", config_file)
                 	       editFile.setKeyValue("build.environment", "${BUILD_ENVIRONMENT}", config_file)
                 	       editFile.setKeyValue("compileSensorsSdk", "${COMPILE_SENSORS_SDK}", config_file)
                 	       file_content = readFile config_file
                           println file_content
                 	       }catch (Exception e) {
                 	           error("Error editFile :" + e)
                 	        }
              }
          }
        }

        stage('Build master APK') {
            when {
                branch 'master'
            }
            steps {
              bat "./gradlew clean assemble${MARKET}${BUILD_TYPE}"
            }
            post {
                failure {
                    echo "Build master APK Failure!"
                }
                success {
                    echo "Build master APK Success!"
                }
            }
        }

        stage('Build dev APK') {
            when {
                branch 'dev-hcc'
            }
            steps {
                bat "./gradlew clean assemble${MARKET}${BUILD_TYPE}"
            }
            post {
                failure {
                    echo "Build dev APK Failure!"
                }
                success {
                    echo "Build dev APK Success!"

                }
            }
        }

        stage('ArchiveAPK') {//存储的apk
            steps {
                archiveArtifacts(artifacts: 'app/build/outputs/apk/**/*.apk', fingerprint: true, onlyIfSuccessful: true)
            }
            post {
                failure {
                    echo "Archive Failure!"
                }
                success {
                    echo "Archive Success!"
                }
            }
        }

        stage('Report') {//显示提交信息
            steps {
                echo getChangeString()
            }
        }

        stage('Publish'){//发布fir.im
          steps{
            bat './gradlew apkToFir'
          }
          post {
             failure {
                echo "Publish Failure!"
             }
             success {
                 echo "Publish Success!"
                 emailext body: 'apk版本有更新', subject: 'apk上传成功啦', to: '1375****431@163.com'
             }
          }
        }
    }
}
//report 提交日志
@NonCPS
def getChangeString() {
    MAX_MSG_LEN = 100
    def changeString = ""

    echo "Gathering SCM Changes..."
    def changeLogSets = currentBuild.changeSets
    for (int i = 0; i < changeLogSets.size(); i++) {
        def entries = changeLogSets[i].items
        for (int j = 0; j < entries.length; j++) {
            def entry = entries[j]
            truncated_msg = entry.msg.take(MAX_MSG_LEN)
            changeString += "[${entry.author}] ${truncated_msg}\n"
        }
    }

    if (!changeString) {
        changeString = " - No Changes -"
    }
    return changeString
}
复制代码

六.总结

Jenkins 多分支流水线自动化构建,可以将构建过程都交给config.yaml去管理,通过pipeLine语法,写jenkinsFile设置项目整体构建步骤,jenkins读取yaml配置文件 ,调用groovy脚本修改本地文件local.properties,执行打包流程,groovy调用python脚本上传fir.im. 通过这些步骤实现各个分支的自动构建apk上传到fir.im.

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享