前言

最近幸得空闲,就来自己实践一遍 Android 的持续集成,之前公司一直在使用同事搭建的 gitlab+ci+firim,确实是方便了很多,所以就有了自己实现一遍的想法。

在实践的过程中,也是磕磕绊绊各种填坑。网上有很多实现持续集成的教程,但是自己实践的时候各种坑总是出其不意的出现,所以我想记录一下我的实践过程,尽可能的做到详细,希望实践过程中遇到的问题,在这里都能找到解决方案。

系列文章

Android 持续集成实践(一)——从0开始搭建 Gitlab 服务器

Android 持续集成实践(二)——配置 Docker + gitlab-runner 实现线上自动编译

Android 持续集成实践(三)——编写 .gitlab-ci.yml 实现自动化

Android 持续集成实践(四)——配置 WebHook 通知编译结果

Android 持续集成实践(五)—— ABI 分包、特殊渠道编译需求

Android 持续集成实践(三)——编写 .gitlab-ci.yml 实现自动化

要实现的效果

在第二篇的时候,gitlab 已经可以根据 tag 自动打包正式版和测试版了,但是这还远不能满足日常发包场景。
一般的正常发版流程是:签名并打包正式版 -> 加固 -> 多渠道 -> 上传应用市场
接下来记录一下我使用 gitlab-ci 实践这一过程的自动化。

定义 gitlab-ci 工作场景阶段

实现着整个流程,需要划分不同的阶段:

  • 编译阶段 build

    在第二篇里边已经实现了编译任务,每次提交代码或者推送相应的 tag 标签的时候,就会执行编译任务。

  • 加固和多渠道阶段 reinforceAndChannel

    因为我们发正式版的时候才需要去加固和统计渠道信息,所以这个任务执行的一个前提条件是正式版编译成功。
    当正式版编译完成后,就可以自动执行这个去进行加固和写入渠道信息。

  • 部署阶段 deploy

    这个任务就是把已经处理好的安装包,自动上传到对应的平台上,省去手动上传的成本。
    对于正式版,目前国内应用市场众多,上传到各应用市场是否能实现还有待研究。
    对于测试版,可以自动上传到 firim 这个 app 内测托管平台来完成测试版发版。

通过工作场景阶段的划分,正式版和测试版的自动化要经历的阶段就很清晰了:

正式版:通过 build -> reinforceAndChannel 这两个阶段输出加固过的渠道包
测试版:通过 build -> deploy 这两个阶段把编译的测试包发布到 firim

下面是 .gitlab-ci.yml 定义工作场景阶段的部分代码:


stages:
  - build
  - reinforceAndChannel
  - deploy

阶段划分完成后,接下来要定义每个阶段的任务。

定义 build 任务

build 阶段有下面几种情况:

  • 普通的一次提交代码到 gitlab

    每次提交代码到远端,都要触发编译任务去编译一次,可以及时发现代码错误

  • 发布测试版

    利用特定的 Tag 格式来触发编译任务,之后阶段的任务触发也用相同的触发条件

  • 发布正式版

    利用特定的 Tag 格式来触发编译任务,之后阶段的任务触发也用相同的触发条件

  • 打包测试用的正式包

    因为发布正式版会加固和打渠道包,打包一次会很耗时。而如果我们只是想用于测试的话(比如测试微信登录和分享,需要正式环境测),只需要打包一个不需要加固的正式包就可以测了。所以还需要一个只打包一个正式包的任务。

下面是 .gitlab.yml build 阶段的代码:


#region build 编译阶段

# 提交代码自动编译
build:
  stage: build # 表示 build 阶段执行的任务
  only:
    - master # 只有在 master 分支有提交时才执行任务
  script:
    - ./gradlew assembleDebug # 由 runner 执行的 shell 脚本,这里是执行 gradle 脚本
  tags:
    - android # 指定脚本在有 android 标签的 runner 上运行

# 利用标签打测试包,格式:“x.x.x-beta.x”
beta:
  stage: build
  only:
    - /^[\d]+\.[\d]+\.[\d]+-beta\.[\d]+$/
  script:
    - ./update-version-code # 这是自动更新版本号和版本名的脚本
    - ./gradlew assembleDebug
  artifacts:
    paths:
      - app/build/outputs/apk/debug/debug_com.tianxing.wln.aat_${CI_COMMIT_TAG}_${CI_PIPELINE_ID}.apk # 编译完成后输出debug安装包,用于 deploy 阶段上传 firim
  tags:
    - android

# 利用标签打测试包,格式:“x.x.x”
release:
  stage: build
  only:
    - /^[\d]+\.[\d]+\.[\d]+$/
  except:
    - branches
  script:
    - ./update-version-code
    - ./gradlew assembleRelease
  artifacts:
    paths:
      - app/build/outputs/apk/release/
  tags:
    - android

# 测试用正式包编译任务
releaseOfficial:
  stage: build
  only:
    - official # 新建 official 分支推送到远端,就可以自动打包测试用正式包了
  script:
    - export CI_COMMIT_TAG=$CI_PIPELINE_ID
    - ./update-version-code
    - ./gradlew assemble_360Release
  artifacts:
    paths:
      - app/build/outputs/apk/release/
  tags:
    - android

#endregion

update-version-code 脚本:

#!/usr/bin/env bash
echo "Updating Android build information. New version code: ${CI_PIPELINE_ID}, New version name: ${CI_COMMIT_TAG}";

sed -i "s/versionCode .*$/versionCode ${CI_PIPELINE_ID}/;" app/build.gradle
sed -i "s/versionName .*$/versionName \"${CI_COMMIT_TAG}\"/;" app/build.gradle

定义 reinforceAndChannel 任务

加固和多渠道是用360加固保linux版实现的。360加固保提供了命令行进行操作的方式,写个脚本用在持续集成里边很方便。

把 360加固保上传到 gitlab-runner 缓存目录
  1. 下载 360加固保linux版 ,解压得到 jiagu 文件夹
  2. jiagu 整个文件夹用 FileZilla 上传到服务器上我们挂载的 runner 缓存目录里边,我配置的目录是 /home/android-cache/ (不了解这个目录的可以再看看第二篇:传送门
  3. 这时候 360加固的相关文件所在目录是 /home/android-cache/jiagu
注意,我们所用的 360加固,必须是 linux 版的。虽然命令模式都是是用的 jar 包,但是 windows 版的 jar 包是不能用的,会加固失败,这里一定要格外注意。
创建加固和多渠道的 shell 脚本

360jiahu 脚本代码:

#!/usr/bin/env bash

# 如果你对这里的 cache 目录有疑问的话,请看第二篇
JAVACMD=/cache/jiagu/java/bin/java # 360加固自带的 java 运行环境
BASE=/cache/jiagu/jiagu.jar # 360加固的 jar 包
NAME=xxx # 这里写你的 360加固 的账号
PASSWORD=xxx # 360加固的密码

APK_NAME=xxxx.apk
APK_PATH=app/build/outputs/apk/release/${APK_NAME}   #需要加固的apk路径
DEST=app/build/outputs/apk/release/  #输出加固包路径

# 打印一下参数看是否正确
echo "------ args info ------

JAVACMD = ${JAVACMD}
BASE = ${BASE}
NAME = ${NAME}
PASSWORD = ${PASSWORD}
APK_NAME = ${APK_NAME}
APK_PATH = ${APK_PATH}
DEST = ${DEST}
KEYSTORE_PATH = ${KEYSTORE_PATH}
KEY_PASSWORD = ${KEY_PASSWORD}
KEY_ALIAS = ${KEY_ALIAS}
STORE_PASSWORD = ${STORE_PASSWORD}"

echo "------ running! ------"

chmod +x ${JAVACMD}
${JAVACMD} -jar ${BASE} -version
${JAVACMD} -jar ${BASE} -login ${NAME} ${PASSWORD}
${JAVACMD} -jar ${BASE} -importsign ${KEYSTORE_PATH} ${KEY_PASSWORD} ${KEY_ALIAS} ${STORE_PASSWORD} # 配置签名信息
${JAVACMD} -jar ${BASE} -showsign
# 这里渠道信息的配置文件直接放在项目根目录,格式参考 360加固官方文档
${JAVACMD} -jar ${BASE} -importmulpkg ./channels.txt # 配置渠道信息
${JAVACMD} -jar ${BASE} -showmulpkg
${JAVACMD} -jar ${BASE} -showconfig
${JAVACMD} -jar ${BASE} -jiagu ${APK_PATH} ${DEST} -autosign -automulpkg

echo "------ finished! ------"
注意,这里的 java 运行环境,也必须用 360加固 自带的 java 运行环境,避免引发不必要的异常
在 .gitlab-ci.yml 创建任务
#region 加固和打渠道包阶段

reinforceAndChannel:
  stage: reinforceAndChannel
  only:
    - /^[\d]+\.[\d]+\.[\d]+$/
  script:
    - ./360jiagu
  artifacts:
    paths:
      - app/build/outputs/apk/release/
  tags:
    - android

#endregion

定义 deploy 任务

测试版发布到 firim
  1. 获取 api token

    首先需要有一个 firim 的账号,没有的请先注册,由于我们用到它的 api,所以需要获取一下 api_token
    要想上传应用还必须完成 实名认证

  2. 写上传 firim 的脚本 fir-publisher

    #!/bin/bash
    
    # Get API Token from http://fir.im/apps
    API_TOKEN="xxxxxx" # 这里填你自己的 token
    ORIGINAL_FILENAME="xxxxxx.apk" # gitlab 自动编译测试包生成的 apk 名字
    PACKAGE_DIR="app/build/outputs/apk/debug/" # 安装包位置
    # ios or android
    TYPE="android"
    # App 的 bundleId 或 包名
    BUNDLE_ID="xxx.xxx.xxx"
    
    package_path="${PACKAGE_DIR}${new_filename}"
    
    # Get upload_url
    credential=$(curl -X "POST" "http://api.fir.im/apps" \
    -H "Content-Type: application/json" \
    -d "{\"type\":\"${TYPE}\", \"bundle_id\":\"${BUNDLE_ID}\", \"api_token\":\"${API_TOKEN}\"}"
    )
    binary_response=$(echo ${credential} | grep -o "binary[^}]*")
    KEY=$(echo ${binary_response} | awk -F '"' '{print $5}')
    TOKEN=$(echo ${binary_response} | awk -F '"' '{print $9}')
    UPLOAD_URL=$(echo ${binary_response} | awk -F '"' '{print $13}')
    
    # Upload package
    echo 'Uploading...'
    echo '✈ -------------------------------------------- ✈'
    response=$(curl -F "key=${KEY}" \
    -F "token=${TOKEN}" \
    -F "file=@${package_path}" \
    -F "x:build=${CI_PIPELINE_ID}" \
    ${UPLOAD_URL}
    )
    echo $response;
    
  3. .gitlab-ci.yml 中创建部署任务

    uploadToFir:
      stage: deploy
      only:
        - /^[\d]+\.[\d]+\.[\d]+-beta\.[\d]+$/
      script:
        - ./fir-publisher
      tags:
        - android
    
    
  4. 测试版 app 应用内更新

    上传成功后,可以把 firim 检查更新的 api 用到 app 内,每次打开 app 检查新的测试版并提示测试用户去下载。
    需要注意的是,测试版和正式版包名不同,是两个 app,应用内更新的逻辑也是分两个,这里只针对测试版的应用内更新逻辑

正式版发布到各应用市场

这个目前没有好的想法,有待继续研究。。

.gitlab-ci.yml 完整代码

image: jangrewe/gitlab-ci-android

variables:
  GRADLE_OPTS: "-Dorg.gradle.daemon=false"

before_script:
  #  配置 gradle 的缓存目录
  - export GRADLE_USER_HOME=/cache/.gradle
  #  获取权限
  - chmod +x ./update-version-code
  - chmod +x ./360jiagu
  - chmod +x ./gradlew
  - chmod +x ./fir-publisher

stages:
  - build
  - reinforceAndChannel
  - deploy

#region build 编译阶段

# 提交代码自动编译
build:
  stage: build
  only:
    - master
  script:
    - ./gradlew assembleDebug
  tags:
    - android

# 利用标签打测试包,格式:“x.x.x-beta.x”
beta:
  stage: build
  only:
    - /^[\d]+\.[\d]+\.[\d]+-beta\.[\d]+$/
  script:
    - ./update-version-code
    - ./gradlew assembleDebug
  artifacts:
    paths:
      - app/build/outputs/apk/debug/xxx.apk # xxx 写你编译生成的测试版 app 的名字
  tags:
    - android

# 利用标签打测试包,格式:“x.x.x”
release:
  stage: build
  only:
    - /^[\d]+\.[\d]+\.[\d]+$/
  except:
    - branches
  script:
    - ./update-version-code
    - ./gradlew assembleRelease
  artifacts:
    paths:
      - app/build/outputs/apk/release/
  tags:
    - android

releaseOfficial:
  stage: build
  only:
    - official
  script:
    - export CI_COMMIT_TAG=$CI_PIPELINE_ID
    - ./update-version-code
    - ./gradlew assemble_360Release
  artifacts:
    paths:
      - app/build/outputs/apk/release/
  tags:
    - android

#endregion

#region 加固和打渠道包阶段

reinforceAndChannel:
  stage: reinforceAndChannel
  only:
    - /^[\d]+\.[\d]+\.[\d]+$/
  script:
    - ./360jiagu
  artifacts:
    paths:
      - app/build/outputs/apk/release/
  tags:
    - android

#endregion

uploadToFir:
  stage: deploy
  only:
    - /^[\d]+\.[\d]+\.[\d]+-beta\.[\d]+$/
  script:
    - ./fir-publisher
  tags:
    - android

效果展示

测试自动化编译

  • 发布测试版

    测试版编译发布
    上传 firim

  • 发布正式版

    正式版编译输出

    正式版加固多渠道

    正式版渠道包输出

    验证渠道信息

FAQ

  • curl: no URL specified!

    没有上传地址,报这个错误基本上是 firim 获取上传凭证那个 api 没有成功,可以用 postman 模拟一下请求,看报了什么错。
    我遇到的是因为刚注册的账号没有实名认证所以限制了不能上传应用

  • Uploading artifacts to coordinator… too large archive should fail job

    在打包的渠道包很多,输出渠道包的时候会报这个错误。这是因为输出的文件太大,超过了 gitlab 的 Maximum artifacts size 的默认值 100MB ,我们需要去 Admin Area -> Settings -> CI/CD -> Continuous Integration and Deployment 把这个值设置大一点就可以了。

参考资料

Uploading artifacts to coordinator… too large archive should fail job
Maximum artifacts size
GitLab CI/CD Pipeline Configuration Reference
360命令行加固
firim文档

下一篇:Android 持续集成实践(四)——配置 WebHook 通知编译结果

整理不易,如果这篇文章帮助到你了,请随意打赏~

在这里插入图片描述

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐