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。

Android CI 完整流程 代码Push/Tag 参数化构建 类型/版本/渠道 Lint + 单元测试 质量门禁 Gradle构建 assemble 自动化签名 环境变量注入 产物归档 Artifacts 分发(蒲公英/内测平台) 失败通知(钉钉/邮件) 触发 配置 检查 构建 签名 归档 分发

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