CI/CD集成:Jenkins/GitHub Actions/GitLab CI配置、Gradle参数化构建、并行任务、报告上传、自动化发布

CI/CD,说白了就是让机器替我们干那些重复的体力活。我刚开始做Android开发那会儿,每次发版都得手动打包、签名、上传,折腾下来大半天就没了。后来我意识到,这些流程完全可以自动化。今天我们就聊聊怎么把Gradle脚本和CI/CD工具结合起来,让构建、测试、发布一气呵成。

核心思路:Gradle脚本是构建的“大脑”,CI/CD工具是“手脚”。你只需要把参数暴露出来,剩下的交给流水线去调度。

CI/CD工具的选择与对比

目前主流的CI/CD工具有三个:Jenkins、GitHub Actions、GitLab CI。我三个都用过,简单说说我的感受。

工具 托管方式 配置语言 适合场景
Jenkins 自托管 Groovy / Pipeline DSL 企业内部、复杂流水线
GitHub Actions 云托管 YAML 开源项目、中小团队
GitLab CI 云托管/自托管 YAML GitLab深度用户

我个人习惯是:如果公司有现成的Jenkins集群,就用Jenkins;如果是个人项目或小团队,GitHub Actions最省心。GitLab CI嘛,如果你已经在用GitLab管理代码,那自然是最顺手的。

Gradle参数化构建

要让CI/CD灵活起来,第一步就是把构建参数暴露出来。你想想看,如果每次改版本号、改构建类型都要改代码,那还叫什么自动化?

我一般在项目的根目录build.gradle.kts里定义一些可配置的参数:

// 根目录 build.gradle.kts
ext {
    set("appVersionName", project.findProperty("versionName") ?: "1.0.0")
    set("appVersionCode", (project.findProperty("versionCode") ?: "1").toString().toInt())
    set("buildType", project.findProperty("buildType") ?: "debug")
}

然后在app模块的build.gradle.kts里引用:

android {
    defaultConfig {
        versionName = rootProject.ext.get("appVersionName") as String
        versionCode = rootProject.ext.get("appVersionCode") as Int
    }
    buildTypes {
        getByName("release") {
            isMinifyEnabled = true
            // 只有传了 buildType=release 才启用混淆
            if (rootProject.ext.get("buildType") != "release") {
                isMinifyEnabled = false
            }
        }
    }
}

这样做的好处是,CI/CD工具只需要通过-P参数传值就行。比如:

./gradlew assembleRelease -PversionName=2.0.0 -PversionCode=10 -PbuildType=release

小技巧:我习惯把敏感信息(比如签名密码、API Key)放在gradle.properties里,但这个文件不提交到Git。CI/CD工具通过环境变量注入,然后在脚本里读取System.getenv()

并行任务:让构建飞起来

你有没有遇到过这种情况:打包一个release版本要等十几分钟?其实很多任务是可以并行的。Gradle本身就支持并行执行,你只需要在gradle.properties里开启:

org.gradle.parallel=true
org.gradle.caching=true
org.gradle.daemon=true

但要注意,并行不是万能的。我在项目中遇到过一个问题:多个模块同时执行lint任务,结果因为共享同一个输出目录,导致报告互相覆盖。后来我给每个模块单独指定了输出路径:

android {
    lintOptions {
        xmlOutput = file("${project.buildDir}/reports/lint-${project.name}.xml")
    }
}

嗯,这里要注意:并行任务一定要考虑资源竞争,尤其是文件写入和网络请求。

在CI/CD层面,你也可以并行执行多个Job。比如GitHub Actions的矩阵策略:

jobs:
  build:
    strategy:
      matrix:
        api-level: [21, 29, 33]
    steps:
      - run: ./gradlew connectedCheck -PapiLevel=${{ matrix.api-level }}

这样三个API级别的测试可以同时跑,总时间从30分钟降到10分钟。

报告上传:让结果看得见

构建完了,测试跑完了,报告得有个地方放。我见过不少团队,测试报告只存在CI机器的临时目录里,出了问题还得去翻日志,太原始了。

我建议把报告上传到CI平台自带的Artifact存储里。以GitHub Actions为例:

- name: Upload Lint Report
  uses: actions/upload-artifact@v3
  with:
    name: lint-report
    path: app/build/reports/lint-results-*.html

- name: Upload Unit Test Report
  uses: actions/upload-artifact@v3
  with:
    name: unit-test-report
    path: app/build/reports/tests/

如果是Jenkins,可以用publishHTML插件:

publishHTML(target: [
    reportName: 'Lint Report',
    reportDir: 'app/build/reports',
    reportFiles: 'lint-results.html'
])

我曾经踩过的坑:有一次Jenkins的HTML报告一直显示空白,查了半天发现是Jenkins默认开启了CSRF保护,导致内嵌的CSS和JS被拦截。解决办法是在Jenkins系统设置里关闭“Enable security”中的“Prevent Cross Site Request Forgery”选项,或者改用纯静态HTML报告。

自动化发布:一键上架

自动化发布的终极目标是什么?是“代码合并到主分支,然后睡一觉醒来发现App已经上架了”。

我常用的流程是这样的:

  1. 打Release包:用./gradlew assembleRelease,同时传入版本号参数。
  2. 签名:签名信息从环境变量读取,不写死在脚本里。
  3. 上传到分发平台:比如Firebase App Distribution、蒲公英、或者直接上传到Google Play。
  4. 打Tag:自动给Git打上版本标签。
  5. 通知团队:发个Slack消息或者邮件。

下面是一个GitHub Actions的完整示例:

name: Release to Play Store

on:
  push:
    branches: [ main ]

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3

      - name: Set up JDK 17
        uses: actions/setup-java@v3
        with:
          java-version: '17'

      - name: Build Release APK
        run: ./gradlew assembleRelease -PversionName=${{ github.ref_name }}

      - name: Sign APK
        env:
          KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
        run: |
          echo $KEYSTORE_BASE64 | base64 -d > keystore.jks
          jarsigner -keystore keystore.jks -storepass $KEY_PASSWORD \
            -keypass $KEY_PASSWORD app/build/outputs/apk/release/app-release-unsigned.apk \
            $KEY_ALIAS

      - name: Upload to Google Play
        uses: r0adkll/upload-google-play@v1
        with:
          serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }}
          packageName: com.example.app
          releaseFiles: app/build/outputs/apk/release/app-release-unsigned.apk
          track: production

我的建议:不要把Google Play的Service Account JSON直接放在代码里。用GitHub的Secrets功能存Base64编码后的内容,在CI里解码使用。这样既安全又方便。

知识体系总览

下面这张图是我整理的CI/CD集成核心逻辑,你可以对照着看:

CI/CD 集成核心流程 代码提交 CI 触发 Gradle 参数化构建 并行测试 (Lint/Unit) 并行打包 (多Flavor) 报告上传 / 自动化发布

从这张图可以看出来,整个流程是线性的,但中间有并行分支。我个人觉得,CI/CD的核心不是工具本身,而是你如何设计这个流程。参数化构建是基础,并行任务是加速器,报告上传是保障,自动化发布是最终目标。

好了,这一章的内容就到这里。记住一句话:让机器做机器擅长的事,让人做人擅长的事。CI/CD就是帮你实现这个目标的工具。


公众号:蓝海资料掘金营,微信deep3321