Jenkins 多分支流水线自动化构建Android项目打包并上传到fir.im
Jenkins 多分支流水线自动化构建实现了Git仓库下所有的分支管理,只要Git仓库目标分支的代码有更新,就会去自动触发打包上传流程 ,所有的分支只需一个通用的pipeline编写的脚本文件jenkinsFile 加上个性化config.yaml配置文件就可以管理Git分支的自动化构建打包上传步骤,首先说明完成这个流程需要用到哪些工具:
一 .准备工具
1.JDK(8/11) , SDK ,Gradle ,Git 下载并配置环境变量

2.jenkin 安装
windows版本的

然后直接下一步到安装完成,浏览器打开http://localhost:8080/ 显示jenkins主页。
①配置 Global Tool Configuration 相关项目的 JDK,SDK,Gradle ;配置Gradle环境变量时要注意一个坑,gradle版本要与编译的项目的Gradle版本相同,否则会出现时SDK License 问题;
②安装BlueOcean所有的插件方便图形化界面管理
③安装所有推荐的插件

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

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


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

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里看到流水线的相关步骤执行耗时等,如图


五.根据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.



















![[02/27][官改] Simplicity@MIX2 ROM更新-一一网](https://www.proyy.com/wp-content/uploads/2020/02/3168457341.jpg)



![[桜井宁宁]COS和泉纱雾超可爱写真福利集-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/4d3cf227a85d7e79f5d6b4efb6bde3e8.jpg)

![[桜井宁宁] 爆乳奶牛少女cos写真-一一网](https://www.proyy.com/skycj/data/images/2020-12-13/d40483e126fcf567894e89c65eaca655.jpg)