一.本文目标
Jenkins实现持续集成与自动打包
自定义gradle打包脚本
自动上传蒲公英并钉钉群通知
二.Jenkins持续集成与自动打包构建
一切重复的工作皆可自动化,大厂里面都有自动化包构建平台,大多是基于Jenkins持续集成方案来定制的.
Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,使软件的持续集成变成可能Jenkins提供数百个插件来支持构建,部署和自动化任何项目
Jenkins软件包安装与服务管理
- 安装:brew install jenkins-lts
- 开启服务:brew services start jenkins-lts
- 重启服务:brew services restart jenkins-lts
- 升级服务:brew upgrade jenkins-lts
安装Homebrew
Mac用户需要先安装Homebrew才能安装Jenkins,如果已安装请跳过
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
将以上命令粘贴至终端即可安装Homebrew。
服务启动,这个过程首次可能需要4-5分钟,耐心等待
三.在浏览器打开http://localhost:8080/
- 需要安装一些插件,选择安装推荐的插件这个选项(这个过程建议翻墙比较快),过程需要30分钟左右
-
账户创建,这里我们不创建新用户,点击右下角的使用admin账户继续,而admin账户的密码就存储在上面的红色字体文件中
-
不用改实例配置,默认就好
主页面
四.插件安装--->环境配置--->创建Job--->获取蒲公英api_key--->获取钉钉webhool--->编写gradle打包脚本
1.插件安装
- Multiple SCMs plugin -- 多仓库构建
- Groovy Postbuild -- groovy脚本
- build user vars plugin -- 获取当前登录用户
- Upload to pgyer -- apk上传蒲公英
- Locale plugin -- 本地语言
Manage Jenkins -->Manage Plugins 去进行安装上述插件,记得安装完成后重启服务
2.环境配置(jdk,git,gradle,android_sdk)
- Manage Jenkins -->Global Tool Configuration
配置JDK
配置Git
配置gradle,建议和项目的gradle版本号保持一致
- Manage Jenkins -->Configule System
配置android_sdk,下面的Locale记得写上zh_CN,这样就支持中文了
3.创建Job
- 新建Item
- 配置项目
主页--->项目--->配置
General的基本配置
选中This project is parameterized,然后添加参数选择Choice Parameter,按照下面的格式填写
- 源码管理配置
需要添加项目在github或者gitlab的仓库url,然后添加私钥,当然也可以用账号密码,这里是私钥演示
点击添加按钮,注册凭证,可以直接选择username-password
也可以选择私钥,Mac的私钥是在 /Users/houyadong/.ssh/id_rsa 文件中
因为主项目是ASProj,然后引用了i-ui项目的一些模块,这里需要选择Additional Behaviours添加Check out to a sub-directory,然后添加仓库的本地子目录,如果项目是单工程结构,这一步可以跳过
- 构建触发器
可以执行周期性的构建项目,也可以当有新代码提交的时候去构建,也可以定时构建,这里面我们不需要这些
- 构建环境
勾选图中两个,在构建的时候在控制台中把时间戳打印上去,登录的用户信息设置到jenkins的环境变量里面
- 构建
自定义的打包脚本任务,打包之前先clean一遍,然后把堆栈信息给打印出来
点高级,需要勾选图中的pass all job parameters as Project properties,然后在输入框中输入图中内容
BUILD_TYPE:就是上面创建的debug包还是release包
BUILD_URL:指Jekins本次任务构建的url
JOB_NAME:本次构建的任务的名称
BRANCH_NAME:本次构建的仓库的分支
BUILD_USER:发起本次构建的当前用户
参数透传,这些key,还必须在项目根目录下的gradle.properties中去同样定义一份,值无所谓
4.获取蒲公英的apiKey
注册账号,然后API信息中获取
5.钉钉自定义消息格式
- 创建钉钉群
智能群助手,自定义,复制粘贴webhook
一定要定义关键词,钉钉为了安全起见,在往这个webHook发送消息的时候,如果这个消息中不包含设置的关键词,钉钉群是收不到消息的.
所以一般设置关键词为项目名称然后自定义脚本中 添加 项目名称到消息体中,这样钉钉就能收到信息了
6.自定义打包脚本上传apk到蒲公英与钉钉群通知
在项目根目录下创建jenkins_package.gradle
import groovy.json.JsonSlurper
import java.text.SimpleDateFormat
task packageApk {
println("BUILD_TYPE:" + BUILD_TYPE)
File apkFileDir = null
dependsOn("assemble" + BUILD_TYPE.capitalize())
if(BUILD_TYPE.equals("release")){
apkFileDir = new File(project.buildDir, "outputs/apk/release")
}else{
apkFileDir = new File(project.buildDir, "outputs/apk/debug")
}
doLast {
def uploadFile = findApkFile(apkFileDir)
println("uploadFile:" + uploadFile.name)
uploadApk(uploadFile)
}
}
def findApkFile(File apkFile) {
println("apkFile:" + apkFile.name)
if (apkFile.isDirectory()) {
def files = apkFile.listFiles()
for (int i = 0; i < files.length; i++) {
def findFile = findApkFile(files[i])
if (findFile != null) {
return findFile
}
}
} else if (apkFile.name.endsWith(".apk")) {
return apkFile
} else return null
}
def uploadApk(File uploadApkFile) {
// 查找上传的 apk 文件, 这里需要换成自己 apk 路径
println("uploadApk:" + uploadApkFile.absolutePath + "--" + uploadApkFile.exists())
if (uploadApkFile == null || !uploadApkFile.exists()) {
throw new RuntimeException("apk file not exists!")
}
println "*************** upload start ***************"
String BOUNDARY = UUID.randomUUID().toString(); // 边界标识 随机生成
String PREFIX = "--", LINE_END = "\r\n";
String CONTENT_TYPE = "multipart/form-data"; // 内容类型
try {
URL url = new URL("https://www.pgyer.com/apiv2/app/upload");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setReadTimeout(30000);
conn.setConnectTimeout(30000);
conn.setDoInput(true); // 允许输入流
conn.setDoOutput(true); // 允许输出流
conn.setUseCaches(false); // 不允许使用缓存
conn.setRequestMethod("POST"); // 请求方式
conn.setRequestProperty("Charset", "UTF-8"); // 设置编码
conn.setRequestProperty("connection", "keep-alive");
conn.setRequestProperty("Content-Type", CONTENT_TYPE + ";boundary=" + BOUNDARY);
DataOutputStream dos = new DataOutputStream(conn.getOutputStream());
StringBuffer sb = new StringBuffer();
sb.append(PREFIX).append(BOUNDARY).append(LINE_END);//分界符
sb.append("Content-Disposition: form-data; name=\"" + "_api_key" + "\"" + LINE_END);
sb.append("Content-Type: text/plain; charset=UTF-8" + LINE_END);
//sb.append("Content-Transfer-Encoding: 8bit" + LINE_END);
sb.append(LINE_END);
sb.append("*********************************************");//替换成你再蒲公英上申请的apiKey
sb.append(LINE_END);//换行!
if (uploadApkFile != null) {
/**
* 当文件不为空,把文件包装并且上传
*/
sb.append(PREFIX);
sb.append(BOUNDARY);
sb.append(LINE_END);
/**
* 这里重点注意: name里面的值为服务器端需要key 只有这个key 才可以得到对应的文件
* filename是文件的名字,包含后缀名的 比如:abc.png
*/
sb.append("Content-Disposition: form-data; name=\"file\"; filename=\"" + uploadApkFile.getName() + "\"" + LINE_END);
sb.append("Content-Type: application/octet-stream; charset=UTF-8" + LINE_END);
sb.append(LINE_END);
dos.write(sb.toString().getBytes())
InputStream is = new FileInputStream(uploadApkFile)
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes)) != -1) {
dos.write(bytes, 0, len);
}
is.close();
dos.write(LINE_END.getBytes());
byte[] end_data = (PREFIX + BOUNDARY + PREFIX + LINE_END).getBytes();
dos.write(end_data);
dos.flush();
/**
* 获取响应码 200=成功 当响应成功,获取响应的流
*/
int res = conn.getResponseCode();
if (res == 200) {
println("Upload request success");
BufferedReader br = new BufferedReader(new InputStreamReader(conn.getInputStream()))
StringBuffer ret = new StringBuffer();
String line
while ((line = br.readLine()) != null) {
ret.append(line)
}
String result = ret.toString();
println("Upload result : " + result);
def resp = new JsonSlurper().parseText(result)
println result
println "*************** upload finish ***************"
sendMsgToDing(resp.data)
} else {
//发送钉钉 消息--构建失败
}
}
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
def sendMsgToDing(def data) {
def conn = new URL("*********************************************").openConnection()//替换成自己的钉钉webHook的url
conn.setRequestMethod('POST')
conn.setRequestProperty("Connection", "Keep-Alive")
conn.setRequestProperty("Content-type", "application/json;charset=UTF-8")
conn.setConnectTimeout(30000)
conn.setReadTimeout(30000)
conn.setDoInput(true)
conn.setDoOutput(true)
def dos = new DataOutputStream(conn.getOutputStream())
def downloadUrl = "https://www.pgyer.com/" + data.buildShortcutUrl
def qrCodeUrl = "![](" + data.buildQRCodeURL + ")"
def detailLink = "[项目地址](${BUILD_URL})"
def _title = "### 【${JOB_NAME}】构建成功"
def _content = new StringBuffer()
_content.append("\n\n### ${JOB_NAME}构建成功")
_content.append("\n\n构建版本:${BRANCH_NAME}")
_content.append("\n\n构建类型:${BUILD_TYPE}")
_content.append("\n\n下载地址:" + downloadUrl)
_content.append("\n\n" + qrCodeUrl)
_content.append("\n\n构建用户:${BUILD_USER}")
_content.append("\n\n构建时间:" + getNowTime())
_content.append("\n\n查看详情:" + detailLink)
def json = new groovy.json.JsonBuilder()
json {
msgtype "markdown"
markdown {
title _title
text _content.toString()
}
at {
atMobiles([])
isAtAll false
}
}
println(json)
dos.writeBytes(json.toString())
def input = new BufferedReader(new InputStreamReader(conn.getInputStream()))
String line = ""
String result = ""
while ((line = input.readLine()) != null) {
result += line
}
dos.flush()
dos.close()
input.close()
conn.connect()
println(result)
println("*************** 钉钉消息已发送 ***************")
}
//获取当前时间
def getNowTime() {
def str = "";
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Calendar lastDate = Calendar.getInstance();
str = sdf.format(lastDate.getTime());
return str;
}
在app中的build.gradle中添加
apply from: '../jenkins_package.gradle'
五.Jenkins执行打包
项目 -->Build with parameters --> 选择debug或release -- >开始构建
可以在控制台看打包输出
打包成功并且上传到了蒲公英
发送钉钉群消息