使用Travis-CI构建Android项目并自动打包部署到GitHub-Release

CI

CI (Continuous Integration,持续集成),CI 是 XP(Extreme Programming,极限编程)的实践之一。近年来
人们普遍使用 Jenkins 等软件来实现这一目的。

让 CI 软件监视仓库,可以在开发者发送提交后立刻执行自动测试构建。通过持续执行这样一个操作,可以检测出开发者意外发送的提交或无意的逻辑偏差,让代码保持在一定质量以上。

在现代的软件开发中持续集成已经不可或缺,甚至逐渐成为开发中的常识。在开源世界中也是同样。

常见的CI有: Travis CI 、 Jenkins 、Circle CI

基于Travis CI的Android自动发布流程

Travis CI对Github上的开源Repo是免费的,私有Repo收费。

可以将表示Travis CI的执行结果的图片嵌入到markdown中

,这个图片在很多GitHub上的项目中都有见到。

Travis CI使用YAML文件作为构建脚本。

这里我们利用Travis CI来自动根据我们打的Git Tag来构建APK并Push到自己的Github Release上(也可设置分发到fir.im)。

使GitHub帐号登录Travis CI;点击右上将脚的头像,点击"Sync account"按钮,然后打开对自己项目的Hook开关;最后在对应的项目中添加.travis.yml文件来配置 Travis CI即可。

使用Travis CI时,刚开始一直无法发现仓库里面的其它分支;后来再仓库页面 setting -> Integrations & services -> ... -> Test serviece ;对其进行测试后,Travis CI才有反映。

Android项目对应的 .travis.yml 文件

下面是一个简单的Android项目下的 .travis.yml 文件

# .travis.yml 文件

# 声明构建语言环境
language: android
android:        # 配置信息
  components:
    # 你可能需要修改下面两项的版本
    - build-tools-25.0.0
    - android-25
      # Android Support Repository
    - extra-android-m2repository
      # Support Library
    - extra-android-support
script:
  # 生成 APK
  - ./gradlew assembleRelease

# 部署
deploy:
  # 部署到GitHub Release。
  # 除此之外,Travis CI还支持发布到fir.im、AWS、Google App Engine等
  provider: releases
  # Github oauth token
  api_key: "GITHUB OAUTH TOKEN"
  # 部署文件路径;对于Android就部署生成的 apk 文件
  file: "FILE TO UPLOAD"
  # 避免 Travis CI在部署之前清空生成的APK文件
  skip_cleanup: true
  # 发布时机
  on:
    # tags设置为true表示只有在有tag的情况下才部署
    tags: true

先来测试上面的文件。 创建一个简单的Android工程进行测试;在工程的根目录添加 上述 .travis.yml 文件。

根据实际情况修改文件中相关内容,这里修改file: app-release-unsigned.apk;还有api_key就参考下文 "方法一:将GitHub的token设置为环境变量"

然后测试 Travis CI服务:

GitHub对应仓库页面 setting -> Integrations & services -> ... -> Test serviece 

这里生成的 app-release-unsigned.apk 并没有进行过签名。

一份示例文件: android_Travis_CI_Test/travis.yml.1.bak
Travis CI中有这样一句提示: Please note that deploying GitHub Releases works only for tags, not for branches.
在这里可以检测你的.travis.yml有没有基本的语法错误: Validate your .travis.yml file

遇到的问题

错误1:

Error: Could not find or load main class org.gradle.wrapper.GradleWrapperMain

原因:推送到GitHub中的Android项目中没有: gradle/wrapper/gradle-wrapper.jar 文件(被.gitignore_global忽略了)。

错误2:

FAILURE: Build failed with an exception.
* What went wrong:
A problem occurred configuring project ':app'.
> You have not accepted the license agreements of the following SDK components:
  [ConstraintLayout for Android 1.0.2, Solver for ConstraintLayout 1.0.2].
  Before building your project, you need to accept the license agreements and complete the installation of the missing components using the Android Studio SDK Manager.

如果使用了ConstraintLayout则会出现错误2。
上面的意思是:您尚未接受以下SDK组件的许可协议:[ConstraintLayout for Android 1.0.2,Solver for ConstraintLayout 1.0.2]。在构建项目之前,您需要接受许可协议,并使用Android Studio SDK Manager完成缺少组件的安装。

解决办法在文件中添加如下内容:

before_install:
  - mkdir "$ANDROID_HOME/licenses" || true
  - echo -e "\n8933bad161af4178b1185d1a37fbf41ea5269c55" > "$ANDROID_HOME/licenses/android-sdk-license"
  - echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"

Accept ConstraintLayout licenses · Issue #6617 · travis-ci/travis-ci
android - Travis CI failed because cannot accept license Constrain Layout - Stack Overflow

错误3:

Dpl does not have permission to upload assets. Make sure your token contains the repo or public_repo scope.
dpl.2
Preparing deploy
failed to deploy

之前误以为 public access就是public_repo;回到 GitHub页面修改token的访问权限,添加 public_repo scope 然后更新。

错误4:

/home/travis/.rvm/gems/ruby-2.2.7/gems/octokit-4.6.2/lib/octokit/client/releases.rb:86:in `initialize': No such file or directory @ rb_sysopen - app/build/outputs/apk/app-release.apk (Errno::ENOENT)

/home/travis/.rvm/gems/ruby-2.2.7/gems/octokit-4.6.2/lib/octokit/client/releases.rb:86:in `initialize': No such file or directory @ rb_sysopen - app/build/outputs/apk/app-debug.apk (Errno::ENOENT)

在部署前使用ls命令查看app/build/outputs/apk/路径下的文件。添加如下语句:

before_deploy:
  - ls app/build/outputs/apk/

Log输出:

$ ls app/build/outputs/apk/
app-release-unsigned.apk

解决方法:

为构建添加缓存

构建速度好慢,尝试添加缓存:

before_cache:
 - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
 - rm -fr $HOME/.gradle/caches/*/plugin-resolution/

cache:
 directories:
 - $HOME/.gradle/caches/
 - $HOME/.gradle/wrapper/

手动清楚缓存:

参考:Caching Dependencies and Directories - Travis CI

加密自动化构建中使用到的密码和token

自动化构建中需要用到密码和证书,我们必须保证的安全性。为此Travis CI为此提供了2种解决方案:

  • 一种是对敏感信息、密码、证书、token等进行对称加密,在CI构建环境时解密
  • 另一种是将密码等通过Travis CI的控制台(即网站)设置为构建时的环境变量。

获取GitHub的认证

将apk部署到GitHub Release需要向Github传送文件,就需要获得GitHub的相关认证。

获取认证的方法:

  • 可以是在配置文件.travis.yml中使用明文密码。极不安全。
  • 或使用 Github Personal Access Token 进行验证。下面介绍。

创建 Github Personal Access Token

在Github -> settings -> Personal access token 生成一个勾选了public_repo scope的Github Personal Access Token。

生成 token: Github auth token on TravisCI
Creating a personal access token for the command line - User Documentation

生成了Github Personal Access Token后我们就将其应用到 .travis.yml中。

应用Github Personal Access Token的两种方法

方法一:将GitHub的token设置为环境变量

  • 在Travis CI网站中对应的项目页面,点击"More options"然后选择上图中的 setting 。
  • 在“Environment Variables”下添加环境变量:这里我添加 "GH_TOKEN"变量名称,值就复制你之前生成的token,并确保"Display value in build log" 为"OFF"状态。

然后在.travis.yml文件中这样使用该环境变量:

api_key:
    secure: ${GH_TOKEN}

Github Personal Access Token在第一次复制后就会自动隐藏,无法再次查看;如果忘记了token就只有重新生成 token。

方法二:在本地安装travis并使用travis的相关命令对token进行加密

token可以重复使用,但是对于特定的仓库必须重新运行上面的命令进行加密并指定仓库。

  • 安装ruby。可参考:Ubuntu安装ruby到opt下的rbenv目录
  • 查询最新的Travis CI的版本: gem search travis。我这里是 travis (1.8.8)
  • 安装该版本的travis: gem install travis -v 1.8.8 --no-rdoc --no-ri
  • 查看是否安装完成: travis --version,然后会提示你“shellcompletion not installed ...” shell补全没有安装是否需要安装,输入 y。重新打开终端即可对该命令使用tab补全。
  • 如果首次运行,travis会提示需要登录,可以通过运行travis login并输入Github用户名密码即可。或者如果你不想输入密码,使用travis login --github-token <之前生成的github-token>也可进行登录。
  • 更简单一点的是直接使用travis encrypt <之前生成的token> -r <owner>/<repo>其中 -r用于指定仓库。然后有根据命令输出添加相关信息到.travis.yml文件。示例:
    deploy:
    api_key:
      secure: "XR0ZYSp+eWOhHwO0az80nEZoOOyaOIeV7brTekEdl7APUfPziQHrV4G/xPxPZB2Fcn13efyiwpPbmXpSVkIqJJZg2Ok/efBviUIwgl5pqGrddgXEskW8+lebHJA/17JjmXoo2ZT9BcmMlVrT0Dj5WCJXH2QOSAXzVy/9DO7e3pEijAImCHgh0oH/hQzlO86IMBn3Fzlb520bplEI4fnuJNU/nKZrl05dTRCso2ZB2/MXt1X5vfYm4zrwjTgZXOpfSVur6kv/EMNQFDiGLa3F22m1FDQULDfHY7evT79hYlcRKVGx7hoQRrOxXr0Ag6wcb+4buvwaxutNPjaEXkH3FfocrIGxZtox1kE19ebqjc6fT/CH85Lf+AqT2rR86Wr5aaEQcKAxNZxUJtir5MAIo0j7vkx1PUi0MCb9K6JHca9lkFsoYP1L0pjDztD8w4J15rv5TCV/JjMxhCGQkGwDH+M3Z7dQOgikoRaO11o6C5/fC8qfCeGKT3GzL9966dy0UY9ZDQbB7y3donlWgoTZHn6xahGNlP4CYiP6Ax94egDmbSmyiJud5Y0NmwXMY0IEuJPWEFkmBdGEbZuqqsL3mxqxebox7q7gz613ToZSsfbNhKw3ouETftjXoMnlfRFIl7LSAOwAtL5Wig5zlwCMafGTFaGpLH0zl02Udjy3UbU="
    
  • 或者使用命令travis encrypt GITHUB_TOKEN=<之前生成的token> -r <owner>/<repo>,加密环境变量GITHUB_TOKEN。其中 -r用于指定仓库。这里解密之后得到GITHUB_TOKEN=<之前生成的token>而并非<之前生成的token>

对于加密环境变量GITHUB_TOKEN的形式,需这样使用:

env:
  global:
    secure: "XR0ZYSp+eWOhHwO0az80nEZoOOyaOIeV7brTekEdl7APUfPziQHrV4G/xPxPZB2Fcn13efyiwpPbmXpSVkIqJJZg2Ok/efBviUIwgl5pqGrddgXEskW8+lebHJA/17JjmXoo2ZT9BcmMlVrT0Dj5WCJXH2QOSAXzVy/9DO7e3pEijAImCHgh0oH/hQzlO86IMBn3Fzlb520bplEI4fnuJNU/nKZrl05dTRCso2ZB2/MXt1X5vfYm4zrwjTgZXOpfSVur6kv/EMNQFDiGLa3F22m1FDQULDfHY7evT79hYlcRKVGx7hoQRrOxXr0Ag6wcb+4buvwaxutNPjaEXkH3FfocrIGxZtox1kE19ebqjc6fT/CH85Lf+AqT2rR86Wr5aaEQcKAxNZxUJtir5MAIo0j7vkx1PUi0MCb9K6JHca9lkFsoYP1L0pjDztD8w4J15rv5TCV/JjMxhCGQkGwDH+M3Z7dQOgikoRaO11o6C5/fC8qfCeGKT3GzL9966dy0UY9ZDQbB7y3donlWgoTZHn6xahGNlP4CYiP6Ax94egDmbSmyiJud5Y0NmwXMY0IEuJPWEFkmBdGEbZuqqsL3mxqxebox7q7gz613ToZSsfbNhKw3ouETftjXoMnlfRFIl7LSAOwAtL5Wig5zlwCMafGTFaGpLH0zl02Udjy3UbU="

deploy:
  api_key:
    secure: ${GITHUB_TOKEN}

这里可以对 secure: 字串的作用做一个猜测: Travis读取到 secure 时便会解密该字串,并将解密后得到的值替换 secure 这行内容。比如如果加密的是 变量=值,那么当其出现在上文的 global: 时,就是定义了一个全局的环境变量,然后就可在后面使用该变量。

可以使用 travis help encrypt查看命令帮助;
使用 travis repos列出所有仓库;参考:travis-ci/travis.rb: Travis CI Client (CLI and Ruby library)

Travis CI's use of GitHub API Scopes - Travis CI

加密签名apk时使用到的密码和keystore文件(方式一)

事先创建好key和keystore文件。 这里我把keystore文件的后缀名变为了.keystore,大家使用的都是 .jks

1、 加密keystore文件my.keystore: travis encrypt-file my.keystore -r FanDean/android_Travis_CI_Test。根据命令输出的提示,将加密后的keystore文件 my.keystore.enc 放入Android工程的根目录;并在 .travis.yml 文件中添加如下内容:

before_install:
  - openssl aes-256-cbc -K $encrypted_f6c9a5f058a8_key -iv $encrypted_f6c6a5f053a8_iv -in my.keystore.enc -out my.keystore -d

2、 加密keystore password 、 key password 和 keyAlias。方法和加密 GitHub Token 一样;也可使用添加环境变量的方式。

这里我们准备下面三个环境变量,并通过在travis网站添加环境变量的形式进行添加:

  • KEYSTORE_PWD
  • KEY_PWD
  • KEY_ALIAS

具体添加方式略(后面我们使用另外一种方法)。

.travis.yml 文件中添加如下内容对apk进行签名:

before_deploy:
- cp $TRAVIS_BUILD_DIR/.keystore $HOME
# 记住这里已经将当前工作目录切换到 apk 下
- cd app/build/outputs/apk/
# Signing 签名
- jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore $HOME/my.keystore -storepass $KEYSTORE_PWD -keypass $KEY_PWD app/build/outputs/apk/app-release-unsigned.apk $KEY_ALIAS

# Verification 验证您的 APK 是否已签署
- jarsigner -verify app-release-unsigned.apk
- "${ANDROID_HOME}/build-tools/24.0.2/zipalign -v 4 app-release-unsigned.apk app-release.apk"

jarsigner - JAR Signing and Verification Tool;也可以用来对apk进行签名。

看起来比较麻烦。

加密签名apk时使用到的密码和keystore文件(方式二)

参考下面的文章,我们可以使用一种新的方式:

创建并加密 keystore.properties 文件

在Android项目根目录创建 keystore.properties文件保存keyPwd等变量。

storePwd=***
keyPwd=***
keyAlias=test_key
storeFileLocal=/home/fan/.android/my_keystore/my.keystore
storeFileCI=../my.keystore

同样加密 keystore.properties 文件: travis encrypt-file keystore.properties -r FanDean/android_Travis_CI_Test
同样添加 "openssl aes-256-cbc -K $encrypted_ad23ea9b533c_key -iv $encrypted_ad23ca6e583c_iv -in keystore.properties.enc -out keystore.properties -d" 到 before_install 下面。

修改app/build.gradle 文件

然后在 app/build.gradle 文件中添加如下内容,用于保护您的密钥和判断当前是在本地还是在 CI服务器:

// Create a variable called keystorePropertiesFile, and initialize it to your
// keystore.properties file, in the rootProject folder.
def keystorePropertiesFile = rootProject.file("keystore.properties")

// Initialize a new Properties() object called keystoreProperties.
def keystoreProperties = new Properties()

// Load your keystore.properties file into the keystoreProperties object.
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))

def keyStoreFile = file(keystoreProperties['storeFileLocal'])

// 判断是位于本地还是CI服务器
if (!keyStoreFile.exists()){
    keyStoreFile = file(keystoreProperties['storeFileCI'])
}

android {
    signingConfigs {
        release {
            keyAlias keystoreProperties['keyAlias']
            keyPassword keystoreProperties['keyPwd']
            storeFile keyStoreFile
            storePassword keystoreProperties['storePwd']
        }
    }

  ...

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
            signingConfig signingConfigs.release
        }
    }

  ...

忽略 keystore.properties文件

在 .gitignore_global 文件中添加:

keystore.properties

示例文件: android_Travis_CI_Test/.travis.yml

一些配置

script: ./gradlew testRelease assembleRelease

script:
- "./gradlew clean build connectedCheck -PdisablePreDex --stacktrace"

Travis Config · kawa89/Testing Wiki

script:
  - ./travis/test.sh

./gradlew clean testDebugUnitTest
./gradlew connectedAndroidTest --info
./gradlew clean assembleDebug

GoIV/.travis.yml at master · farkam135/GoIV

script:
   - ./gradlew assembleDebug testOnlineDebugUnitTest testOfflineDebugUnitTest checkstyle

查找android .travis.yml 的相关配置,可尝试在GitHub中搜索 "android travis"然后选择查看 issues 和 wiki 。

参考:

Jenkins学习资料 Jenkins Overview

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

推荐阅读更多精彩内容