Android Jenkins CI集成

Android Jenkins CI集成

Jenkins是一个独立的基于Java的程序,随时可以运行,包含Windows,Mac OS X和其他类Unix操作系统的软件包。作为可扩展的自动化服务器,Jenkins可以用作简单的CI服务器,也可以用作任何项目的持续交付中心。

环境搭建

Jenins启动及初始化

  1. 启动java -jar jenkins.war --httpPort=8888

  2. 浏览器中通过http://127.0.0.1:8888访问Jenkins

  3. 使用cat命令查看密码,并使用admin账号进行登录

cat /home/dev/.jenkins/secrets/initialAdminPassword

系统在初始化时,会下载一些必要的插件,等待下载完成即可使用。

插件管理及系统设置

Jeninks提供了大量的插件,供开发人员使用。如果未找到合适的,可自己开发相应的开源插件。

Android CI需要如下插件(根据个人及公司需要自行选择):

  • Email Extension Plugin(用于构建后邮件通知)

  • Git Parameter Plug-In(提供参数化构建)

  • Gradle Plugin(用户打包编译)

  • Locale plugin(用于汉化Jenkins)

  • Dingding[钉钉] Plugin(用于构建后使用webhook通知)

新建任务及配置

主界面

主界面

选择【系统管理】-【系统设置】

系统设置】

进入系统设置,配置全局变量ANDROID_HOME、汉化、Jenkins Location信息填写

ANDROID_HOME、汉化、Jenkins Location信息填写

配置Git账号信息

配置Git账号信息

配置邮件通知

配置邮件通知
配置邮件通知

配置完成系统环境后,返回【系统管理】选择【全局工具配置】,指定JDK、Gradle及Git的环境信息

全局工具配置
全局工具配置

【系统设置】及【全局工具配置】完成后,就可以创建任务进行配置我们的打包项目了

创建任务

通用配置,选择参数化配置,参数化配置可选择10多种参数类型,但Git Parameter参数需要使用Git Parameter Plugins支持

主界面
主界面

【源码管理】这里指定项目的git地址以及用户名和密码,并指定需要拉取源码的分支路径,这里使用了master主分支

主界面

根据情况而定,如果使用了SSH登录则选择SSH username with private key(具体SSH配置自行百度)

主界面

【构建触发器】构建触发器可配置何时来触发Jenkins自动执行任务,有脚本,定时任务等多种配置

【构建环境】貌似没看到太大做用在此处

【构建】构建需要指定Gradle的版本如果在【全局工具配置】中加入后该处会显示,Tasks即在使用Gradle编译时的命令:


clean assemble${PRODUCT_FLAVORS}${BUILD_TYPE} --stacktrace --debug

clean:作用是在编译前清除原始build产物

assemble${PRODUCT_FLAVORS}${BUILD_TYPE}:会拼接成为一个buildTask,例如:assembleWandoujiaDebug,该任务可参考AS右侧Gradle-:app-build中的任务列表

构建

【构建后操作】构建后可选择使用邮件通知或webhook通知,这里使用了邮件通知+钉钉的Webhook机器人通知

构建后操作
构建后操作

注:钉钉的通知插件有2个目前【Dingding[钉钉]Plugin】【Dingding JSON Pusher Plugin】,后者是支持Json数据格式发送的即钉钉支持的所有消息格式,但是亲测不生效。也不知道是自己配置错误的问题。第一个则支持的其实是link消息格式。但是Link消息格式不知道如何指定@人,所以这里使用了另一种解决方法,使用[shell curl]命令进行网络请求测试发送(详见上图【构建-执行shell命令】及钉钉开放平台文档)。

配置完成后,返回主界面,进入创建的工程,选择左面的Build with Parameters进行参数化构建

参数化构建

构建完成后的邮件通知:

参数化构建

构建完成后的钉钉通知:

参数化构建

Jenkins内建环境变量

以下是Jenkins的一些内部环境变量,在你的Jenkins项目中可以找到例如:

`

http://192.168.10.123:8888/env-vars.html

`


The following variables are available to shell scripts

BRANCH_NAME

For a multibranch project, this will be set to the name of the branch being built, for example in case you wish to deploy to production from master but not from feature branches; if corresponding to some kind of change request, the name is generally arbitrary (refer to CHANGE_ID and CHANGE_TARGET).

CHANGE_ID

For a multibranch project corresponding to some kind of change request, this will be set to the change ID, such as a pull request number, if supported; else unset.

CHANGE_URL

For a multibranch project corresponding to some kind of change request, this will be set to the change URL, if supported; else unset.

CHANGE_TITLE

For a multibranch project corresponding to some kind of change request, this will be set to the title of the change, if supported; else unset.

CHANGE_AUTHOR

For a multibranch project corresponding to some kind of change request, this will be set to the username of the author of the proposed change, if supported; else unset.

CHANGE_AUTHOR_DISPLAY_NAME

For a multibranch project corresponding to some kind of change request, this will be set to the human name of the author, if supported; else unset.

CHANGE_AUTHOR_EMAIL

For a multibranch project corresponding to some kind of change request, this will be set to the email address of the author, if supported; else unset.

CHANGE_TARGET

For a multibranch project corresponding to some kind of change request, this will be set to the target or base branch to which the change could be merged, if supported; else unset.

BUILD_NUMBER

The current build number, such as "153"

BUILD_ID

The current build ID, identical to BUILD_NUMBER for builds created in 1.597+, but a YYYY-MM-DD_hh-mm-ss timestamp for older builds

BUILD_DISPLAY_NAME

The display name of the current build, which is something like "#153" by default.

JOB_NAME

Name of the project of this build, such as "foo" or "foo/bar".

JOB_BASE_NAME

Short Name of the project of this build stripping off folder paths, such as "foo" for "bar/foo".

BUILD_TAG

String of "jenkins-${JOB_NAME}-${BUILD_NUMBER}". All forward slashes ("/") in the JOB_NAME are replaced with dashes ("-"). Convenient to put into a resource file, a jar file, etc for easier identification.

EXECUTOR_NUMBER

The unique number that identifies the current executor (among executors of the same machine) that’s carrying out this build. This is the number you see in the "build executor status", except that the number starts from 0, not 1.

NODE_NAME

Name of the agent if the build is on an agent, or "master" if run on master

NODE_LABELS

Whitespace-separated list of labels that the node is assigned.

WORKSPACE

The absolute path of the directory assigned to the build as a workspace.

JENKINS_HOME

The absolute path of the directory assigned on the master node for Jenkins to store data.

JENKINS_URL

Full URL of Jenkins, like http://server:port/jenkins/ (note: only available if Jenkins URL set in system configuration)

BUILD_URL

Full URL of this build, like http://server:port/jenkins/job/foo/15/ (Jenkins URL must be set)

JOB_URL

Full URL of this job, like http://server:port/jenkins/job/foo/ (Jenkins URL must be set)

GIT_COMMIT

The commit hash being checked out.

GIT_PREVIOUS_COMMIT

The hash of the commit last built on this branch, if any.

GIT_PREVIOUS_SUCCESSFUL_COMMIT

The hash of the commit last successfully built on this branch, if any.

GIT_BRANCH

The remote branch name, if any.

GIT_LOCAL_BRANCH

The local branch name being checked out, if applicable.

GIT_URL

The remote URL. If there are multiple, will be GIT_URL_1, GIT_URL_2, etc.

GIT_COMMITTER_NAME

The configured Git committer name, if any.

GIT_AUTHOR_NAME

The configured Git author name, if any.

GIT_COMMITTER_EMAIL

The configured Git committer email, if any.

GIT_AUTHOR_EMAIL

The configured Git author email, if any.

SVN_REVISION

Subversion revision number that's currently checked out to the workspace, such as "12345"

SVN_URL

Subversion URL that's currently checked out to the workspace.

其他配置

  • 邮箱配置

不使用模板:

例如邮件配置可以指定为以下形式,


邮件由Jenkins自动发出(请勿回复!)<br />

项目名称:$PROJECT_NAME <br />

构建编号:# $BUILD_NUMBER <br />

构建状态:$BUILD_STATUS <br/>

触发原因:$CAUSE <br/>

日志地址:$BUILD_URL console <br/>

构建地址:<a href="$BUILD_URL">$BUILD_URL</a> <br/>

Apk地址:"http://192.168.10.123:8080/edu-release-v1.0.0.apk"

开发人员:<br/>

<a href="mailto:zhangsandev@gmail.com">张三</a> $nbsp;$nbsp;

<a href="mailto:lisidev@gmail.com">李四</a>

使用模板:

Jenkins默认自带groovy-html.template可以在邮件配置中使用

${SCRIPT,template="groovy-html.template"}进行配置,如果需要自己指定样式及模板,可将模板放置在.jenkins/email-template文件夹下(没有该文件夹手动创建)邮件配置中引用该模板即可。

groovy-html.template模板内容如下(我替换了一些英文为汉字):


<STYLE>

  BODY, TABLE, TD, TH, P {

    font-family: Calibri, Verdana, Helvetica, sans serif;

    font-size: 12px;

    color: black;

  }

  .console {

    font-family: Courier New;

  }

  .filesChanged {

    width: 10%;

    padding-left: 10px;

  }

  .section {

    width: 100%;

    border: thin black dotted;

  }

  .td-title-main {

    color: white;

    font-size: 200%;

    padding-left: 5px;

    font-weight: bold;

  }

  .td-title {

    color: white;

    font-size: 120%;

    font-weight: bold;

    padding-left: 5px;

    text-transform: uppercase;

  }

  .td-title-tests {

    font-weight: bold;

    font-size: 120%;

  }

  .td-header-maven-module {

    font-weight: bold;

    font-size: 120%;   

  }

  .td-maven-artifact {

    padding-left: 5px;

  }

  .tr-title {

    background-color: <%= (build.result == null || build.result.toString() == 'SUCCESS') ? '#27AE60' : build.result.toString() == 'FAILURE' ? '#E74C3C' : '#f4e242' %>;

  }

  .test {

    padding-left: 20px;

  }

  .test-fixed {

    color: #27AE60;

  }

  .test-failed {

    color: #E74C3C;

  }

</STYLE>

<BODY>

  <!-- BUILD RESULT -->

  <table class="section">

    <tr class="tr-title">

      <td class="td-title-main" colspan=2>

        BUILD ${build.result ?: 'COMPLETED'}

      </td>

    </tr>

    <tr>

      <td>构建路径:</td>

      <td><A href="${rooturl}${build.url}">${rooturl}${build.url}</A></td>

    </tr>

    <tr>

      <td>项目名称:</td>

      <td>${project.name}</td>

    </tr>

    <tr>

      <td>构建日期:</td>

      <td>${it.timestampString}</td>

    </tr>

    <tr>

      <td>构建用时:</td>

      <td>${build.durationString}</td>

    </tr>

    <tr>

      <td>触发原因:</td>

      <td><% build.causes.each() { cause -> %> ${cause.shortDescription} <%  } %></td>

    </tr>

    <tr>

    <td>开发人员</td>

    <td>

        <a href="mailto:wangxiaoliang@danyuantech.com">王晓亮</a> &nbsp;&nbsp:

            <a href="mailto:gaopengfei@danyuantech.com">高鹏飞</a>

    </td>

    </tr>

    <tr>

        <td>Apk下载:</td>

        <td><a href="http://192.168.10.123:8080/download/android/edu">下载</a></td>

    </tr>

  </table>

  <br/>

  <!-- CHANGE SET -->

  <%

  def changeSets = build.changeSets

  if(changeSets != null) {

    def hadChanges = false %>

  <table class="section">

    <tr class="tr-title">

      <td class="td-title" colspan="2">变更记录</td>

    </tr>

    <% changeSets.each() {

      cs_list -> cs_list.each() {

        cs -> hadChanges = true %>

    <tr>

      <td>

        修订版本

        <%= cs.metaClass.hasProperty('commitId') ? cs.commitId : cs.metaClass.hasProperty('revision') ? cs.revision : cs.metaClass.hasProperty('changeNumber') ? cs.changeNumber : "" %>

        by <B><%= cs.author %></B>

      </td>

      <td>${cs.msgAnnotated}</td>

    </tr>

        <% cs.affectedFiles.each() {

          p -> %>

    <tr>

      <td class="filesChanged">${p.editType.name}</td>

      <td>${p.path}</td>

    </tr>

        <% }

      }

    }

    if ( !hadChanges ) { %>

    <tr>

      <td colspan="2">No Changes</td>

    </tr>

    <% } %>

  </table>

  <br/>

  <% } %>

<!-- ARTIFACTS -->

  <%

  def artifacts = build.artifacts

  if ( artifacts != null && artifacts.size() > 0 ) { %>

  <table class="section">

    <tr class="tr-title">

      <td class="td-title">BUILD ARTIFACTS</td>

    </tr>

    <% artifacts.each() {

      f -> %>

      <tr>

        <td>

          <a href="${rooturl}${build.url}artifact/${f}">${f}</a>

      </td>

    </tr>

    <% } %>

  </table>

  <br/>

  <% } %>

<!-- MAVEN ARTIFACTS -->

  <%

  try {

    def mbuilds = build.moduleBuilds

    if ( mbuilds != null ) { %>

  <table class="section">

    <tr class="tr-title">

      <td class="td-title">BUILD ARTIFACTS</td>

    </tr>

      <%

      try {

        mbuilds.each() {

          m -> %>

    <tr>

      <td class="td-header-maven-module">${m.key.displayName}</td>

    </tr>

          <%

          m.value.each() {

            mvnbld -> def artifactz = mvnbld.artifacts

            if ( artifactz != null && artifactz.size() > 0) { %>

    <tr>

      <td class="td-maven-artifact">

              <% artifactz.each() {

                f -> %>

        <a href="${rooturl}${mvnbld.url}artifact/${f}">${f}</a><br/>

              <% } %>

      </td>

    </tr>

            <% }

          }

        }

      } catch(e) {

        // we don't do anything

      } %>

  </table>

  <br/>

    <% }

  } catch(e) {

    // we don't do anything

  } %>

<!-- JUnit TEMPLATE -->

  <%

  def junitResultList = it.JUnitTestResult

  try {

    def cucumberTestResultAction = it.getAction("org.jenkinsci.plugins.cucumber.jsontestsupport.CucumberTestResultAction")

    junitResultList.add( cucumberTestResultAction.getResult() )

  } catch(e) {

    //cucumberTestResultAction not exist in this build

  }

  if ( junitResultList.size() > 0 ) { %>

  <table class="section">

    <tr class="tr-title">

      <td class="td-title" colspan="5">${junitResultList.first().displayName}</td>

    </tr>

    <tr>

        <td class="td-title-tests">Name</td>

        <td class="td-title-tests">Failed</td>

        <td class="td-title-tests">Passed</td>

        <td class="td-title-tests">Skipped</td>

        <td class="td-title-tests">Total</td>

      </tr>

    <% junitResultList.each {

      junitResult -> junitResult.getChildren().each {

        packageResult -> %>

    <tr>

      <td>${packageResult.getName()}</td>

      <td>${packageResult.getFailCount()}</td>

      <td>${packageResult.getPassCount()}</td>

      <td>${packageResult.getSkipCount()}</td>

      <td>${packageResult.getPassCount() + packageResult.getFailCount() + packageResult.getSkipCount()}</td>

    </tr>

    <% packageResult.getPassedTests().findAll({it.getStatus().toString() == "FIXED";}).each{

        test -> %>

            <tr>

              <td class="test test-fixed" colspan="5">

                ${test.getFullName()} ${test.getStatus()}

              </td>

            </tr>

        <% } %>

        <% packageResult.getFailedTests().sort({a,b -> a.getAge() <=> b.getAge()}).each{

          failed_test -> %>

    <tr>

      <td class="test test-failed" colspan="5">

        ${failed_test.getFullName()} (Age: ${failed_test.getAge()})

      </td>

    </tr>

        <% }

      }

    } %>

  </table>

  <br/>

  <% } %>

<!-- CONSOLE OUTPUT -->

  <%

  if ( build.result == hudson.model.Result.FAILURE ) { %>

  <table class="section" cellpadding="0" cellspacing="0">

    <tr class="tr-title">

      <td class="td-title">控制台输出</td>

    </tr>

    <% build.getLog(100).each() {

      line -> %>

  <tr>

      <td class="console">${org.apache.commons.lang.StringEscapeUtils.escapeHtml(line)}</td>

    </tr>

    <% } %>

  </table>

  <br/>

  <% } %>

</BODY>

  • 构建后配置-归档成品:该配置只适用于将成品生成到workspaces下,其他路径不生效,归档成品后会在工程页面展示成品信息

  • 构建后发布到蒲公英:文档


参考文章:

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容