26、Gradle与CI/CD集成:Jenkins/GitHub Actions/GitLab CI配置、Gradle构建扫描、构建参数化、自动化发布流程

说实话,很多团队在本地开发时跑得飞起,一到CI服务器就各种翻车。我见过太多人把Gradle脚本写得跟本地测试一样,结果Jenkins上一跑就是半小时,还动不动就缓存失效。今天咱们就聊聊怎么把Gradle和CI/CD真正玩明白。

为什么CI/CD里Gradle总出问题?

先说说我踩过的坑。有一次项目上线前,Jenkins构建突然失败,查了半天发现是Gradle守护进程在CI环境里内存不够。你想想看,本地开发机16G内存随便用,CI容器可能就给了2G,守护进程一启动就占掉一半,剩下的根本不够编译。

另一个常见问题是缓存。本地构建有~/.gradle/caches,但CI环境每次都是全新拉代码,如果不配置缓存,每次都要重新下载所有依赖。我见过一个项目,光依赖下载就占了构建时间的70%。

Gradle构建扫描:你的构建显微镜

我个人习惯,所有CI构建都开启Gradle构建扫描。这东西就像给你的构建过程装了个显微镜,能看清每一步花了多少时间、哪些任务被跳过了、依赖是怎么解析的。

核心配置:在build.gradle.kts中启用构建扫描

// settings.gradle.kts
plugins {
    id("com.gradle.enterprise") version("3.16.1")
}

gradleEnterprise {
    server = "https://你的扫描服务地址"
    buildScan {
        publishAlways()
        capture {
            isTaskInputFiles = true
        }
    }
}

如果你用Gradle Cloud,更简单:

// 在gradle.properties中
org.gradle.enterprise.build-scan.publish=true

构建完成后,你会得到一个URL,点进去能看到完整的构建时间线。我曾经靠这个发现了一个诡异的性能问题——某个自定义任务每次都要重新执行,原因是输入文件路径里有个动态时间戳。

小技巧:构建扫描的URL可以自动发布到CI的构建日志里,方便团队回溯。在Jenkins里用post步骤,GitHub Actions里用step的output就行。

构建参数化:让CI脚本更灵活

为什么要参数化?说白了,就是同一个构建脚本,能在不同场景下跑出不同结果。比如:

  • PR构建只跑单元测试,不打包
  • develop分支构建要生成Debug包并上传
  • release分支构建要生成Release包并签名

我建议用Gradle属性来传递参数,而不是硬编码在脚本里:

// gradle.properties
buildType=debug
versionCode=1
versionName=1.0.0
enableTestCoverage=false

在CI脚本里动态覆盖:

# Jenkins Pipeline
./gradlew assembleRelease -PbuildType=release -PversionCode=${BUILD_NUMBER}

这样你的Gradle脚本就变成了一个灵活的构建引擎,CI只管传参数就行。

三大CI工具实战配置

我分别说说Jenkins、GitHub Actions和GitLab CI的配置要点。嗯,这里要注意,不同工具有不同的缓存机制,配置错了会浪费大量时间。

Jenkins Pipeline

Jenkins我用了好多年,它的Pipeline as Code确实强大。关键是要配置好Gradle缓存目录:

pipeline {
    agent any
    environment {
        GRADLE_USER_HOME = "${WORKSPACE}/.gradle"
    }
    stages {
        stage('Checkout') {
            steps {
                checkout scm
            }
        }
        stage('Build') {
            steps {
                sh './gradlew assembleDebug --build-cache'
            }
        }
    }
    post {
        always {
            // 发布构建扫描URL
            sh 'cat build/reports/build-scan-url.txt'
        }
    }
}

我曾经踩过一个坑:Jenkins默认会清理工作空间,导致每次构建都要重新下载依赖。后来我配置了GRADLE_USER_HOME的持久化目录,构建时间从8分钟降到了2分钟。

GitHub Actions

GitHub Actions的缓存配置更直观,直接用actions/cache:

name: Android CI
on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Cache Gradle
        uses: actions/cache@v3
        with:
          path: |
            ~/.gradle/caches
            ~/.gradle/wrapper
          key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
          
      - name: Build with Gradle
        run: ./gradlew assembleDebug --build-cache
        
      - name: Publish Build Scan
        if: always()
        run: cat build/reports/build-scan-url.txt

我个人习惯把Gradle Wrapper也缓存起来,这样不用每次下载Gradle发行版。注意key要包含gradle文件的hash,否则缓存可能失效。

GitLab CI

GitLab CI的缓存配置稍微不同,但原理一样:

image: gradle:8.5-jdk17

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

cache:
  key: "$CI_COMMIT_REF_SLUG"
  paths:
    - .gradle/
    - build/

stages:
  - build
  - test
  - deploy

build-debug:
  stage: build
  script:
    - ./gradlew assembleDebug --build-cache
  artifacts:
    paths:
      - app/build/outputs/apk/debug/

这里有个坑:GitLab CI的缓存默认是按分支隔离的,如果你在不同分支间切换,缓存会重建。我建议用$CI_COMMIT_REF_SLUG作为key,这样每个分支有自己的缓存。

自动化发布流程

自动化发布是CI/CD的终极目标。我一般分三步走:

  1. 构建验证:跑单元测试、lint检查、构建APK
  2. 版本管理:自动生成版本号、打Git Tag
  3. 发布上传:上传到应用商店或内部分发平台

这里给个GitHub Actions的完整发布示例:

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build Release APK
        run: ./gradlew assembleRelease \
          -PversionName=${GITHUB_REF#refs/tags/v} \
          -PversionCode=${{ github.run_number }}
          
      - name: Sign APK
        run: |
          echo "${{ secrets.KEYSTORE }}" | base64 --decode > keystore.jks
          ./gradlew signReleaseApk
          
      - name: Upload to Play Store
        run: ./gradlew publishReleaseBundle
        env:
          PLAY_STORE_CREDENTIALS: ${{ secrets.PLAY_STORE_CREDENTIALS }}

注意:签名密钥和商店凭证绝对不能硬编码在脚本里。用CI的Secrets功能存储,构建时通过环境变量注入。我曾经见过有人把密钥直接写在gradle.properties里提交到GitHub,后果可想而知。

SVG流程图:CI/CD与Gradle集成架构

Gradle与CI/CD集成架构 代码提交 CI触发构建 Jenkins Pipeline GitHub Actions GitLab CI Gradle构建(参数化) 构建扫描(性能分析) 自动化发布(APK/Bundle) 代码触发 CI平台 Gradle构建

避坑指南

最后分享几个我实战中遇到的坑:

  • 守护进程问题:CI环境里一定要禁用Gradle守护进程,加-Dorg.gradle.daemon=false。否则进程残留会导致后续构建失败。
  • 缓存失效:如果构建脚本经常变,缓存命中率会很低。我建议把gradle文件单独拿出来做缓存key的一部分。
  • 构建扫描权限:如果用了私有构建扫描服务,记得在CI环境里配置API密钥,否则扫描结果发布不上去。
  • 并行构建:Gradle的并行构建在CI里要谨慎使用。我遇到过因为并行导致资源竞争,最终构建反而更慢的情况。

说实话,CI/CD集成这件事,配置一次能管很久。但一旦出了问题,排查起来特别痛苦。所以我的建议是:先把构建扫描配好,出了问题能快速定位;再把缓存配好,节省团队时间;最后才是自动化发布,减少人工操作失误。

我的习惯:每次CI构建失败,第一件事就是看构建扫描的时间线。90%的问题都能从那里找到线索。剩下的10%,嗯,那就得看日志了。


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