Jenkins是开源 CI&CD 软件领导者, 提供超过1000个插件来支持构建、部署、自动化, 满足任何项目的需要。
我们日常开发一般流程: Commit -> Push -> Merge -> Build. 基本就算完成. 而Jenkins的存在就是代替这一些系列从而实现自动化,侧重在于后面几个阶段,我们可以做很多的事情. 自动化的过程是确保构建编译都是正确的,平时我们手动编译不同版本的时候难免可能会出错,有了它可以降低编译错误,提高构建速度. 然而一般我们Jenkins都是需要配合Docker来完成的,所以需要具备一定的Docker的基础与了解. 文末有Github地址,共享了DockerFile及JenkinsFile. Why Pipeline?
详细如图(Gitlab CI/CD):
在MergeRequest/PullRequest中应用如下:
一个DevOps的工作序列基本主要区分与Jenkins Server两种工作模式,这两种工作模式分为:
下面主要介绍一下以Webhook工作方式的时序图如下:
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 复制代码
这将产生一个流程图。:
graph LR A(User) --Push a commit --> B(Gitlab/Github) B --Push a message via webhook --> C(Jenkins) 复制代码
配置一个Jenkins Server;(由于文章主要讲解 Jenkins脚本高级应用 ,所以还请网上搜索相关环境搭建)
在Jenkins 里面创建一个应用如下图:
配置好对应的远程仓库地址后,我们需要 指定Jenkins脚本路径 如下:
由于Jenkins配置的路径是在项目路径下,所以我们Android Studio也得配置在对应跟布局下:
最后以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