1987WEB视界-分享互联网热点话题和事件

您现在的位置是:首页 > WEB开发 > 正文

WEB开发

当Android遇到Jenkins,太完美了

1987web2023-10-06WEB开发58
热文推荐:

热文推荐:

作者:Tanck

博客:https://juejin.im/post/5dc4ea51e51d452d583140b9

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 MRorIM/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 {agentanystages {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 MRifthe 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()echoCurrent Branch has MR id :+ branchHasMRIDif(branchHasMRID ==){echo"The id of MR doesnt 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()echoCurrent MR state is :+ TheMRStateif(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{echoThe MR not is opened, skip the comment on MR}}}

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

defpushTag(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{filenameDockerfile// https://github.com/gradle/gradle/issues/851args-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 anyoptions {// 配置当前branch不支持同时构建,为了避免资源竞争,当一个新的commit到来,会进入排队如果之前的构建还在进行disableConcurrentBuilds()// 链接到Gitlab的服务器,用于访问Gitlab一些APIgitLabConnection(Jenkins_CI_CD)}environment {// 配置缓存路径在主机GRADLE_CACHE =/tmp/gradle-user-cache}stages {// 初始化阶段stage(Setup) {steps {// 将初始化阶段修改到这次commit即Gitlab会展示对应的UIgitlabCommitStatus(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()echoCurrent cacheFile is exist :+ cacheFileExist// Make dir if not existif(cacheFileExist ==false) sh"mkdir${GRADLE_CACHE}/ || true"}}}}// 构建阶段stage(Build) {agent {dockerfile {// 构建的时候指定一个DockerFile,该DockerFile有Android的构建环境filenameDockerfile// https://github.com/gradle/gradle/issues/851args-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.htmlshchmod +x gradlew// Try with the all build types.sh"./gradlew build"}else{// https://docs.gradle.org/current/userguide/gradle_daemon.htmlshchmod +x gradlew// Try with the production build type.sh"./gradlew compileReleaseJavaWithJavac"}}}/* Comment out the inner cache rsync logicgitlabCommitStatus(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 rsyncs 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 triggeredif(env.BRANCH_NAME == CSD_DEPLOY_BRANCH) {gitlabCommitStatus(name:Signature) {// signing the apks with the platform keysignAndroidApks(keyStoreId:"platform",keyAlias:"platform",apksToSign:"**/*.apk",archiveSignedApks:false,skipZipalign:true)}gitlabCommitStatus(name:Deploy) {script {echo"Debug finding apks"// debug statement to show the signed apksshfind . -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{echoCurrent branch of the build not on the development branch, Skip the next steps!}}}// This post working on the docker. not on the jenkins of localpost {// 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\\` 

Results available at:[[${env.BUILD_NUMBER}${env.JOB_NAME}](${env.BUILD_URL})]"
)
notifySlack(FAILED)}success {addCommentToGitLabMR("\\:white_check_mark\\: Jenkins Build \\`SUCCESS\\`

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()echoCurrent Branch has MR id :+ branchHasMRIDif(branchHasMRID ==) {echo"The id of MR doesnt 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()echoCurrent MR state is :+ TheMRStateif(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{echoThe 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 ?:SUCCESSdef colorif(buildStatus ==STARTED) {color =D4DADF}elseif(buildStatus ==SUCCESS) {color =good}elseif(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

正如我在第一篇博客以及 JEP-300 中所讨论的 Jenkins Evergreen 的前两大支柱是我们关注的要点.

自动更新的发行版

不出所料, 实现安全、自动地更新Jenkins发行版(包括核心和插件)所需的机制需要很多的工作。在 Baptiste 的演讲中他将讨论如何使 Evergreen 走起来,而我会讨论为何自动更新的发行版很重要。

持续集成和持续交付变得越来越普遍,并且是现代软件工程的基础 ,在不同的组织当中有两种不同的方式使用 Jenkins 。在一些组织当中,Jenkins 通过 Chef ,Puppet 等自动化工具有条不紊的被管理和部署着。然而在许多其他组织当中, Jenkins 更像是一个设备,与办公室的无线路由器不同。当安装完毕,只要它能继续完成工作,人们就不会太多的关注这个设备。

Jenkins Evergreen 发行版通过确保最新的功能更新,bug 修复以及安全性修复始终能安装到 Jenkins 当中,让 Jenkins 更像是一个设备。

除此之外, 我相信 Evergreen 能够向一些我们现在没有完全服务的团队提供良好的服务:这些团体希望能够以服务的形式使用 Jenkins 。我们暂时没有考虑提供公有云版本的 Jenkins 。我们意识到了自动接收增量更新,使用户可以在无需考虑更新 Jenkins 的情况下进行持续开发的好处。

我相信 Jenkins Evergreen 可以并且可以提供相同的体验。

自动配置默认值

Jenkins 平台真正强大的地方是可以为不同的组织提供不同的模式和做法。对于很多新用户来说,或一些只希望使用通用案例的用户来说, Jenkins 的灵活性与让用户做出合适的选择形成了悖论。使用 Jenkins Evergreen,很多常用的配置将自动配置,使 Jenkins 变成开箱即用的工具。

默认情况下将包括 Jenkins 流水线和 Jenkins Blue Ocean,我们也删除了一些 Jenkins 的遗留功能。

我们同样在使用非常棒的 Configuration as Code 进行工作, Configuration as Code 现在已经完成了1.0版本的发布, 我们通过它实现自动进行默认配置。

现状

迄今为止,这个项目取得了重大的进展,我们非常高兴有用户开始尝试 Jenkins Evergreen,现在 Jenkins Evergreen 已经可以被早期使用者尝试. 不过我们现在推荐在生产环境中使用 Jenkins Evergreen 。

自动更新、易于使用的Jenkins

本文转载自:Jenkins中文社区https://jenkins-zh.cn