Jenkins是開源CI&CD軟件領導者, 提供超過1000個插件來支持構建、部署、自動化, 知足任何項目的須要。html
咱們平常開發通常流程: Commit -> Push -> Merge -> Build. 基本就算完成. 而Jenkins的存在就是代替這一些系列從而實現自動化,側重在於後面幾個階段,咱們能夠作不少的事情. 自動化的過程是確保構建編譯都是正確的,平時咱們手動編譯不一樣版本的時候不免可能會出錯,有了它能夠下降編譯錯誤,提升構建速度. 然而通常咱們Jenkins都是須要配合Docker來完成的,因此須要具有必定的Docker的基礎與瞭解. 文末有Github地址,共享了DockerFile及JenkinsFile. Why Pipeline?java
詳細如圖(Gitlab CI/CD): git
在MergeRequest/PullRequest中應用以下:一個DevOps的工做序列基本主要區分與Jenkins Server兩種工做模式,這兩種工做模式分爲:github
下面主要介紹一下以Webhook工做方式的時序圖以下:web
sequenceDiagram
User ->> Gitlab/Github: push a commit
Gitlab/Github-->>Jekins: push a message via webhook
Jenkins -->> Jenkins: Sync with branchs and do a build with freestyle if there are changes
Jenkins --x Gitlab/Github: Feedback some comments on MR or IM/EMAIL
複製代碼
這將產生一個流程圖。:docker
graph LR
A(User) --Push a commit --> B(Gitlab/Github)
B --Push a message via webhook --> C(Jenkins)
複製代碼
配置一個Jenkins Server;(因爲文章主要講解Jenkins腳本高級應用,因此還請網上搜索相關環境搭建)api
在Jenkins 裏面建立一個應用以下圖: 緩存
配置好對應的遠程倉庫地址後,咱們須要指定Jenkins腳本路徑以下: 服務器
因爲Jenkins配置的路徑是在項目路徑下,因此咱們Android Studio也得配置在對應跟佈局下: app
最後以Gitlab爲例子配置Webhook以下:
全部的配置完畢後,接下來就是詳解Jenkins腳本.
pipeline {
agent any
stages {
stage('Build') {
steps {
// Do the build with gradle../gradlew build
}
}
stage('Test') {
steps {
// Do some test script
}
}
stage('Deploy') {
steps {
// Deploy your project to other place
}
}
}
}
複製代碼
高級特性詳解:
/** * Add the comment to gitlab on MR if the MR is exist and state is OPEN */
def addCommentToGitLabMR(String commentContent) {
branchHasMRID = sh(script: "curl --header \"PRIVATE-TOKEN: ${env.gitUserToken}\" ${GITLAB_SERVER_URL}/api/v4/projects/${XXPROJECT_ID}/merge_requests?source_branch=${env.BRANCH_NAME} | grep -o 'iid\":[^,]*' | head -n 1 | cut -b 6-", returnStdout: true).trim()
echo 'Current Branch has MR id : ' + branchHasMRID if (branchHasMRID == '') {
echo "The id of MR doesn't exist on the gitlab. skip the comment on MR"
} else {
// TODO : Should be handled on first time.
TheMRState = sh(script: "curl --header \"PRIVATE-TOKEN: ${env.gitUserToken}\" ${GITLAB_SERVER_URL}/api/v4/projects/${XXPROJECT_ID}/merge_requests?source_branch=${env.BRANCH_NAME} | grep -o 'state\":[^,]*' | head -n 1 | cut -b 9-14", returnStdout: true).trim()
echo 'Current MR state is : ' + TheMRState if (TheMRState == 'opened') {
sh "curl -d \"id=${XXPROJECT_ID}&merge_request_iid=${branchHasMRID}&body=${commentContent}\" --header \"PRIVATE-TOKEN: ${env.gitUserToken}\" ${GITLAB_SERVER_URL}/api/v4//projects/${XXPROJECT_ID}/merge_requests/${branchHasMRID}/notes"
} else {
echo 'The MR not is opened, skip the comment on MR'
}
}
}
複製代碼
def pushTag(String gitTagName, String gitTagContent) {
sh "curl -d \"id=${XXPROJECT_ID}&tag_name=${gitTagName}&ref=development&release_description=${gitTagContent}\" --header \"PRIVATE-TOKEN: ${env.gitUserToken}\" ${GITLAB_SERVER_URL}/api/v4/projects/${XXPROJECT_ID}/repository/tags"
}
複製代碼
environment {
GRADLE_CACHE = '/tmp/gradle-user-cache'
}
...
agent {
dockerfile {
filename 'Dockerfile'
// https://github.com/gradle/gradle/issues/851
args '-v $GRADLE_CACHE/.gradle:$HOME/.gradle --net=host'
}
}
複製代碼
完整的JenkinsFile;
#!/usr/bin/env groovy
//This JenkinsFile is based on a declarative format
//https://jenkins.io/doc/book/pipeline/#declarative-versus-scripted-pipeline-syntax
def CSD_DEPLOY_BRANCH = 'development'
// Do not add the `def` for these fields
XXPROJECT_ID = 974
GITLAB_SERVER_URL = 'http://gitlab.com'// Or your server
pipeline {
// 默認代理用主機,意味着用Jenkins主機來運行一下塊
agent any
options {
// 配置當前branch不支持同時構建,爲了不資源競爭,當一個新的commit到來,會進入排隊若是以前的構建還在進行
disableConcurrentBuilds()
// 連接到Gitlab的服務器,用於訪問Gitlab一些API
gitLabConnection('Jenkins_CI_CD')
}
environment {
// 配置緩存路徑在主機
GRADLE_CACHE = '/tmp/gradle-user-cache'
}
stages {
// 初始化階段
stage('Setup') {
steps {
// 將初始化階段修改到此次commit即Gitlab會展現對應的UI
gitlabCommitStatus(name: 'Setup') {
// 經過SLACK工具推送一個通知
notifySlack('STARTED')
echo "Setup Stage Starting. Depending on the Docker cache this may take a few " +
"seconds to a couple of minutes."
echo "${env.BRANCH_NAME} is the branch. Subsequent steps may not run on branches that are not ${CSD_DEPLOY_BRANCH}."
script {
cacheFileExist = sh(script: "[ -d ${GRADLE_CACHE} ] && echo 'true' || echo 'false' ", returnStdout: true).trim()
echo 'Current cacheFile is exist : ' + cacheFileExist
// Make dir if not exist
if (cacheFileExist == 'false') sh "mkdir ${GRADLE_CACHE}/ || true"
}
}
}
}
// 構建階段
stage('Build') {
agent {
dockerfile {
// 構建的時候指定一個DockerFile,該DockerFile有Android的構建環境
filename 'Dockerfile'
// https://github.com/gradle/gradle/issues/851
args '-v $GRADLE_CACHE/.gradle:$HOME/.gradle --net=host'
}
}
steps {
gitlabCommitStatus(name: 'Build') {
script {
echo "Build Stage Starting"
echo "Building all types (debug, release, etc.) with lint checking"
getGitAuthor()
if (env.BRANCH_NAME == CSD_DEPLOY_BRANCH) {
// TODO : Do some checks on your style
// https://docs.gradle.org/current/userguide/gradle_daemon.html
sh 'chmod +x gradlew'
// Try with the all build types.
sh "./gradlew build"
} else {
// https://docs.gradle.org/current/userguide/gradle_daemon.html
sh 'chmod +x gradlew'
// Try with the production build type.
sh "./gradlew compileReleaseJavaWithJavac"
}
}
}
/* Comment out the inner cache rsync logic
gitlabCommitStatus(name: 'Sync Gradle Cache') {
script {
if (env.BRANCH_NAME != CSD_DEPLOY_BRANCH) {
// TODO : The max cache file should be added.
echo 'Write updates to the Gradle cache back to the host'
// Write updates to the Gradle cache back to the host
// -W, --whole-file:
// With this option rsync's delta-transfer algorithm is not used and the whole file is sent as-is instead.
// The transfer may be faster if this option is used when the bandwidth between the source and
// destination machines is higher than the bandwidth to disk (especially when the lqdiskrq is actually a networked filesystem).
// This is the default when both the source and destination are specified as local paths.
sh "rsync -auW ${HOME}/.gradle/caches ${HOME}/.gradle/wrapper ${GRADLE_CACHE}/ || true"
} else {
echo 'Not on the Deploy branch , Skip write updates to the Gradle cache back to the host'
}
}
}*/
script {
// Only the development branch can be triggered
if (env.BRANCH_NAME == CSD_DEPLOY_BRANCH) {
gitlabCommitStatus(name: 'Signature') {
// signing the apks with the platform key
signAndroidApks(
keyStoreId: "platform",
keyAlias: "platform",
apksToSign: "**/*.apk",
archiveSignedApks: false,
skipZipalign: true
)
}
gitlabCommitStatus(name: 'Deploy') {
script {
echo "Debug finding apks"
// debug statement to show the signed apk's
sh 'find . -name "*.apk"'
// TODO : Deploy your apk to other place
//Specific deployment to Production environment
//echo "Deploying to Production environment"
//sh './gradlew app:publish -DbuildType=proCN'
}
}
} else {
echo 'Current branch of the build not on the development branch, Skip the next steps!'
}
}
}
// This post working on the docker. not on the jenkins of local
post {
// The workspace should be cleaned if the build is failure.
failure {
// notFailBuild : if clean failed that not tell Jenkins failed.
cleanWs notFailBuild: true
}
// The APKs should be deleted when the server is successfully built.
success {
script {
// Only the development branch can be deleted these APKs.
if (env.BRANCH_NAME == CSD_DEPLOY_BRANCH) {
cleanWs notFailBuild: true, patterns: [[pattern: '**/*.apk', type: 'INCLUDE']]
}
}
}
}
}
}
post {
always { deleteDir() }
failure {
addCommentToGitLabMR("\\:negative_squared_cross_mark\\: Jenkins Build \\`FAILURE\\` <br /><br /> Results available at:[[#${env.BUILD_NUMBER} ${env.JOB_NAME}](${env.BUILD_URL})]")
notifySlack('FAILED')
}
success {
addCommentToGitLabMR("\\:white_check_mark\\: Jenkins Build \\`SUCCESS\\` <br /><br /> Results available at:[[#${env.BUILD_NUMBER} ${env.JOB_NAME}](${env.BUILD_URL})]")
notifySlack('SUCCESS')
}
unstable { notifySlack('UNSTABLE') }
changed { notifySlack('CHANGED') }
}
}
def addCommentToGitLabMR(String commentContent) {
branchHasMRID = sh(script: "curl --header \"PRIVATE-TOKEN: ${env.gitTagPush}\" ${GITLAB_SERVER_URL}/api/v4/projects/${XXPROJECT_ID}/merge_requests?source_branch=${env.BRANCH_NAME} | grep -o 'iid\":[^,]*' | head -n 1 | cut -b 6-", returnStdout: true).trim()
echo 'Current Branch has MR id : ' + branchHasMRID
if (branchHasMRID == '') {
echo "The id of MR doesn't exist on the gitlab. skip the comment on MR"
} else {
// TODO : Should be handled on first time.
TheMRState = sh(script: "curl --header \"PRIVATE-TOKEN: ${env.gitTagPush}\" ${GITLAB_SERVER_URL}/api/v4/projects/${XXPROJECT_ID}/merge_requests?source_branch=${env.BRANCH_NAME} | grep -o 'state\":[^,]*' | head -n 1 | cut -b 9-14", returnStdout: true).trim()
echo 'Current MR state is : ' + TheMRState
if (TheMRState == 'opened') {
sh "curl -d \"id=${XXPROJECT_ID}&merge_request_iid=${branchHasMRID}&body=${commentContent}\" --header \"PRIVATE-TOKEN: ${env.gitTagPush}\" ${GITLAB_SERVER_URL}/api/v4//projects/${XXPROJECT_ID}/merge_requests/${branchHasMRID}/notes"
} else {
echo 'The MR not is opened, skip the comment on MR'
}
}
}
def pushTag(String gitTagName, String gitTagContent) {
sh "curl -d \"id=${XXPROJECT_ID}&tag_name=${gitTagName}&ref=development&release_description=${gitTagContent}\" --header \"PRIVATE-TOKEN: ${env.gitTagPush}\" ${GITLAB_SERVER_URL}/api/v4/projects/${XXPROJECT_ID}/repository/tags"
}
//Helper methods
//TODO Probably can extract this into a JenkinsFile shared library
def getGitAuthor() {
def commitSHA = sh(returnStdout: true, script: 'git rev-parse HEAD')
author = sh(returnStdout: true, script: "git --no-pager show -s --format='%an' ${commitSHA}").trim()
echo "Commit author: " + author
}
def notifySlack(String buildStatus = 'STARTED') {
// Build status of null means success.
buildStatus = buildStatus ?: 'SUCCESS'
def color
if (buildStatus == 'STARTED') {
color = '#D4DADF'
} else if (buildStatus == 'SUCCESS') {
color = 'good'
} else if (buildStatus == 'UNSTABLE' || buildStatus == 'CHANGED') {
color = 'warning'
} else {
color = 'danger'
}
def msg = "${buildStatus}: `${env.JOB_NAME}` #${env.BUILD_NUMBER}:\n${env.BUILD_URL}"
slackSend(color: color, message: msg)
}
複製代碼
DockerFile支持Android構建環境(包含JNI,API:26.0.3+)及JenkinsFile開源在Github: JenkinsWithDockerInAndroid