Gradle (Android, Kotlin, Java)
The gocdnext/gradle
plugin wraps a JDK + Gradle wrapper image. Tests run, the artefact
is assembled, JUnit XML lands in the Tests tab, the build cache +
configuration cache + dependency cache all survive across runs so
warm builds drop to seconds.
Layout assumed
repo/├── settings.gradle.kts (or .gradle)├── build.gradle.kts├── gradlew # wrapper script├── gradle/wrapper/...├── app/ # one or more subprojects│ ├── build.gradle.kts│ └── src/...└── build/ # generatedThe plugin always uses ./gradlew (the wrapper) so the Gradle
version is locked by the repo, not the agent’s image — pin the
wrapper, pin the build.
The pipeline
name: ci
when: event: [push, pull_request]
stages: [test, build]
jobs: check: stage: test uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1 with: command: check --no-daemon --build-cache cache: - key: gradle-${CI_COMMIT_BRANCH} paths: - .gradle-user-home - .gradle-cache test_reports: - "**/build/test-results/test/*.xml" artifacts: optional: - "**/build/reports/jacoco/test/jacocoTestReport.xml" - "**/build/reports/tests/test/index.html"
assemble: stage: build uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1 needs: [check] with: command: assemble --no-daemon --build-cache -x test cache: - key: gradle-${CI_COMMIT_BRANCH} paths: - .gradle-user-home - .gradle-cache artifacts: paths: - "app/build/libs/*.jar" - "**/build/distributions/*"What’s worth highlighting:
--no-daemon is mandatory in CI
The Gradle daemon was designed for IDE workflows where the JVM
stays warm across invocations. In CI, where each job runs in a
fresh container that’s destroyed at the end, the daemon does the
opposite of what you want: it spawns, hangs around, holds locks
on the cache, and never serves a second request. --no-daemon
keeps each invocation single-shot.
Two cache paths
gradle/ plugin redirects two locations into the workspace so the
platform’s cache: block can tar both:
.gradle-user-home— the dependency cache (GRADLE_USER_HOME). Equivalent to~/.gradle/. Big — typically 300-500 MB..gradle-cache— the build cache (--build-cacheoutput). The task-output memo: if your inputs haven’t changed, Gradle pulls the output bytes from here instead of re-running the task.
Warm builds with both populated hit the cache for compilation, test fixtures, resource processing — a 3-minute cold build often drops to under 30 seconds.
--build-cache
Off by default in older Gradle versions, on by default in 8.x. The flag is harmless to leave in either way and turns off-by- default surprises into a no-op.
-x test on assemble
assemble re-runs test by default. We already ran it in the
check job, and the artefacts cache means the compiled classes
are reused. Skipping the duplicate test run cuts the assemble
job by half.
Variations
Android app — release build with bundle
bundle: stage: build uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1 needs: [check] with: command: bundleRelease --no-daemon --build-cache cache: - key: gradle-${CI_COMMIT_BRANCH} paths: [.gradle-user-home, .gradle-cache] secrets: - ANDROID_KEYSTORE_BASE64 - ANDROID_KEY_ALIAS - ANDROID_KEY_PASSWORD artifacts: paths: - "app/build/outputs/bundle/release/*.aab" - "app/build/outputs/mapping/release/mapping.txt"Decode the keystore in app/build.gradle.kts from the env var, or
use the signingConfig block — gocdnext’s secrets layer masks the
base64 string in logs.
Multi-module, parallel
check: stage: test uses: ghcr.io/klinux/gocdnext-plugin-gradle@v1 with: command: check --no-daemon --build-cache --parallel ...--parallel lets independent subprojects run in parallel. Most
modern Gradle multi-module builds are configured for this; the
flag’s a no-op when project dependencies serialise the work.
Convention plugins / build-logic
If you use the build-logic/ convention pattern, the build cache
benefits compound — convention compilation is a major chunk of cold
builds, and warm runs skip it entirely.
Common pitfalls
org.gradle.parallel=trueingradle.propertiesis fine, but combine it withorg.gradle.workers.max=Nto bound the JVM count under CI memory limits. Default =Runtime.availableProcessorswhich on a 4-vCPU agent with 8 GiB RAM thrashes.org.gradle.daemon=trueingradle.propertiesis silently overridden by--no-daemon— you don’t need to remove it, just know which wins.- Configuration cache (
--configuration-cache) is great but finicky on legacy plugins. Adopt incrementally; if you hit a cache-incompatible plugin, remove the flag rather than disabling the cache for the whole build.