Jenkins+Git+python+Pgyer Android打包发布实践

[TOC]

经常在开发的时候,测试/产品/运营等人员会来要求安装一下软件,这时候不得不停下手中的事情来打包安装,但终归不是长久之计:

  1. 自己开发时被经常打断思路;
  2. 停下来手头的工作来打包,每次怎么也得浪费个几分钟,长期下来不划算;
  3. 开发中的代码经常不是稳定或者未经过自测的代码,冒然给人安装总是容易发生问题,徒增bug;

作为一个 '懒人' ,这种重复性的工作肿么能每次都自己动手呢,另外最好再有个地方能提供稳定分支代码的安装包供人下载安装,省得有人说不会安装apk ==!...
话说前公司就提供有自动打包功能,以前不觉得有什么,等到没有的时候才发觉它的好...无奈,只能自己动手搭一套;

图侵删

P.S. jenkins会去gitlab仓库中读取最新的分支代码到本地,具体地址也就是其环境变量 WORKSPACE 指代的位置;

基于:
系统: mac 10.12.4(Sierra)
Jenkins: 2.46.1
Git: 2.10.0
Python: 3.x

Jenkins的基本使用

安装运行Jenkins

  1. 官网 下载软件,应该是一个 jenkins.war 单文件;
  2. 运行:
// 方式1: 假如系统中安装有Tomcat,就把它当做普通的war包放置到 webapps/ 下运行即可;
// 方式2: (假设 jenkins.war 放置在 ~/Downloads/ 目录下)
cd  ~/Downloads/
nohup java -jar jenkins.war & // 后台运行jenkins.war程序,默认使用8080端口
// 指定端口
--httpPort=8080 // 用来设置jenkins运行时的web端口,避免冲突
  1. 安装运行成功后在浏览器中打开 localhost:8080 (端口号请按需修改),第一次会要求输入账号密码, jenkins 提供了一个初始密码,可以根据页面提示在文件 initialAdminPassword 中获取:
 sudo cat /Users/***/initialAdminPassword // 提示路径可能不同,根据页面提示修改
initialAdminPassword
  1. 初始化后就会弹出添加账号的界面,按需设置一个新的账号密码,也可以后续在 Manage Users 中进行创建或修改;
  2. 登录成功后会有个安装插件提示页面,选择 Install suggested plugins :
    安装插件

后续也可在 jenkins首页 - Manage Jenkins - Manage Plugins 中安装插件,主要是 Gradle Plugin Android Signing Plugin Git Parameter Plug-In GitHub Authentication plugin Gitlab Authentication plugin Git plugin

  1. 安装完重启就可以在首页添加任务 New Item ,添加完成后会在右侧显示已添加的 job 列表:
    new item

初始化配置

开始打包发布Android应用前需要进行如下环境的设置:Android/Git/Gradle/Python等

指定ANDROID_HOME全局变量

==! 不知道咋回事,没有识别到我配置在 ~/.bash_profile 中的ANDROID_HOME环境变量,最后折腾了好久才发现要在jenkins中手动指定一个

Manage Jenkins - Configure System - Global properties 勾选 Environment variables ,并添加一个:

Name : ANDROID_HOME
Value : /Users/***/Applications/AndroidSDK
ANDROID_HOME

配置JDK/Git/Gradle

Manage Jenkins - global tool configuration 中按需选择工具 , name 随意指定,其他的参考下图:

// 配置JDK主目录
name:     MAC_JDK
path:     /Library/Java/JavaVirtualMachines/jdk1.8.0_101.jdk/Contents/Home

// 配置Git运行路径
name:    Default
path:    /usr/bin/git

// 配置Gradle主目录
name:    gradle3.5
path:    /Users/lynxz/.sdkman/candidates/gradle/current/bin
// 注意: 在 `gradle /current/bin/bin`中需存在 `gradle` 可执行文件;
// 备注: 我之前是使用 `sdkman` 来安装的 `gradle` ,所以路径比较奇怪
curl -s https://get.sdkman.io | bash
sdk install gradle 3.5
Home指定

创建和配置job

  1. 在jenkins首页左侧导航栏中点击 new item , 输入名称, 选择 Freestyle project ,点击 ok 按钮即可;
  2. 创建完成后,在首页右侧的 Job 列表中选择刚才创建的job,然后选择 Configure 进行配置;
  3. General 中按需输入 project nameDescription ;
    另外,这个tab页面中比较常用的还有参数化设置( This project is parameterized ), 后续会讲到;
  4. Source Code Management 中指定版本管理类型/仓库地址/认证信息等;
    Source Code Management
  5. Build 中选择 Invoke Gradle Script ,在 Gradle Version 下拉列表中选择自定义的本机Gradle版本;并在 Tasks 中输入打包命令:
// 默认未做多渠道多版本配置时,打包release类型的命令:
clean assembleRelease  --stacktrace --debug

如果有需要将 general 标签也中定义的变量注入到项目中让 gradle 脚本使用,则请勾选 Pass job parameters as Gradle properties ,则gradle脚本中用到的同名自定义变量就会使用jenkins中指定的值;

build.png

参数化构建

有时候需要在构建的时候进行一些定制化操作,比如指定编译的代码分支,增加打包版本说明等,又或者项目中使用了私有仓库,而仓库的登录账号名(如mavenUser)以及密码(mavenPassword)存储于 gradle.properties (不同步到gitlab仓库中),这时就需要添加参数,并将该参数注入到Android项目中了,以便gradle脚本能获取到正确的值,具体操作如下:

  1. 在job页面的 Configure - General 面板中勾选 This project is parameterized;
  2. Add Parameter 下拉列表中就可以选择对应的类型变量
    参数化构建

注意: 若参数需要注入到Android项目构建脚本中,则需要勾选 configure - Build - Pass job parameters as Gradle properties ;

  1. 比如选择添加一个 String Parameter
    String Parameter
  2. 比如增加一个 Choice Parameter ,用于指定要打包的版本:
    Choice Parameter

    图中指定的三个choice是我在Android项目 app/build.gradle 中定义过的,用于后续的打包命令:
android{
    buildTypes {
        release {
            signingConfig signingConfigs.release
        }
        //不能以"test"开头
        tstEnv {
            debuggable true
            signingConfig signingConfigs.release
        }
        debug {
            versionNameSuffix "-dev"
            debuggable true
            signingConfig signingConfigs.release
        }
    }
}
效果

上面指定的 choice parameter 类型参数 buildTypes 便可用在 Build 命令中:

使用

签名

还没去研究过jenkins签名插件,一般直接在Android项目中配置好脚本即可:

  1. 将签名文件 *.jks 放置于 app/ 目录下;
  2. app/build.gradle 中配置签名参数,这样jenkins打包出来的apk就是签名过的:
android{
  signingConfigs {
        release {
            keyAlias '***'
            keyPassword '***'
            storeFile file('*.jks')
            storePassword '***'
        }
    }

 buildTypes {
        release {
            signingConfig signingConfigs.release
        }

        debug {
            signingConfig signingConfigs.release
      }
  }
}

P.S. 不过总觉得这样直接公开签名文件到仓库中不太好,在打包服务器上进行控制会更合适点,毕竟知晓的人更少,这个后续再研究,先占个位;

获取当前用户的名称等信息

  1. 需要安装插件 user build vars plugin ,可在插件列表中直接获取安装 或者到 这里 下载插件包;
  2. 安装后等待jenkins重启,并找到 job - configure - Build Environment - Set jenkins user build variables,勾选此项后才生效;
  3. 在 shell 中便可像jenkins自带的变量那样使用,如 echo "$BUILD_USER":
插件可用的变量名 变量描述
BUILD_USER Full name (first name + last name)
BUILD_USER_FIRST_NAME First name
BUILD_USER_LAST_NAME Last name
BUILD_USER_ID Jenkins user ID
BUILD_USER_EMAIL Email address

Build History 定制

默认的job构建历史命名( 如 #3​5 Apr 27, 2017 11:15 AM )不容易理解记忆,我们可以自定义,加入构建者姓名,版本等信息;
定制包括"build name" 和 "build description" 两部分的定制,效果如下

默认 -> 定制

构建名称定制

  1. 下载 Build Name Setter Plugin 插件;
  2. 手动安装: manage jenkins - manage plugins - Advancedupload plugin 中选择刚才下载的插件,提交后重启jenkins即可;
  3. 选择一个 job 进入 Configure - Build Environment ,就会多出一个 Set Build Name 复选项,勾选后即可定制;
    定制build名称

构建描述定制

  1. 安装插件 description setter plugin (可以在jenkins的 manage plugins 中找到);

  2. 重启jenkins后,进入 job 的 Configure - post-build Actions ,选择 Add post-build action - set build description 即可定制;

    Add post-build action

  3. 为了显示蒲公英的二维码图片,需要先在 manage jenkins - Configure global security ,找到 Markup Formatter ,将默认的 plain text 改为 safe html;

    safe html

  4. 在 job - configure - post-build actions - set build description 中就可以输入html标签了,比如我设置了:

// 这里的${pgyerNotes}是我在general中添加的参数
<p>${pgyerNotes}</p><br/>![](https://static.pgyer.com/app/qrcode/Cb6T)<br/><a href='https://www.pgyer.com/Cb6T'>Download Directly</a>

构建完成后打包记录并显示在 job 首页面,便于下载

  1. 进入 job - Configure - Post-Build Actions
  2. Add post-build action 列表中选择 Archive the artifacts 输入要存档的文件路径,可使用通配符,比如我输入的是 app/build/outputs/apk/Sb*.apk ,就会在job项目首页看到存档记录,可以直接下载;
    Archive the artifacts

    注意:这里不能使用 ${WORKSPACE} 等变量,貌似就是直接以当前工作空间为根目录的;</br>
    效果

使用shell上传apk到蒲公英

请首先到 蒲公英 上注册账号,并认证,若不认证则上传失败;
蒲公英上传文件接口
蒲公英的key值可在 账户设置 - API信息 中查看到
由于我是mac系统,因此在 Job - Configure - Build - Add build step 列表中选择 Execute shell,运行shell脚本
P.S. 若是对shell不熟悉,可参考 教程
另外,由于我在项目中根据版本号(vesionName)重命名了生成的apk,因此需要在shell脚本中提取版本号以便获得apk全称,进而上传蒲公英

app/build.gradle

自定义apk名称,这里用到了versionName

# build -> Add build step -> Execute shell
pgyerApiKey="******"
pgyerUKey=="******"
echo "获取apk版本号..."
#  ${WORKSPACE} 是jenkins提供的环境变量,表示当前项目跟目录路径
#  下面的命令是获取 app/build.gradle 的第17行内容,然后按照双引号进行切换,提取第2部分内容,即上面图示中的  1.1.4
versionName=`sed -n '17p' ${WORKSPACE}/app/build.gradle | cut -d \" -f 2`
echo "获取apk所在路径..."
# _360 是项目中定义了多渠道,但由于之前在 Build - Task 中设置的打包命令,直接指定了渠道号,因此这里也直接固定写好就可以;
apkAbsPath="${WORKSPACE}/app/build/outputs/apk/SonicMoving_${buildTypes}_[_360]_v${versionName}.apk"
echo "上传apk到蒲公英进行发布..."
response=$(curl -F "file=@${apkAbsPath}" -F "uKey=${pgyerUKey}" -F "_api_key=${pgyerApiKey}" https://qiniu-storage.pgyer.com/apiv1/app/upload)
echo "上传结束"

# 原本上传结束后想要使用 jq 工具 (`brew install jq`) 对蒲公英上传时返回的response进行json处理的,结果在电脑的shell中测试可行,但写到这里就一直不成功,无奈,只好放弃
# 提取蒲公英返回的json数据中的 appShortcutUrl 字段值,可拼接成下载地址
#responseCode=$(echo -E "${response}" | jq .code) 
#if [ $((responseCode)) == 0 ] 
#then
#   echo "上传结束,处理返回相应..."
#   appShortcutUrl=$(echo -E "${response}" | jq ".data.appShortcutUrl" | cut -d \" -f 2)
#   apkOnlineUrl="https://www.pgyer.com/${appShortcutUrl}"
#else
#   echo "上传失败,返回码为: ${responseCode} ,具体请看日志"
#fi
jenkins报错

使用python上传apk到蒲公英

最早之前我也是尝试直接使用python插件的,操作如下:

  1. 安装 Python Plugin;
  2. 在 job 的 Configure - Build 中就会多一个选项 Execute python script;
import os
# 获取jenkins变量 'BUILD_NUMBER'
print("build_number is ==> ",os.getenv("BUILD_NUMBER"))

但是由于我是mac,系统中默认的python是2.7.x,而我又装了其他版本的python,虽然在jenkins的全局变量中指定了python版本,但实际执行的时候却用的不是它,大致的错误如下:

python插件执行错误信息

可以发现jenkins把我们写在 python script 中的生成了一份位于 /var/.../*.py 的文件,然后使用系统默认的 python 版本来执行,而我 mac 默认的python是python2.7.*,导致里面写的很多基于python3.x的代码出错:
默认的python版本

解决方案

使用python多版本问题的常用方法 virtualenv,最后演变成通过shell来启用版本隔离,然后手动调用python命令加载脚本:
Build - add build step - execute shell 中写入如下脚本:

# 如果当前无指定的环境目录存在,则创建,并指定python版本
if [ ! -d ".env" ]; then
    virtualenv -p /usr/local/bin/python3 .env
fi
# 启动virtualenv
source .env/bin/activate
echo "当前操作的用户是 : $BUILD_USER "

#requestsLibName="requests"
#isInstallRequest=$(pip freeze | grep $requestsLibName)
#if [[ $isInstallRequest =~ "*requests*" ]];then
#   pip install requests
#fi
# 安装所需要的第三方库,若已安装,则不会重新安装
pip install requests
# 运行指定路径下的python脚本
python3 ~/Desktop/upload.py

上面shell脚本中的 upload.py 内容如下(可考虑将其放在Android项目中,上传gitlab):

#!/usr/local/bin/python3.5
# -*- coding: utf-8 -*-

'''
jenkins 打包线上代码生成apk后发布到蒲公英
本脚本路径:  ~/Desktop/upload.py
'''

import os
import io
import re
import requests
import json

WORKSPACE = os.getenv("WORKSPACE")  # 获取jenkins环境变量
userName = os.getenv("BUILD_USER")  # 获取用户名
buildTypes = os.getenv("buildTypes")  # 获取用户选择的编译版本
pgyerNotes = os.getenv("pgyerNotes")  # 获取用户填写的版本说明

# 重置默认编码为utf8
import sys
default_encoding = 'utf-8'
if sys.getdefaultencoding() != default_encoding:
    reload(sys)
    sys.setdefaultencoding(default_encoding)
# 确认下当前python版本
print("当前编译器版本: %s " % sys.version)
print("python编译器详细信息: ", sys.version_info)

# 获取版本号
# guild.gralde文件所在路径
buildGradleFilePath = "%s/app/build.gradle" % (WORKSPACE)  # 指定文件所在的路径
print("build.gradle路径是: ", buildGradleFilePath)

# 读取build.gradle并获取versionName值 
with open(buildGradleFilePath, 'r', encoding='utf-8') as buildGradleFile:
    line = buildGradleFile.readlines()[16:17][0] # 读取第17行数据, 切片从0开始;
    print("line 17 is .... ", line)
    versionName = re.split(r'\"', line)[1]
    print("版本号为: %s" % versionName)

# "获取apk所在路径..."
apkAbsPath = "%s/app/build/outputs/apk/SonicMoving_%s_[_360]_v%s.apk" % (WORKSPACE, buildTypes, versionName)
print("准备上传apk到蒲公英进行发布,apk所在路径为: %s" % apkAbsPath)

# 上传结束后发出请求通知服务端,进而由服务端发送钉钉消息
def notify_upload_result(msg):
    headers = {'user-agent': 'jenkins_upload_pgyer'}
    params = {'userName': userName, 'msg': msg}  # 字段中值为None的字段不会被添加到url中
    response = requests.get('http://btcserver.site:8080/WebHookServer_war', params=params, headers=headers)
    #response = requests.get('http://localhost:8081', params=params, headers=headers)
    print("通知webhook服务器结果: ", response.text)

# 蒲公英账号信息
pgyerApiKey = "******"
pgyerUKey = "******"

# response=$(curl -F "file=@${apkAbsPath}" -F "uKey=${pgyerUKey}" -F "_api_key=${pgyerApiKey}" -F "updateDescription=${pgyerNotes}" https://qiniu-storage.pgyer.com/apiv1/app/upload | jq .)
# post请求中所需携带的信息
data = {
    '_api_key': pgyerApiKey,
    'uKey': pgyerUKey,
    'updateDescription': pgyerNotes
}

files = {'file': open(apkAbsPath, 'rb')}
uploadUrl = 'https://qiniu-storage.pgyer.com/apiv1/app/upload'
response = requests.post(uploadUrl, data=data, files=files)
print(response.status_code, response.text)
if response.status_code == 200:
    print("上传成功,通知webhook服务器...")
    notify_upload_result(response.text)
else:
    print("上传失败,状态码为: %s" % (response.status_code))

蒲公英发布成功后通知钉钉

参考 打通Gitlab与钉钉之间的通讯
这里有两种方式通知服务器后台:

  1. 使用蒲公英项目中自带的webhook功能,但是这个通知无法具体得知是谁进行的这次打包发布,并且若上传蒲公英失败的话也不会进行webhook通知,因此不太方便:


    蒲公英webhook设置
  2. 如上面python脚本中写那样,在上传返回后,主动调用服务器接口,将response和jenkins相关信息上传,然后后台有针对性的发送消息;

碰到的异常

1. Failed to connect to repository : Command "git ls-remote -h https://git.***.git HEAD" returned status code 143:

需要在仓库中添加公钥;

//获取公钥
ssh-keygen -t rsa -f ~/.ssh/id_rsa.pub

测试时用的是 coding.net , 因此在 coding.net 对应项目的 设置 - 部署公钥 - 新建部署公钥 将刚才输出的公钥粘贴进去即可;

2. token-macro v1.5.1 is missing. To fix, install v1.5.1 or later

token-macro-plugin
到插件管理页面中搜索 Token Macro Plugin 安装即可;

推荐阅读更多精彩内容