We are excited to announce Gradle 9.4.0-20260120212845+0000 (released 2026-01-20).
This release features 1, 2, ... n, and more.
We would like to thank the following community members for their contributions to this release of Gradle: Niels Doucet, Ujwal Suresh Vanjare.
Be sure to check out the public roadmap for insight into what's planned for future releases.
Switch your build to use Gradle 9.4.0-20260120212845+0000 by updating the wrapper in your project:
./gradlew wrapper --gradle-version=9.4.0-20260120212845+0000 && ./gradlew wrapper
See the Gradle 9.x upgrade guide to learn about deprecations, breaking changes, and other considerations when upgrading to Gradle 9.4.0-20260120212845+0000.
For Java, Groovy, Kotlin, and Android compatibility, see the full compatibility notes.
When downloading Gradle distributions from an HTTPS backend (or even from an HTTP one, but the secure version is preferred), the Wrapper now also supports Bearer token authentication. This is in addition to Basic authentication (username and password), which was the only supported method in previous versions.
Bearer tokens can be specified via system properties and take precedence over Basic authentication, if both configured.
Both Basic authentication and Bearer token authentication can now be configured on a per-host basis, which is the recommended approach for avoiding leaking credentials to unintended hosts.
See the Wrapper documentation for further details.
Gradle's progress bars are now more compatible with modern terminal environments: Unicode characters are now used to render the progress bar where supported.

The incubating Problems HTML report has been refined to provide a more useful user experience.
The summary clearly display the number of problems without location or skipped for performance reasons. Each tab starts with collapsed trees to show a clear view of the root nodes on load. Locations and solutions nodes are expanded by default, reducing the number of clicks necessary to see useful information. Everything is sorted alphabetically and by location. Problem details are displayed with a monospaced font to preserve the alignment of multi-line messages. Duplicate information is reduced across the board for a better readability. The size of the report file is reduced.
Printing a link to the report at the end of the build can now be influenced via the org.gradle.warning.mode Gradle property. If the mode is set to none, the report is still generated but a link is omitted from the build output.
Gradle now allows listening for test metadata events during test execution. In the exact same manner as TestOutputListener, a TestMetadataListener can be registered to receive metadata events emitted by the test framework during via the new Test#addTestMetadataListener(TestMetadataListener) method.
class LoggingListener(val logger: Logger) : TestMetadataListener {
override fun onMetadata(descriptor: TestDescriptor , event: TestMetadataEvent) {
logger.lifecycle("Got metadata event: " + event.toString())
}
}
tasks.named<Test>("test").configure {
addTestMetadataListener(LoggingListener())
}
This addition enables support for additional JUnit Platform features, and allows tests to communicate additional information back to the process running the tests in a more structured manner than just logging to the standard output or error streams.
Daemon logs older than 14 days are now automatically cleaned up when the daemon shuts down, eliminating the need for manual cleanup.
See the daemon documentation for more details.
When using the Build Init Plugin to generate a Gradle build from an existing Maven project, Gradle now imports <exclusion> elements from the Maven POM and translates them into Gradle dependency exclusions.
<dependencies>
<dependency>
<groupId>sample.Project</groupId>
<artifactId>Project</artifactId>
<version>1.0</version>
<exclusions>
<exclusion>
<groupId>excluded.group</groupId>
<artifactId>excluded-artifact</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
Due to differences in how Maven and Gradle handle dependency exclusions, some exclusions may not translate perfectly.
The generated exclusions will be marked with a comment noting they require manual verification:
dependencies {
implementation("some.group:some-artifact:1.0") {
// TODO: This exclude was sourced from a POM exclusion and is NOT exactly equivalent
exclude(group = "excluded.group", module = "excluded-artifact")
}
}
Configuration.extendsFrom accepts ProvidersPreviously, calling extendsFrom() on a Configuration required the specified parent configuration to be a realized Configuration object.
It is now possible to specify a Provider<Configuration> instead, which does not require that a registered configuration be realized before specifying it as a parent configuration.
configurations {
val parent = dependencyScope("parent")
val child = resolvable("child") {
extendsFrom(parent) // previously required 'parent.get()'
}
The PMD plugin now supports generating reports in CSV, Code Climate, and SARIF formats in addition to the existing XML and HTML formats.
They are not enabled by default, but can be configured as follows:
// Note that report configuration must be done on the `Pmd` task (here `pmdMain`), not the `pmd` extension.
tasks.pmdMain {
reports {
csv.required = true
// Optional, defaults to "<project dir>/build/reports/pmd/main.csv"
csv.outputLocation = layout.buildDirectory.file("reports/my-custom-pmd-report.csv")
codeClimate.required = true
// Optional, defaults to "<project dir>/build/reports/pmd/main.codeclimate.json"
codeClimate.outputLocation = layout.buildDirectory.file("reports/my-custom-codeclimate-pmd-report.json")
sarif.required = true
// Optional, defaults to "<project dir>/build/reports/pmd/main.sarif.json"
sarif.outputLocation = layout.buildDirectory.file("reports/my-custom-sarif-pmd-report.json")
}
}
For plugin builds that apply any of the com.gradle.plugin-publish, ivy-publish, or maven-publish plugins, Gradle now automatically enables stricter validation of plugin code.
In order not to break your builds, this does not apply to local plugins (in buildSrc or included builds containing build logic). However, we encourage you to always enable stricter validation:
tasks.validatePlugins {
enableStricterValidation = true
}
Plugin builds that use the java-gradle-plugin can now register each plugin with less ceremony. The plugin ID is now set to the registration's name by default:
gradlePlugin {
plugins {
register("my.plugin-id") {
implementationClass = "my.PluginClass"
}
}
}
See the Java Gradle Plugin plugin documentation for more information.
This release adds a few enhancements to the built-in Tooling API models:
gradle --version without starting a daemon, via the new BuildEnvironment.getVersionInfo() property.Help model exposes the output of the gradle --help command-line build invocation.For example:
import org.gradle.tooling.GradleConnector;
import org.gradle.tooling.ProjectConnection;
import org.gradle.tooling.model.build.BuildEnvironment;
import org.gradle.tooling.model.build.Help;
import java.io.File;
void main() {
var projectDir = new File("/path/to/project");
try (var conn = GradleConnector.newConnector().forProjectDirectory(projectDir).connect()) {
System.out.println("--version:\n + " + conn.getModel(BuildEnvironment.class).getVersionInfo());
System.out.println("--help:\n" + conn.getModel(Help.class).getRenderedText());
}
}
A new Gradle property org.gradle.tooling.parallel allows explicitly controlling whether Tooling API clients can run actions against the build in parallel. This is particularly relevant for the IDE Sync scenarios, where IDEs can take advantage of the parallelism to improve performance.
# gradle.properties
org.gradle.tooling.parallel=true
Historically, this was only controlled by the org.gradle.parallel property, which is often used to get parallel task execution. However, previously it was not possible to enable or disable one without affecting the other.
With this release, Gradle supports Java 26. This means you can now use Java 26 for the daemon in addition to toolchains. Third-party tool compatibility with Java 26 may still be limited.
See the compatibility documentation for more details.
When testing using JUnit Platform, Gradle can now discover and execute tests that are not defined in classes.
JUnit Platform TestEngines are capable of discovering and executing tests defined in arbitrary formats, extending testing beyond the confines of JVM classes. However, Gradle's Test task requires test classes to be present; otherwise execution fails with a message:
There are test sources present and no filters are applied, but the test task did not discover any tests to execute.
In this release, tests can be defined in whatever format is understood by the configured TestEngine. Gradle no longer requires a test class be present to “unlock” test execution.
For example, this library project structure doesn't use typical class-based testing, but instead uses XML test definitions understood by a custom TestEngine:
my-lib/
├── src/
│ ├── main/
│ │ └── test/
│ └── test/
│ └── definitions/
│ ├── some-tests.xml
│ ├── some-other-tests.xml
│ └── sub/
│ └── even-more-tests.xml
└── build.gradle.kts
testing.suites.named("test", JvmTestSuite::class) {
useJUnitJupiter()
dependencies {
implementation("...") // Library containing custom TestEngine
}
targets.all {
testTask.configure {
testDefinitionDirs.from("src/test/definitions") // Conventional non-class-based test definitions location
}
}
}
This feature works both with and without using JvmTestSuites.
We recommend storing non-class test definitions in the conventional location src/<TEST_TASK_NAME>/definitions to keep builds using this feature structured similarly; however, any location can be used.
For more information, see the section on Non-Class-Based Testing in the User Manual.
TestEngines such as Cucumber previously required workarounds when testing with Gradle, such as creating an empty @Suite class, or using a JUnit extension like @RunWith(Cucumber.class) to satisfy Gradle's class-based test discovery requirement.
These non-class-based tests can now be run directly without workarounds:
testing.suites.named("test", JvmTestSuite::class) {
useJUnitJupiter()
dependencies {
implementation("io.cucumber:cucumber-java:7.15.0")
runtimeOnly("io.cucumber:cucumber-junit-platform-engine:7.15.0")
}
targets.all {
testTask.configure {
testDefinitionDirs.from("src/test/resources") // Conventional Cucumber *.feature files location
}
}
}
During test execution, JUnit Platform tests can emit additional data such as file attachments or arbitrary key–value pairs using the TestReporter API.
For example:
@Test
void someTestMethod(TestReporter testReporter) {
testReporter.publishEntry("myKey", "myValue");
testReporter.publishFile("test1.txt", MediaType.TEXT_PLAIN_UTF_8, file -> Files.write(file, List.of("Test 1")));
// Test logic continues...
}
Gradle now captures this additional data and includes it in both the HTML test report and the XML test results.
In the HTML test report, when such data is published during a test, two new tabs are shown alongside stdout and stderr:
In the JUnit XML report, the data is represented as:
ReportEntry values as <properties/>FileEntry values as [[ATTACHMENT|/path/to/file]], following conventions used by Jenkins, Azure Pipelines, and GitLabThis information is captured for both class-based and non-class-based tests, and includes data published during test construction as well as setup/teardown phases.
The Configuration Cache improves build time by caching the result of the configuration phase and reusing it for subsequent builds. This feature can significantly improve build performance.
Identifying the source of configuration cache violations can be challenging when a task contains multiple lambdas or closures. Common examples include task actions like doFirst/doLast, or task predicates such as onlyIf, upToDateWhen, and cacheIf/doNotCacheIf. Previously, if one of these closures captured an unsupported type (such as a reference to the enclosing script), the problem report was often ambiguous:
fun myFalse() = false
fun noOp() { }
tasks.register("myTask") {
outputs.cacheIf { myFalse() }
outputs.doNotCacheIf("reason") { myFalse() }
outputs.upToDateWhen { myFalse() }
onlyIf { myFalse() }
doLast { noOp() }
}
In earlier versions, the report would reference a cryptic generated class name, leaving you to guess which specific block was the culprit:

Starting with this release, the Configuration Cache report now explicitly identifies the type of action or spec associated with each lambda. This provides the necessary context to pinpoint and fix the violation immediately:

Promoted features are features that were incubating in previous versions of Gradle but are now supported and subject to backward compatibility. See the User Manual section on the "Feature Lifecycle" for more information.
The following are the features that have been promoted in this Gradle release.
getSettingsDirectory() in ProjectLayoutThe task graph, introduced as an incubating feature in Gradle 9.1.0, is now stable. It's no longer marked as experimental.
Known issues are problems that were discovered post-release that are directly related to changes made in this release.
We love getting contributions from the Gradle community. For information on contributing, please see gradle.org/contribute.
If you find a problem with this release, please file a bug on GitHub Issues adhering to our issue guidelines. If you're not sure if you're encountering a bug, please use the forum.
We hope you will build happiness with Gradle, and we look forward to your feedback via Twitter or on GitHub.