當Jenkins遇到Android

什麼是Jenkins

Jenkins是開源CI&CD軟件領導者, 提供超過1000個插件來支持構建、部署、自動化, 滿足任何項目的需要。

爲什麼需要Jenkins(DevOps)

我們日常開發一般流程: Commit -> Push -> Merge -> Build. 基本就算完成. 而Jenkins的存在就是代替這一些系列從而實現自動化,側重在於後面幾個階段,我們可以做很多的事情. 自動化的過程是確保構建編譯都是正確的,平時我們手動編譯不同版本的時候難免可能會出錯,有了它可以降低編譯錯誤,提高構建速度. 然而一般我們Jenkins都是需要配合Docker來完成的,所以需要具備一定的Docker的基礎與瞭解.
文末有Github地址,共享了DockerFile及JenkinsFile.
Why Pipeline?

有Jenkins在Android能實現什麼:

  • 當push一個commit到服務器,將構建結果提交到MR/PR上(MR/PR存在)
  • 當push一個commit到服務器,執行構建–>多渠道–>簽名–>發佈到各大市場–>通知相關人員
  • 當push一個commit到服務器,在指定的branch做一些freestyle
  • 當push一個commit到服務器,創建一個TAG

詳細如圖(Gitlab CI/CD):
在這裏插入圖片描述
在這裏插入圖片描述
在MergeRequest/PullRequest中應用如下:
在這裏插入圖片描述

一個DevOps基本序列

一個DevOps的工作序列基本主要區分與Jenkins Server兩種工作模式,這兩種工作模式分爲:

  • Webhook的方式(在Gitlab/Github配置event觸發後的地址,即當Gitlab/Gtihub產生事件會通過HTTP/HTTPS的方式將一個事件詳細發送給Jenkins Service,隨後Jenkins Service收到該消息會解析並做定義的處理);
  • 輪訓方式;(即無需侵入Gitlab/Github,由Jenkins定期輪訓對應倉庫的代碼,如果發生改變則立即出發構建.)

下面主要介紹一下以Webhook工作方式的時序圖如下:

UserGitlab/GithubJekinsJenkinspush a commitpush a message via webhookSync with branchs and do a build with freestyle if there are changesFeedback some comments on MR or IM/EMAILUserGitlab/GithubJekinsJenkins

這將產生一個流程圖。:

Push a commit
Push a message via webhook
User
Gitlab/Github
Jenkins

構建一個的Android應用多分支步驟

  • 配置一個Jenkins Server;(由於文章主要講解Jenkins腳本高級應用,所以還請網上搜索相關環境搭建)

  • 在Jenkins 裏面創建一個應用如下圖:
    在這裏插入圖片描述

  • 配置好對應的遠程倉庫地址後,我們需要指定Jenkins腳本路徑如下:
    在這裏插入圖片描述

  • 由於Jenkins配置的路徑是在項目路徑下,所以我們Android Studio也得配置在對應跟佈局下:
    在這裏插入圖片描述

  • 最後以Gitlab爲例子配置Webhook如下:
    在這裏插入圖片描述

所有的配置完畢後,接下來就是詳解Jenkins腳本.

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章