13、持续集成(CI)集成:Jenkins/GitHub Actions/GitLab CI配置、构建参数化、自动化签名、构建产物归档与分发
持续集成,说白了就是让机器替你干活。
我刚开始带团队那会儿,每天最烦的就是帮同事打Release包。谁要签名、谁要渠道包、谁要改版本号……一天下来,光打包就能耗掉两小时。后来我下定决心,把CI这套东西彻底搞通。今天这章,我就把这几年的实战经验掰开揉碎讲给你听。
13.1 为什么Android项目必须上CI?
你想想看,一个标准的Android项目,从代码提交到最终分发,中间要经历多少步?
- 代码拉取、依赖下载
- Lint检查、单元测试
- 构建Debug/Release包
- 签名、对齐、混淆
- 上传到分发平台或应用商店
这些步骤如果全靠手动,出错的概率极高。我曾经有一次因为手抖选错了签名文件,导致线上包闪退,紧急回滚了一整天。嗯,从那以后,我再也不敢手动签名了。
核心原则:CI不是锦上添花,而是Android工程的标配。它能保证每次构建都是可复现、可追溯的。
13.2 三大CI工具对比与选型
目前主流的CI工具就三个:Jenkins、GitHub Actions、GitLab CI。我三个都用过,说说我的感受。
| 特性 | Jenkins | GitHub Actions | GitLab CI |
|---|---|---|---|
| 部署方式 | 自托管服务器 | 云端(SaaS) | 云端/自托管 |
| 配置复杂度 | 较高(需维护Jenkinsfile) | 低(YAML配置) | 中等(YAML配置) |
| 插件生态 | 极其丰富 | Marketplace丰富 | 内置功能较多 |
| 适合场景 | 大型企业、私有化部署 | 开源项目、中小团队 | GitLab深度用户 |
| Android构建支持 | 需配置Gradle/Android SDK | 官方提供setup-android action | 需自定义Docker镜像 |
我个人习惯是:公司内部项目用Jenkins,因为可以完全控制构建环境;开源项目用GitHub Actions,省心省力;如果团队已经在用GitLab管理代码,那就直接用GitLab CI,减少工具切换成本。
13.3 构建参数化——让CI更灵活
为什么要参数化?举个例子:你有时候想打Debug包,有时候想打Release包;有时候需要改版本号,有时候不需要。如果每次都要改配置文件,那CI就失去了意义。
参数化的本质,就是把构建过程中可能变化的变量抽出来,通过CI界面或API传入。
13.3.1 Jenkins参数化构建
在Jenkins中,我通常配置以下参数:
// Jenkinsfile 片段
properties([
parameters([
choice(name: 'BUILD_TYPE', choices: ['debug', 'release'], description: '构建类型'),
string(name: 'VERSION_NAME', defaultValue: '1.0.0', description: '版本号'),
booleanParam(name: 'SIGN_ENABLED', defaultValue: true, description: '是否签名'),
choice(name: 'FLAVOR', choices: ['all', 'huawei', 'xiaomi', 'oppo'], description: '渠道')
])
])
pipeline {
agent any
stages {
stage('Build') {
steps {
script {
def buildType = params.BUILD_TYPE
def version = params.VERSION_NAME
def flavor = params.FLAVOR
// 动态拼接Gradle命令
sh "./gradlew assemble${flavor.capitalize()}${buildType.capitalize()} -PversionName=${version}"
}
}
}
}
}
小技巧:在Jenkins中,我习惯把Gradle的JVM参数也做成参数,比如`GRADLE_OPTS`。因为不同项目的内存需求不一样,硬编码容易导致OOM。
13.3.2 GitHub Actions参数化
GitHub Actions通过`workflow_dispatch`事件实现参数化:
# .github/workflows/android-build.yml
name: Android CI
on:
workflow_dispatch:
inputs:
build_type:
description: '构建类型'
required: true
default: 'release'
type: choice
options:
- debug
- release
version_name:
description: '版本号'
required: true
default: '1.0.0'
sign_apk:
description: '是否签名'
required: true
default: true
type: boolean
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'zulu'
java-version: '17'
- name: Build with Gradle
run: |
./gradlew assembleRelease \
-PversionName=${{ github.event.inputs.version_name }} \
-PbuildType=${{ github.event.inputs.build_type }}
13.3.3 GitLab CI参数化
GitLab CI通过`variables`和`rules`实现:
# .gitlab-ci.yml
variables:
BUILD_TYPE: "release"
VERSION_NAME: "1.0.0"
SIGN_ENABLED: "true"
stages:
- build
build-release:
stage: build
script:
- ./gradlew assembleRelease -PversionName=$VERSION_NAME
rules:
- if: '$CI_PIPELINE_SOURCE == "web"'
when: always
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.\d+$/'
when: always
注意:GitLab CI的参数化相对弱一些,我通常配合GitLab的CI/CD变量功能,在项目设置中预定义敏感参数(如签名密码),然后在pipeline中引用。
13.4 自动化签名——告别手动输入密码
签名是Android构建中最容易出错的环节。我见过太多人把签名文件传到Git仓库里,或者把密码写在代码里。这都是安全隐患。
正确的做法是:签名文件加密存储,密码通过CI的环境变量注入。
13.4.1 签名信息配置
在`app/build.gradle`中,我这样配置签名:
android {
signingConfigs {
release {
storeFile file(System.getenv("KEYSTORE_PATH") ?: "debug.keystore")
storePassword System.getenv("KEYSTORE_PASSWORD") ?: "android"
keyAlias System.getenv("KEY_ALIAS") ?: "androiddebugkey"
keyPassword System.getenv("KEY_PASSWORD") ?: "android"
}
}
buildTypes {
release {
signingConfig signingConfigs.release
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
13.4.2 在CI中注入签名变量
Jenkins:使用Credentials Binding插件,将签名文件以Secret File形式存储,密码以Secret Text存储。
pipeline {
environment {
KEYSTORE_PATH = credentials('android-keystore-file')
KEYSTORE_PASSWORD = credentials('android-keystore-password')
KEY_ALIAS = credentials('android-key-alias')
KEY_PASSWORD = credentials('android-key-password')
}
stages {
stage('Sign Build') {
steps {
sh './gradlew assembleRelease'
}
}
}
}
GitHub Actions:将签名文件Base64编码后存入Secrets,在workflow中解码。
- name: Decode Keystore
run: |
echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 -d > keystore.jks
env:
KEYSTORE_PATH: keystore.jks
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
重要:签名文件永远不要提交到Git仓库。我见过有人把release签名文件放在`app/`目录下,结果被实习生push到了公开仓库……那场面,简直灾难。
13.5 构建产物归档与分发
构建完了,APK放哪儿?这是CI流程的最后一步,也是最容易被忽视的一步。
13.5.1 产物归档
归档的目的是让每个构建版本都有迹可循。
Jenkins归档:
post {
success {
archiveArtifacts artifacts: '**/build/outputs/apk/**/*.apk', fingerprint: true
}
}
GitHub Actions上传Artifact:
- name: Upload APK
uses: actions/upload-artifact@v4
with:
name: app-release-${{ github.sha }}.apk
path: app/build/outputs/apk/release/app-release.apk
GitLab CI归档:
build-release:
artifacts:
paths:
- app/build/outputs/apk/release/*.apk
expire_in: 30 days
13.5.2 自动分发到内测平台
光归档还不够,你得让测试同学能拿到包。我常用的分发方式有两种:
- 蒲公英/fir.im:通过API上传,返回下载链接和二维码
- 自建分发平台:通过SSH上传到公司内网服务器,生成下载页面
以蒲公英为例,在CI中集成:
# GitHub Actions 上传到蒲公英
- name: Upload to Pgyer
run: |
curl -F "file=@app/build/outputs/apk/release/app-release.apk" \
-F "_api_key=${{ secrets.PGYER_API_KEY }}" \
-F "buildInstallType=2" \
-F "buildPassword=123456" \
https://www.pgyer.com/apiv2/app/upload
我的习惯:每次构建成功后,我会让CI自动给测试群发一条消息,包含下载链接和版本更新说明。这样测试同学不用追着我要包,效率提升很明显。
13.6 完整CI流程的SVG架构图
下面这张图,是我总结的Android CI完整流程。你可以把它当作搭建CI的checklist。
13.7 避坑指南
最后,分享几个我踩过的坑,希望能帮你少走弯路。
坑1:Gradle守护进程内存溢出
我曾经在Jenkins上同时跑3个构建任务,结果Gradle守护进程直接OOM。后来我限制了每个任务的JVM参数:`-Xmx2048m -XX:MaxMetaspaceSize=512m`,并在CI配置中设置`GRADLE_OPTS`,问题解决。
坑2:签名密码泄露
有一次我把签名密码写在了`build.gradle`里,然后不小心push到了公开分支。虽然马上删除了,但Git历史里还有。从那以后,我强制要求所有敏感信息必须通过环境变量注入,并且在CI配置中开启日志脱敏。
坑3:构建产物命名冲突
多个分支同时构建时,如果APK文件名相同,归档时会互相覆盖。我现在的做法是:在文件名中加入分支名、构建时间和Commit SHA,确保唯一性。
android {
applicationVariants.all { variant ->
variant.outputs.all {
outputFileName = "${variant.name}-${variant.versionName}-${buildTime()}-${gitSha()}.apk"
}
}
}
最后说一句:CI配置不是一蹴而就的。我建议你先从最简单的「代码提交→自动构建→归档」开始,然后逐步加入签名、分发、测试等环节。每加一个环节,都要确保它稳定可靠,再考虑下一个。别想着一步到位,那样反而容易出问题。
公众号:蓝海资料掘金营,微信deep3321