Gradle Build: Phases, Build Files, Scanners and CI
How Gradle builds JVM projects, which files it needs, how to lint, scan and test with it, and a complete GitHub Actions pipeline.
Gradle is the main alternative to Maven on the JVM and the only build tool for Android. Builds are written as code (Kotlin or Groovy DSL) and modelled as a graph of tasks. Incremental builds, the build cache and the configuration cache make Gradle fast on large multi-project repositories.
This post is part of Build Ecosystems by Programming Language. For the declarative alternative see Maven Build.
At a glance
| Category | Common tools |
|---|---|
| Build tool | Gradle 8+, Gradle Wrapper (gradlew) |
| Package registry | Maven Central, Gradle Plugin Portal, private Artifactory / Nexus |
| Build files | settings.gradle.kts, build.gradle.kts, gradle.properties, gradle/libs.versions.toml, gradlew, gradle/wrapper/gradle-wrapper.properties |
| Lint and format | Spotless, Checkstyle, PMD, ktlint and detekt (Kotlin) |
| SAST (open source) | SpotBugs + FindSecBugs, Semgrep, CodeQL, SonarQube Community Build |
| SAST (commercial) | Checkmarx One, OpenText Fortify, Veracode, SonarQube Server |
| SCA | OWASP Dependency-Check plugin, Trivy, Snyk, Mend, Black Duck, GitHub dependency submission |
| Frameworks | Spring Boot, Micronaut, Ktor, Quarkus |
| Test and coverage | JUnit 5, Kotest, Mockito / MockK, JaCoCo, Kover |
| Build output | .jar, .war, distribution zip, container image via Jib |
| CI cache | ~/.gradle/caches, ~/.gradle/wrapper (handled by gradle/actions/setup-gradle) |
How the build works
Every Gradle build runs in three phases. Tasks are then executed in dependency order, and a task whose inputs and outputs have not changed is skipped (UP-TO-DATE) or restored from the build cache (FROM-CACHE).
In CI, each stage of the generic build pipeline maps to a Gradle command, plugin or scanner. Press play, or step through it:
- Source + build scripts: A commit triggers CI. settings.gradle.kts and build.gradle.kts define the build, and the committed wrapper (gradlew) pins the Gradle version.
- Resolve dependencies: Gradle resolves dependencies from Maven Central and the Plugin Portal; gradle/actions/setup-gradle caches ~/.gradle/caches and the wrapper.
- Lint and format: ./gradlew spotlessCheck fails on unformatted code. Checkstyle and PMD, or ktlint and detekt for Kotlin, add lint rules.
- Compile: ./gradlew build compiles, tests and runs every check task. Incremental builds and the build cache skip work whose inputs did not change.
- Test and coverage: The test task runs JUnit 5 or Kotest; ./gradlew test jacocoTestReport writes coverage, and Kover is the Kotlin alternative.
- Security scans in parallel: ./gradlew spotbugsMain (with FindSecBugs) checks our code, ./gradlew dependencyCheckAnalyze checks dependencies for CVEs, and Gitleaks scans for secrets.
- Quality gate: ./gradlew build fails if any test, coverage rule or check task fails, and the dependency scan runs as its own required step.
- Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
- Package (pass): assemble builds the outputs: a .jar or .war in build/libs, or a distribution zip.
- SBOM and signing (pass): ./gradlew cyclonedxBom (org.cyclonedx.bom plugin) writes the SBOM, and the artifact is signed with Sigstore cosign.
- Publish (pass): The maven-publish plugin's publish task uploads to Maven Central or a private Artifactory / Nexus.
- Container image (pass): Jib builds the container image straight from Gradle, without a Dockerfile, and the registry scans it again.
build=assemble(produce artifacts) +check(tests and verification tasks).- Plugins such as
jacoco,checkstyleor SpotBugs add tasks and hook them intocheck. --configuration-cachecaches the configuration phase;--build-cachereuses task outputs between machines.
Build files
| File | Purpose | Commit? |
|---|---|---|
settings.gradle.kts | Root project name, included subprojects, plugin and dependency repositories | Yes |
build.gradle.kts | Plugins, dependencies and task configuration per project | Yes |
gradle.properties | Build flags (org.gradle.caching=true, JVM args) | Yes (no secrets) |
gradle/libs.versions.toml | Version catalog: one place for all dependency and plugin versions | Yes |
gradlew, gradlew.bat, gradle/wrapper/* | Wrapper scripts, jar and pinned Gradle version | Yes |
gradle/verification-metadata.xml | Dependency checksum / signature verification | Yes |
buildSrc/ or build-logic/ | Convention plugins shared by subprojects | Yes |
~/.gradle/gradle.properties | Repository credentials, signing keys | No |
build/, .gradle/ | Outputs and local caches | No (.gitignore) |
Commands by category
1
2
3
4
5
6
7
8
./gradlew build # compile + test + check
./gradlew spotlessCheck # format check
./gradlew spotbugsMain # SAST (com.github.spotbugs plugin)
./gradlew dependencyCheckAnalyze # SCA (org.owasp.dependencycheck plugin)
./gradlew test jacocoTestReport # tests + coverage
./gradlew cyclonedxBom # SBOM (org.cyclonedx.bom plugin)
./gradlew dependencies --configuration runtimeClasspath # inspect the dependency graph
./gradlew build --scan # publish a Build Scan for troubleshooting
Minimal project
A version catalog plus a Kotlin DSL build script with coverage, SpotBugs, Dependency-Check and SBOM.
1
2
3
4
5
6
7
8
9
10
11
# gradle/libs.versions.toml
[versions]
junit = "5.11.4"
[libraries]
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
[plugins]
spotbugs = { id = "com.github.spotbugs", version = "6.0.26" }
dependencycheck = { id = "org.owasp.dependencycheck", version = "11.1.1" }
cyclonedx = { id = "org.cyclonedx.bom", version = "1.10.0" }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// build.gradle.kts
plugins {
java
jacoco
alias(libs.plugins.spotbugs)
alias(libs.plugins.dependencycheck)
alias(libs.plugins.cyclonedx)
}
group = "com.example"
version = "1.0.0"
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
repositories { mavenCentral() }
dependencies {
testImplementation(platform(libs.junit.bom))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.test {
useJUnitPlatform()
finalizedBy(tasks.jacocoTestReport)
}
tasks.jacocoTestCoverageVerification {
violationRules { rule { limit { minimum = "0.80".toBigDecimal() } } }
}
tasks.check { dependsOn(tasks.jacocoTestCoverageVerification) }
dependencyCheck { failBuildOnCVSS = 7.0f }
Plugin versions move quickly; check the Gradle Plugin Portal for the latest release before copying.
Frameworks
| Framework | Start a Gradle project |
|---|---|
| Spring Boot | curl https://start.spring.io/starter.zip -d type=gradle-project-kotlin -d dependencies=web -o demo.zip |
| Micronaut | mn create-app com.example.demo --build=gradle_kotlin |
| Ktor | Generate at start.ktor.io, or ktor new with the Ktor CLI |
| Quarkus | quarkus create app com.example:demo --gradle-kotlin-dsl |
CI with GitHub Actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
name: gradle-ci
on: [push, pull_request]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
- uses: gradle/actions/setup-gradle@v4 # caching + wrapper validation
- name: Build, test, coverage gate, SpotBugs
run: ./gradlew build
- name: Dependency scan (SCA)
run: ./gradlew dependencyCheckAnalyze
env:
NVD_API_KEY: ${{ secrets.NVD_API_KEY }}
- name: SBOM
run: ./gradlew cyclonedxBom
- uses: actions/upload-artifact@v4
with:
name: demo
path: |
build/libs/*.jar
build/reports/bom.json
To pass the NVD key from the environment, read it in the build script: dependencyCheck { nvd { apiKey = System.getenv("NVD_API_KEY") } }.
Best practices
- Commit and validate the wrapper;
setup-gradlechecks the wrapper jar checksum automatically. - Use a version catalog (
libs.versions.toml) and convention plugins inbuild-logic/instead of copying configuration into every subproject or usingallprojects {}. - Use Java toolchains so the JDK used to compile is declared in the build, not inherited from the machine.
- Turn on caching:
org.gradle.caching=trueandorg.gradle.configuration-cache=trueingradle.properties; use a remote build cache for large teams. - Enable dependency verification (
./gradlew --write-verification-metadata sha256 help) to detect tampered artifacts. - Keep credentials out of the repo: read them from
~/.gradle/gradle.propertiesor environment variables.
