为 Android 项目使用 Travis CI 并发布持续集成版本

前言

Travis CI 是 GitHub 上开源项目采用持续集成的常见选择。为了给 豆芽 提供持续集成版本用于公开测试,我配置了 Travis CI,并自己编写了脚本将构建结果发布到另一个空项目的 Release 中,并将其间的过程和遇到的问题记录于此。

Travis CI 构建 Android 项目的时间较长,因此调试配置时十分耗时。希望我的经验能对他人有所帮助。

Travis CI

Travis CI 分为免费版(travis-ci.org)和付费版(travis-ci.com),两者之间没有相互的链接或说明,第一次配置时容易混淆。开源项目选择免费版即可。配置过程可以参考官方的 Getting StartedAndroid 项目配置

关于 Android 项目有一些较为微妙的配置问题,我自己调试并查阅了一些 Issue 方才解决。

  1. 为了能够找到并下载最新的 Build Tools,需要启用最新版本的 Tools(- tools)。
  2. Lint 过程中如果 Platform Tools 版本低于 SDK 版本则会报错,需要启用最新版本的 Platform Tools(-platform-tools)。
  3. 为了能够找到并下载最新的 Platform Tools,需要已有最新的 Tools,因此与官方给出的样例不同,需要将 - tools 放置在 - platform-tools 之前。

因此我的 Android 部分最终配置如下:

1
2
3
4
5
6
7
android:
components:
- tools
- platform-tools
- build-tools-24.0.1
- android-24
- extra-android-m2repository

详细配置可以参考我的 .travis.yml

启用构建缓存

Gradle 是一个为缓存优化过的工具,因此官方也提供了相应的 开启缓存的方法

1
2
3
4
5
6
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/

应用签名

为了给 CI 版本的 APK 签名,需要提供相应的 keystore 和密码。Travis CI 提供了 在设置中定义环境变量 的方式来传递低敏感级的信息。

基于以上文档和一些搜索结果,并参考过 Shadowsocks-Android 的配置方式,我将我的签名配置编写为了从 signing.properties、环境变量、终端输入三个层级进行获取的方式。

以下是我的 signing.gradle

1
2
3
4
5
6
7
8
9
10
11
12
android {
signingConfigs {
release {
Properties signingProperties = new Properties()
signingProperties.load(rootProject.file("signing.properties").newDataInputStream())
storeFile rootProject.file(signingProperties.get("storeFile"))
storePassword signingProperties.get("storePassword") ?: System.getenv("STORE_PASSWORD") ?: System.console()?.readLine("\nStore password: ")
keyAlias signingProperties.get("keyAlias")
keyPassword signingProperties.get("keyPassword") ?: System.getenv("KEY_PASSWORD") ?: System.console()?.readLine("\nKey password: ")
}
}
}

在 Android Studio 中进行 Gradle 同步时,System.console() 返回 null,因而密码均为 null,不会中断同步过程,也不影响调试版本的构建。

我创建了 signing.propertiessigning.properties.travis 两个文件,在前者中填写 keystore 的路径并加入 .gitignore,而将后者在 .travis.ymlbefore_script 中复制为 signing.properties

而在 Travis CI 的设置中,添加 STORE_PASSWORDKEY_PASSWORD 两个环境变量即可。建议在环境变量值的两端加上单引号以避免特殊字符被 shell 错误处理。

获取版本信息

直接在 .travis.yml 中调用 git describegit log 等命令是无法成功的,因为 Travis CI 采用了 git clone --depth=50 来进行 clone。相应地,需要先执行 git fetch --unshallow 来完成 clone。

我采用了 语义化版本(Semver)来命名版本。因此,我的版本名称采用了如下方式获取:

1
version="$(git describe --long --tags | sed 's/^v//;s/-\([0-9]\+\)-g\([0-9a-f]\+\)/+\1.\2/')"

例如,在名为 1.0.0-alpha 的 tag 后第 227 次短哈希值为 886f8ce 的 commit,对应的版本名即为 1.0.0-alpha.1+227.886f8ce

然后使用 sed -i 's/versionName .*/versionName "'"${version}"'"/' app/build.gradle 即可更新 build.gradle 中的 versionName 字段。

上传至 Release

GitHub 提供了在 Release 中上传二进制构建输出的功能。然而,如果直接在项目仓库中为每次 commit 添加 Release(和相应的 tag)则未免过于杂乱,因此我选择了新建 一个只有 README 的仓库,并将所有持续集成版本的 Release 创建在此仓库中。

为了实现此功能,我选择了通过 curl 调用 GitHub API 的方式来完成。经过查阅文档和大量的调试,我的脚本最终是如下编写的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#!/bin/bash

set -e

repo="$1"
shift
echo "Repo: ${repo}" >&2

version="$1"
shift
echo "Version: ${version}" >&2

tag="v${version}"
echo "Tag: ${tag}" >&2

body="$1"
shift
echo "Body: ${body}" >&2

# Get old release by tag
echo "Getting old release by tag..." >&2
response="$(curl -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" "https://api.github.com/repos/${repo}/releases/tags/${tag}")"
echo "${response}" >&2
old_release_id="$(echo "${response}" | jq -r '.id')"

if [["${old_release_id}" != "null" ]]; then

# Delete old release
echo "Deleting old release..." >&2
response="$(curl -X 'DELETE' -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" "https://api.github.com/repos/${repo}/releases/${old_release_id}")"
echo "${response}" >&2
fi

# Create release
echo "Creating release..." >&2
response="$(curl -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" -H 'Content-Type: application/json' --data "{ \"tag_name\": $(echo -n "${tag}" | jq -s -R -r @json), \"name\": $(echo -n "${tag}" | jq -s -R -r @json), \"body\": $(echo -n "${body}" | jq -s -R -r @json) }" "https://api.github.com/repos/${repo}/releases")"
echo "${response}" >&2
upload_url="$(echo "${response}" | jq -r '.upload_url' | sed 's/{?name,label}$//g')"
echo "Upload url: ${upload_url}" >&2

for file in "$@"; do
# Upload file
echo "Uploading file: ${file}" >&2
name="$(basename "${file}")"
response="$(curl -H "Authorization: token ${GITHUB_ACCESS_TOKEN}" -H "Content-Type: $(file -b --mime-type "${file}")" --data-binary "@${file}" "${upload_url}?name=$(echo -n "${name}" | jq -s -R -r @uri)")"
echo "${response}" >&2
done

而设置 GITHUB_ACCESS_TOKEN 环境变量后的用法则如:

1
./upload-to-releases.sh 'zhanghai/DouyaCiBuilds' "${version}" "${description}" "douya-ci-${version}.apk"

其他

如果无法确定错误原因,就多加些 echo 或者 cat 吧。

如果希望在 Lint 失败时查看输出,可以在 after_failure 中加入 cat app/build/outputs/lint-results-*.html

我所采用的配置都可以在 豆芽 中找到。

结语

为 Android 项目使用 Travis CI 的过程还算简单,但是也有一些微妙的问题需要解决,这令我花费了不少时间。而将每次的构建输出上传至另一个仓库的 Release 则是我考虑了一段时间后得出的方案,之前没见到过这种方式,用 curl 调用 GitHub API 也是第一次,同时再次感受到了 bash 的得心应手,总体上是一次十分有趣的体验。

因此,将我的配置过程和结果记录在此,希望对其他开发者有所帮助。