Post

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 Build: Phases, Build Files, Scanners and CI

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

CategoryCommon tools
Build toolGradle 8+, Gradle Wrapper (gradlew)
Package registryMaven Central, Gradle Plugin Portal, private Artifactory / Nexus
Build filessettings.gradle.kts, build.gradle.kts, gradle.properties, gradle/libs.versions.toml, gradlew, gradle/wrapper/gradle-wrapper.properties
Lint and formatSpotless, 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
SCAOWASP Dependency-Check plugin, Trivy, Snyk, Mend, Black Duck, GitHub dependency submission
FrameworksSpring Boot, Micronaut, Ktor, Quarkus
Test and coverageJUnit 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:

  1. 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.
  2. Resolve dependencies: Gradle resolves dependencies from Maven Central and the Plugin Portal; gradle/actions/setup-gradle caches ~/.gradle/caches and the wrapper.
  3. Lint and format: ./gradlew spotlessCheck fails on unformatted code. Checkstyle and PMD, or ktlint and detekt for Kotlin, add lint rules.
  4. Compile: ./gradlew build compiles, tests and runs every check task. Incremental builds and the build cache skip work whose inputs did not change.
  5. Test and coverage: The test task runs JUnit 5 or Kotest; ./gradlew test jacocoTestReport writes coverage, and Kover is the Kotlin alternative.
  6. Security scans in parallel: ./gradlew spotbugsMain (with FindSecBugs) checks our code, ./gradlew dependencyCheckAnalyze checks dependencies for CVEs, and Gitleaks scans for secrets.
  7. Quality gate: ./gradlew build fails if any test, coverage rule or check task fails, and the dependency scan runs as its own required step.
  8. Fail the build (fail): A failed check stops the pipeline at the gate, so nothing is packaged, signed or published.
  9. Package (pass): assemble builds the outputs: a .jar or .war in build/libs, or a distribution zip.
  10. SBOM and signing (pass): ./gradlew cyclonedxBom (org.cyclonedx.bom plugin) writes the SBOM, and the artifact is signed with Sigstore cosign.
  11. Publish (pass): The maven-publish plugin's publish task uploads to Maven Central or a private Artifactory / Nexus.
  12. 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, checkstyle or SpotBugs add tasks and hook them into check.
  • --configuration-cache caches the configuration phase; --build-cache reuses task outputs between machines.

Build files

FilePurposeCommit?
settings.gradle.ktsRoot project name, included subprojects, plugin and dependency repositoriesYes
build.gradle.ktsPlugins, dependencies and task configuration per projectYes
gradle.propertiesBuild flags (org.gradle.caching=true, JVM args)Yes (no secrets)
gradle/libs.versions.tomlVersion catalog: one place for all dependency and plugin versionsYes
gradlew, gradlew.bat, gradle/wrapper/*Wrapper scripts, jar and pinned Gradle versionYes
gradle/verification-metadata.xmlDependency checksum / signature verificationYes
buildSrc/ or build-logic/Convention plugins shared by subprojectsYes
~/.gradle/gradle.propertiesRepository credentials, signing keysNo
build/, .gradle/Outputs and local cachesNo (.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

FrameworkStart a Gradle project
Spring Bootcurl https://start.spring.io/starter.zip -d type=gradle-project-kotlin -d dependencies=web -o demo.zip
Micronautmn create-app com.example.demo --build=gradle_kotlin
KtorGenerate at start.ktor.io, or ktor new with the Ktor CLI
Quarkusquarkus 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-gradle checks the wrapper jar checksum automatically.
  • Use a version catalog (libs.versions.toml) and convention plugins in build-logic/ instead of copying configuration into every subproject or using allprojects {}.
  • 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=true and org.gradle.configuration-cache=true in gradle.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.properties or environment variables.
This post is licensed under CC BY 4.0 by the author.