一.本文目標
Jenkins實現(xiàn)持續(xù)集成與自動打包
自定義gradle打包腳本
自動上傳蒲公英并釘釘群通知
二.Jenkins持續(xù)集成與自動打包構建
一切重復的工作皆可自動化,大廠里面都有自動化包構建平臺,大多是基于Jenkins持續(xù)集成方案來定制的.
Jenkins是一個開源軟件項目,是基于Java開發(fā)的一種持續(xù)集成工具,使軟件的持續(xù)集成變成可能Jenkins提供數(shù)百個插件來支持構建,部署和自動化任何項目
Jenkins軟件包安裝與服務管理
- 安裝:brew install jenkins-lts
- 開啟服務:brew services start jenkins-lts
- 重啟服務:brew services restart jenkins-lts
- 升級服務:brew upgrade jenkins-lts
安裝Homebrew
Mac用戶需要先安裝Homebrew才能安裝Jenkins,如果已安裝請?zhí)^
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
將以上命令粘貼至終端即可安裝Homebrew。
服務啟動,這個過程首次可能需要4-5分鐘,耐心等待
三.在瀏覽器打開http://localhost:8080/
- 需要安裝一些插件,選擇安裝推薦的插件這個選項(這個過程建議翻墻比較快),過程需要30分鐘左右



-
賬戶創(chuàng)建,這里我們不創(chuàng)建新用戶,點擊右下角的使用admin賬戶繼續(xù),而admin賬戶的密碼就存儲在上面的紅色字體文件中
-
不用改實例配置,默認就好

主頁面

四.插件安裝--->環(huán)境配置--->創(chuàng)建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.環(huán)境配置(jdk,git,gradle,android_sdk)
- Manage Jenkins -->Global Tool Configuration
配置JDK

配置Git

配置gradle,建議和項目的gradle版本號保持一致

- Manage Jenkins -->Configule System
配置android_sdk,下面的Locale記得寫上zh_CN,這樣就支持中文了

3.創(chuàng)建Job
- 新建Item

- 配置項目
主頁--->項目--->配置
General的基本配置
選中This project is parameterized,然后添加參數(shù)選擇Choice Parameter,按照下面的格式填寫

- 源碼管理配置
需要添加項目在github或者gitlab的倉庫url,然后添加私鑰,當然也可以用賬號密碼,這里是私鑰演示

點擊添加按鈕,注冊憑證,可以直接選擇username-password
也可以選擇私鑰,Mac的私鑰是在 /Users/houyadong/.ssh/id_rsa 文件中

因為主項目是ASProj,然后引用了i-ui項目的一些模塊,這里需要選擇Additional Behaviours添加Check out to a sub-directory,然后添加倉庫的本地子目錄,如果項目是單工程結構,這一步可以跳過

- 構建觸發(fā)器
可以執(zhí)行周期性的構建項目,也可以當有新代碼提交的時候去構建,也可以定時構建,這里面我們不需要這些

- 構建環(huán)境
勾選圖中兩個,在構建的時候在控制臺中把時間戳打印上去,登錄的用戶信息設置到jenkins的環(huán)境變量里面

- 構建
自定義的打包腳本任務,打包之前先clean一遍,然后把堆棧信息給打印出來

點高級,需要勾選圖中的pass all job parameters as Project properties,然后在輸入框中輸入圖中內容
BUILD_TYPE:就是上面創(chuàng)建的debug包還是release包
BUILD_URL:指Jekins本次任務構建的url
JOB_NAME:本次構建的任務的名稱
BRANCH_NAME:本次構建的倉庫的分支
BUILD_USER:發(fā)起本次構建的當前用戶

參數(shù)透傳,這些key,還必須在項目根目錄下的gradle.properties中去同樣定義一份,值無所謂

4.獲取蒲公英的apiKey
注冊賬號,然后API信息中獲取
5.釘釘自定義消息格式
- 創(chuàng)建釘釘群

智能群助手,自定義,復制粘貼webhook

一定要定義關鍵詞,釘釘為了安全起見,在往這個webHook發(fā)送消息的時候,如果這個消息中不包含設置的關鍵詞,釘釘群是收不到消息的.
所以一般設置關鍵詞為項目名稱然后自定義腳本中 添加 項目名稱到消息體中,這樣釘釘就能收到信息了

6.自定義打包腳本上傳apk到蒲公英與釘釘群通知
在項目根目錄下創(chuàng)建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 {
//發(fā)送釘釘 消息--構建失敗
}
}
} 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 = ""
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("*************** 釘釘消息已發(fā)送 ***************")
}
//獲取當前時間
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執(zhí)行打包
項目 -->Build with parameters --> 選擇debug或release -- >開始構建

可以在控制臺看打包輸出

打包成功并且上傳到了蒲公英

發(fā)送釘釘群消息

