当Android遇到Jenkins

299 阅读5分钟
原文链接: mp.weixin.qq.com

码个蛋(codeegg)第 814 次推文

作者:Tanck

博客:https://juejin.cn/post/6844903990073753608

码妞看世界

1. 什么是Jenkins

Jenkins是开源CI&CD软件领导者, 提供超过1000个插件来支持构建、部署、自动化, 满足任何项目的需要。

2. 为什么需要Jenkins(DevOps)

我们日常开发一般流程: Commit -> Push -> Merge -> Build. 基本就算完成. 而Jenkins的存在就是代替这一些系列从而实现自动化,侧重在于后面几个阶段,我们可以做很多的事情. 自动化的过程是确保构建编译都是正确的,平时我们手动编译不同版本的时候难免可能会出错,有了它可以降低编译错误,提高构建速度. 然而一般我们Jenkins都是需要配合Docker来完成的,所以需要具备一定的Docker的基础与了解. 文末有Github地址,共享了DockerFile及JenkinsFile.Why Pipeline?(https://jenkins.io/doc/book/pipeline/#declarative-versus-scripted-pipeline-syntax)

3. 有Jenkins在Android能实现什么?

  • 当push一个commit到服务器,将构建结果提交到MR/PR上(MR/PR存在)

  • 当push一个commit到服务器,执行构建-->多渠道-->签名-->发布到各大市场-->通知相关人员

  • 当push一个commit到服务器,在指定的branch做一些freestyle

  • 当push一个commit到服务器,创建一个TAG

  • ....

详细如图(Gitlab CI/CD):

在MergeRequest/PullRequest中应用如下:

4. 一个DevOps基本序列

一个DevOps的工作序列基本主要区分与Jenkins Server两种工作模式,这两种工作模式分为:

  • Webhook的方式(在Gitlab/Github配置event触发后的地址,即当Gitlab/Gtihub产生事件会通过HTTP/HTTPS的方式将一个事件详细发送给Jenkins Service,随后Jenkins Service收到该消息会解析并做定义的处理);

  • 轮训方式;(即无需侵入Gitlab/Github,由Jenkins定期轮训对应仓库的代码,如果发生改变则立即出发构建.)

下面主要介绍一下以Webhook工作方式的时序图如下:

sequenceDiagramUser ->> Gitlab/Github: push a commitGitlab/Github-->>Jekins: push a message via webhookJenkins -->> Jenkins: Sync with branchs and do a build with freestyle if there are changesJenkins --x Gitlab/Github: Feedback some comments on MR or IM/EMAIL 

这将产生一个流程图:

graph LRA(User) --Push a commit --> B(Gitlab/Github)B --Push a message via webhook --> C(Jenkins)

5. 构建一个的Android应用多分支步骤

构建一个的Android应用多分支步骤

  • 配置一个Jenkins Server;(由于文章主要讲解Jenkins脚本高级应用,所以还请网上搜索相关环境搭建)

  • 在Jenkins 里面创建一个应用如下图:

  • 配置好对应的远程仓库地址后,我们需要指定Jenkins脚本路径如下:

  • 由于Jenkins配置的路径是在项目路径下,所以我们Android Studio也得配置在对应跟布局下:

  • 最后以Gitlab为例子配置Webhook如下:

所有的配置完毕后,接下来就是详解Jenkins脚本。

6. 脚本详解

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            }        }    }}

高级特性详解:

  • 想要提交comment在MR/PR上: 一般是通过调用Gitlab/Github开放的API来实现,以Gitlab为例:

/** * 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'        }    }}
  • 自动创建一个TAG且有CHANGELOG: 因为我们通过git tag创建的TAG一般是没有描述的,有时候比较难跟踪,所以我们可以调用Gitlab/Github API来创建一个TAG,效果如下:

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"}
  • 将Gradle 缓存共享给Docker,这样每次构建的时候就不会在Docker里面每次去下载依赖包:

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-syntaxdef CSD_DEPLOY_BRANCH = 'development'// Do not add the `def` for these fieldsXXPROJECT_ID = 974GITLAB_SERVER_URL = 'http://gitlab.com'// Or your serverpipeline {    // 默认代理用主机,意味着用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 librarydef 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:(https://github.com/Softtanck/JenkinsWithDockerInAndroid

相关文章:

今日问题:

大家的CI,CD有用起来吗?

专属升级社区:《这件事情,我终于想明白了》