Dev Publish is a Gradle plugin that supports functional testing of a published module.
Each subproject is published to a local file-based Maven repository, containing real publications (Gradle Module Metadata, Plugin Marker Artifacts, checksums) that your tests can resolve.
- A project-local repository for testing only your publications, instead of Maven Local.
"You should avoid adding mavenLocal () as a repository". - Perfect for testing Gradle plugins and
Plugin Marker Artifacts,
instead of TestKit +
withRuntimeClasspath(). - Avoids unnecessary re-publishing - a content checksum means nothing is republished unless it actually changed, even for SNAPSHOT versions.
- Supports Gradle Build Cache, Configuration Cache, and Isolated Projects.
- Gradle: 8.14.5+
- Gradle JVM: 17+
- Kotlin: 2.1+, to compile tests that use
dev-publish-utils. A Gradle plugin project uses Gradle's embedded Kotlin, so it needs Gradle 9+. - Required plugins:
maven-publish, in every project that publishes.
Apply maven-publish and Dev Publish to any subproject that publishes Maven artifacts, using the
plugin ID dev.adamko.dev-publish and
the latest version.
plugins {
kotlin("jvm")
`maven-publish`
id("dev.adamko.dev-publish")
}
group = "com.example"
version = "1.0.0"
description = "A library that does something."
// This project is tested by the `:functional-tests` project.
publishing {
publications {
create<MavenPublication>("maven") {
from(components["java"])
}
}
}See the full code here.
Any project can collect publications from other subprojects into its own dev repo, by declaring a
devPublication(project("...")) dependency.
plugins {
kotlin("jvm")
id("dev.adamko.dev-publish")
}
dependencies {
devPublication(project(":lib-extras"))
devPublication(project(":signed-library"))
testImplementation(kotlin("test"))
// Makes the `test` source set depend all projects declared as `devPublication()`.
testImplementation(devPublish.dependency())
}See the full code here.
Declaring devPublish.dependency() runs updateDevRepo before the tests, puts the dev Maven
repository location on their runtime classpath, and adds dev-publish-utils for reading it.
Important
Dev Publish adds the current project as a default devPublication() dependency.
Adding another devPublication() dependency will replace the default,
so a project that collects other projects and also wants its own publications must declare itself as well.
For example, in :lib-extras:
// lib-extras/build.gradle.kts
dependencies {
devPublication(project(":lib-core")) // some other subproject.
// must re-add this project, if it's required for testing.
devPublication(project())
}This lets a project collect other projects while leaving out its own publications, such as a
test-only project like :functional-tests above.
Note: Gradle 9.4 or below must use project's own path, because project() is only available on Gradle 9.5+.
// lib-extras/build.gradle.kts
dependencies {
devPublication(project(":lib-core")) // some other subproject.
// must re-add this project, if it's required for testing.
devPublication(project(":lib-extras"))
}devPublication dependencies are shared with consumers. If :lib-extras declares
devPublication(project(":lib-core")), then anyone declaring devPublication(project(":lib-extras"))
gets :lib-core's publications too, without naming :lib-core.
That is the default because a dev repo is only useful if it is closed. :lib-extras' published POM
and Gradle Module Metadata name :lib-core, so a test resolving :lib-extras from the dev repo
fails unless :lib-core is there as well. This applies to implementation dependencies too - they
appear in the POM at runtime scope.
To collect a project without its own devPublication dependencies, make the dependency
non-transitive:
dependencies {
devPublication(project(":lib-extras")) { isTransitive = false }
}This prunes every level at once, so anything else that is still needed must be named explicitly.
See the multi-project-aggregation example.
Important
Every project named in a devPublication dependency must itself apply Dev Publish. Declaring a
dependency on a project that does not will fail with a variant-resolution error.
subprojects { devPublication(it) } and allprojects { devPublication(it) } is not supported.
Dev Publish will automatically update the dev repository to a local, isolated directory before running gradle test.
Once devPublish.dependency() is configured use the dev-publish-utils helper library in your tests.
@Test
fun `dev repo contains requested publications`() {
val devRepo: Path = devMavenRepo()
assertFileExists(devRepo.resolve("com/example/lib-extras/1.0.0/lib-extras-1.0.0.jar"))
assertFileExists(devRepo.resolve("com/example/signed-library/1.0.0/signed-library-1.0.0.jar"))
// lib-core is only reachable via lib-extras' devPublication dependency
assertFileExists(devRepo.resolve("com/example/lib-core/1.0.0/lib-core-1.0.0.jar"))
}See the full code here.
For a project in an included build,
use module notation rather than project(...):
dependencies {
devPublication("com.example:data-model")
}Because the dev repo contains the real publication (including the
Plugin Marker Artifact)
the build under test can apply your plugin by id and version, exactly as a user would, instead of
using withPluginClasspath().
devPublish.dependency() also brings in the helpers that write the build under test's
settings.gradle.kts:
@Test
fun `plugin can be resolved by id and version`(@TempDir projectDir: Path) {
// Writes a settings file that resolves the plugin under test from the dev Maven repo.
projectDir.resolve("settings.gradle.kts").writeText(
devPublishSettings(rootProjectName = "greeting-consumer")
)
// ...
}See the full code here.
For finer control, devPublishRepository(), devPublishPluginManagement() and
devPublishDependencyResolutionManagement() return the individual maven(devPublishRepo()) blocks.
See the gradle-plugin-testkit example.
The
signing plugin
attaches signatures to a MavenPublication, not to an individual repository, so signatures cannot be
filtered out per-repository. A project that signs its publications therefore needs a GPG key just to
run its functional tests.
To avoid that, only require signing when the build is actually publishing somewhere else
by using signing { setRequired(...) }.
Dev Publish adds a helper property, publishingOutsideDevRepo, to the signing {} block to help:
signing {
setRequired(publishingOutsideDevRepo)
}Combine it with your own conditions if you have them:
val signingCredentialsPresent: Property<Boolean> = TODO("check if credentials are present")
signing {
setRequired { publishingOutsideDevRepo.get() || signingCredentialsPresent.get() }
}Example projects are available in the examples/ directory.