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的终极目标。我一般分三步走:
- 构建验证:跑单元测试、lint检查、构建APK
- 版本管理:自动生成版本号、打Git Tag
- 发布上传:上传到应用商店或内部分发平台
这里给个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集成架构
避坑指南
最后分享几个我实战中遇到的坑:
- 守护进程问题:CI环境里一定要禁用Gradle守护进程,加
-Dorg.gradle.daemon=false。否则进程残留会导致后续构建失败。 - 缓存失效:如果构建脚本经常变,缓存命中率会很低。我建议把gradle文件单独拿出来做缓存key的一部分。
- 构建扫描权限:如果用了私有构建扫描服务,记得在CI环境里配置API密钥,否则扫描结果发布不上去。
- 并行构建:Gradle的并行构建在CI里要谨慎使用。我遇到过因为并行导致资源竞争,最终构建反而更慢的情况。
说实话,CI/CD集成这件事,配置一次能管很久。但一旦出了问题,排查起来特别痛苦。所以我的建议是:先把构建扫描配好,出了问题能快速定位;再把缓存配好,节省团队时间;最后才是自动化发布,减少人工操作失误。
我的习惯:每次CI构建失败,第一件事就是看构建扫描的时间线。90%的问题都能从那里找到线索。剩下的10%,嗯,那就得看日志了。