24、Gradle与CI/CD:集成Jenkins/GitHub Actions/GitLab CI、环境变量传递、条件构建
CI/CD,说白了就是让机器替你干活。
我刚开始带团队那会儿,每次发布都要手动跑构建、改配置、传包。有一次凌晨三点上线,我忘了切Profile,结果测试包直接发到了生产环境……嗯,从那以后,我再也不敢相信自己的手了。
这一章,我们就聊聊Gradle怎么和主流的CI/CD工具配合。包括Jenkins、GitHub Actions、GitLab CI,还有环境变量怎么传、条件构建怎么写。
核心要点:CI/CD不是把本地命令搬到服务器上跑,而是要让构建过程可重复、可追溯、可控制。
24.1 为什么Gradle在CI/CD中容易翻车?
很多人在本地构建好好的,一上CI就报错。我遇到过最典型的问题:
- 本地有缓存,CI没有,依赖下载超时
- 本地JDK版本和CI不一致,编译报错
- 本地能访问内网仓库,CI访问不了
- 环境变量没传进去,构建脚本直接崩
这些问题,其实都可以通过规范化的配置来解决。我们先从最基础的开始。
24.2 Jenkins + Gradle:老牌组合的实战套路
Jenkins我用了很多年。说实话,它配置起来有点繁琐,但胜在稳定、可控。
24.2.1 基础配置
在Jenkins里跑Gradle,核心就三步:
- 安装Gradle插件(或者直接用Docker Agent)
- 配置构建工具链(JDK版本、Gradle版本)
- 写Pipeline脚本
我个人习惯用Pipeline as Code,也就是Jenkinsfile。这样配置跟着代码走,不会出现「本地能跑,Jenkins跑不了」的尴尬。
// Jenkinsfile
pipeline {
agent any
tools {
jdk 'JDK17'
gradle 'Gradle8.5'
}
environment {
// 定义环境变量,后面可以在Gradle脚本里用
BUILD_ENV = 'production'
DEPLOY_HOST = '192.168.1.100'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Build') {
steps {
// 关键:通过-P传递项目属性
sh 'gradle clean build -Penv=${BUILD_ENV} -Phost=${DEPLOY_HOST}'
}
}
stage('Test') {
steps {
sh 'gradle test --no-daemon'
}
}
stage('Archive') {
steps {
archiveArtifacts artifacts: 'build/libs/*.jar'
}
}
}
post {
failure {
// 构建失败时发邮件
emailext(
subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}",
body: "请检查构建日志",
to: 'team@example.com'
)
}
}
}
我的经验:Jenkins里一定要加--no-daemon参数。Gradle Daemon在CI环境里反而容易出问题,尤其是容器化部署时,Daemon进程不会被正确清理。
24.2.2 环境变量传递的两种方式
在Gradle里读取外部变量,我常用两种方式:
| 方式 | 命令示例 | Gradle中读取 | 适用场景 |
|---|---|---|---|
| -P参数 | gradle build -PmyKey=myValue |
project.findProperty('myKey') |
构建时动态传入 |
| 系统环境变量 | export MY_KEY=myValue |
System.getenv('MY_KEY') |
全局配置,如API密钥 |
我曾经踩过一个坑:用-P传密码,结果Jenkins日志里明文显示了。后来我改用Jenkins的Credentials Binding插件,把敏感信息注入到环境变量里,再用System.getenv()读取。
注意:不要在Gradle脚本里硬编码密码或Token。用环境变量或者Gradle的gradle.properties文件(记得加到.gitignore里)。
24.3 GitHub Actions + Gradle:轻量级CI的玩法
GitHub Actions我最近用得比较多。它的优势是配置简单,和GitHub仓库天然集成。
24.3.1 一个标准的Workflow
# .github/workflows/gradle-build.yml
name: Gradle Build
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v3
- name: Build with Gradle
run: ./gradlew build
env:
# 通过env传递环境变量
BUILD_ENV: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
API_KEY: ${{ secrets.API_KEY }}
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: Package
path: build/libs
这里有个小技巧:${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }},这是GitHub Actions里的条件表达式。说白了就是:main分支走生产环境,其他分支走预发布环境。
24.3.2 条件构建的写法
在Gradle脚本里,我们可以根据环境变量做条件判断:
// build.gradle.kts
val buildEnv: String? = System.getenv("BUILD_ENV")
tasks.register("deploy") {
doLast {
when (buildEnv) {
"production" -> {
println("部署到生产环境...")
// 执行生产部署逻辑
}
"staging" -> {
println("部署到预发布环境...")
// 执行预发布部署逻辑
}
else -> {
println("未知环境,跳过部署")
}
}
}
}
// 条件性配置:生产环境才启用混淆
if (buildEnv == "production") {
tasks.named("build") {
// 启用ProGuard或R8混淆
}
}
我的建议:条件构建的逻辑不要太复杂。我曾经见过一个项目,build.gradle里写了200多行if-else,最后谁也看不懂。把复杂的条件判断放到CI配置里,Gradle脚本只做简单的分支处理。
24.4 GitLab CI + Gradle:企业级的选择
GitLab CI的配置方式和GitHub Actions类似,但有一些自己的特色。
24.4.1 .gitlab-ci.yml示例
# .gitlab-ci.yml
stages:
- build
- test
- deploy
variables:
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true"
BUILD_ENV: "development"
before_script:
- export GRADLE_USER_HOME=`pwd`/.gradle
- chmod +x ./gradlew
build:
stage: build
script:
- ./gradlew assemble
artifacts:
paths:
- build/libs/
expire_in: 1 week
only:
- main
- develop
test:
stage: test
script:
- ./gradlew test
artifacts:
reports:
junit: build/test-results/test/**/TEST-*.xml
deploy:
stage: deploy
script:
- ./gradlew deploy -Penv=$BUILD_ENV
only:
- main
when: manual # 手动触发,防止误操作
GitLab CI里有个好用的功能:when: manual。部署这种高风险操作,我建议都设成手动触发。我曾经因为自动部署把测试环境覆盖了,从那以后,凡是涉及生产的操作,我都加一道人工确认。
24.5 环境变量传递的最佳实践
不管用哪种CI工具,环境变量传递都有一些通用的原则:
- 敏感信息用Secrets:Jenkins用Credentials,GitHub用Secrets,GitLab用Variables
- 非敏感信息用-P参数:比如分支名、构建号、环境标识
- 统一命名规范:我习惯用大写+下划线,比如
BUILD_ENV、DEPLOY_HOST - 提供默认值:在Gradle脚本里用
findProperty()或getenv()时,给个兜底值
// 推荐写法:带默认值
val buildEnv = project.findProperty("env") as? String ?: "development"
val apiKey = System.getenv("API_KEY") ?: "default-dev-key"
// 不推荐:直接取值,没有默认值
// val buildEnv = project.property("env") as String // 没传-Penv就直接崩
24.6 条件构建的高级用法
条件构建不只是if-else。我常用的几个场景:
| 场景 | 实现方式 | 代码示例 |
|---|---|---|
| 按分支执行不同任务 | 读取CI提供的分支变量 | if (System.getenv("CI_COMMIT_BRANCH") == "main") |
| 按构建类型配置 | 通过-P传递buildType | if (project.hasProperty("release")) |
| 跳过特定任务 | 使用onlyIf条件 | tasks.named("lint") { onlyIf { !isCi } } |
| 动态选择依赖仓库 | 根据环境变量切换URL | repositories { maven { url = if (isCi) ciRepo else localRepo } } |
避坑指南:我曾经在条件构建里用了System.getenv()来检测CI环境,结果本地开发时因为没设这个变量,整个构建逻辑都变了。后来我加了一个isCi的辅助函数,统一判断:
val isCi: Boolean = System.getenv("CI") != null ||
System.getenv("JENKINS_HOME") != null ||
System.getenv("GITLAB_CI") != null
24.7 本章小结
Gradle和CI/CD的集成,说白了就是三件事:
- 环境变量怎么传:-P参数、系统环境变量、Secrets
- 条件构建怎么写:根据分支、环境、构建类型做分支
- CI脚本怎么配:Jenkinsfile、GitHub Actions YAML、GitLab CI YAML
嗯,这些套路掌握了,你的构建流水线基本就稳了。记住一点:CI/CD的目的是让构建自动化、可重复,而不是让Gradle脚本变成一团乱麻。保持简单,保持可控。