-
Finally settled! Tsinghua genius Wang Yin joins Huawei at level 22, former Alibaba P10 Zhao Haiping joins ByteDance, level may be 4+ -
Baidu Cloud Disk “Cracked Version”, Pandownload developer arrested
Author: Tanck
Blog: https://juejin.im/post/5dc4ea51e51d452d583140b9
1. What is Jenkins
Our daily development process: Commit -> Push -> Merge -> Build. Basically, that’s it. The existence of Jenkins replaces this series to achieve automation, focusing on the latter stages where we can do many things. The automation process ensures that the build compilation is correct. When we manually compile different versions, errors are inevitable. With it, we can reduce compilation errors and improve build speed. However, Jenkins generally needs to work with Docker, so a certain understanding of Docker is required. At the end of the article, there is a GitHub address sharing the DockerFile and JenkinsFile. Why Pipeline? (https://jenkins.io/doc/book/pipeline/#declarative-versus-scripted-pipeline-syntax)
3. What can Jenkins achieve in Android?
-
When pushing a commit to the server, submit the build results to MR/PR (if MR/PR exists)
-
When pushing a commit to the server, execute build -> multi-channel -> sign -> publish to major markets -> notify relevant personnel
-
When pushing a commit to the server, do some freestyle on the specified branch
-
When pushing a commit to the server, create a TAG
-
….
Details as shown in the figure (Gitlab CI/CD):
Applied in MergeRequest/PullRequest as follows:
A basic work sequence of DevOps mainly distinguishes between two working modes of the Jenkins Server, which are:
-
Webhook method (configure the event trigger address in Gitlab/Github, that is, when Gitlab/Github generates an event, it will send a detailed event to the Jenkins Service via HTTP/HTTPS, and then the Jenkins Service will parse the message and perform defined processing); -
Polling method; (that is, no intrusion into Gitlab/Github, Jenkins periodically polls the corresponding repository code, if there are changes, it will immediately trigger the build.)
The following mainly introduces the timing diagram of the Webhook working method as follows:
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
This will generate a flow chart: graph LRA(User) --Push a commit -->> B(Gitlab/Github)B --Push a message via webhook -->> C(Jenkins) 5. Build an Android application multi-branch steps
Build an Android application multi-branch steps
-
Configure a Jenkins Server; (Since the article mainly discusses advanced applications of Jenkins scripts, please search online for related environment setup)
-
Create an application in Jenkins as shown below: -
After configuring the corresponding remote repository address, we need to specify the Jenkins script path as follows: -
Since the path configured in Jenkins is under the project path, we also need to configure Android Studio in the corresponding layout: -
Finally, configure Webhook in Gitlab as an example:
6. Script Details
Jenkins Script Details (Direct Declaration Method):
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 } } }
-
To submit a comment on MR/PR: Generally achieved by calling the open API of Gitlab/Github, taking Gitlab as an example:
/** * 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' } }}
-
Automatically create a TAG with CHANGELOG: Since the TAG we create through git tag generally has no description, which can be difficult to track, we can call the Gitlab/Github API to create a TAG, as shown below:
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"}
-
Share Gradle cache with Docker, so that each time you build, you won’t have to download dependencies in Docker every time:
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' }}
Complete 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 { // Default agent uses the host, meaning to run this block on the Jenkins host agent any options { // Configure the current branch not to support concurrent builds, to avoid resource competition, when a new commit arrives, it will enter a queue if the previous build is still in progress disableConcurrentBuilds() // Link to the Gitlab server, used to access some Gitlab APIs gitLabConnection('Jenkins_CI_CD') } environment { // Configure the cache path on the host GRADLE_CACHE = '/tmp/gradle-user-cache' } stages { // Initialization stage stage('Setup') { steps { // Modify the initialization stage to this commit, and Gitlab will display the corresponding UI gitlabCommitStatus(name: 'Setup') { // Push a notification through 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" } } } } // Build stage stage('Build') { agent { dockerfile { // Specify a DockerFile with Android build environment during the build 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 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}:
\${env.BUILD_URL}" slackSend(color: color, message: msg}
DockerFile supports Android build environment (including JNI, API: 26.0.3+) and JenkinsFile open source on GitHub:(https://github.com/Softtanck/JenkinsWithDockerInAndroid)