Gradle version catalogs are a new feature of Gradle build system. They help managing dependency versions in the projects. In the previous article, we explored how to use them locally. This time, we are going to learn, how to share a single version catalog between many projects. This may be useful for teams that own several code repositories, and still want to bump versions in a coordinated way.

tl;dr;

You can publish a version catalog to a central repository and import it in other projects. To do so, you create a special Gradle project with version-catalog plugin, and a publication. Then, you can import it in another project, and (optionally) overwrite some versions.

Why a shared version catalog?

Gradle version catalogs can be used locally, within a single project. This is the natural starting point for us. It already solves many problems, such as sharing the same version between multiple libraries, or controlling the version of Kotlin or JVM from a central place. It’s also a great solution if we have many subprojects.

As the project grows, a single team may start owning more than one code repository. With local version catalogs, we end up again with bumping the same versions in many places. Fortunately, version catalogs can nicely scale up to this use case. This is what we can do with them:

  • move the version catalog to a common location and publish it e.g. to Artifactory,
  • import the shared version catalog into our project,
  • overwrite some versions locally, if the common version does not work well for one of the projects.

We are now going to learn, how to do it, and discuss some good practices.

Haven’t heard of Gradle version catalogs before?

If you are new to Gradle version catalogs, take a look at another article Manage your dependencies with Gradle version catalogs, before reading further. The later sections assume some knowledge, how to use them in local projects.

Sharing Gradle version catalogs in practice

The best place to start is an existing project that already uses a local version catalog. Sharing it is very easy, because the shared catalog uses exactly the same DSL. Therefore, all we need to do is copy-pasting it into some other place. Basically, we need to create a new Gradle project for keeping just the version catalog and publishing it to Artifactory. Then, in the local version catalogs of our actual project, we just import it.

Sample code

Find a complete example on Github: shared-version-catalog-demo and version-catalog-consumer-demo.

Creating a project for the shared catalog

My suggestion is to create a dedicated code repository for this project, since it is going to have its own history. Inside, we only need build.gradle.kts and settings.gradle.kts files, and nothing more. Here we can see the only DSL difference between the local and shared version catalog. Locally, we stored everything insite settings.gradle.kts file. This time, we’re going to put our definition directly into build.gradle.kts. To do so, we need to include a version-catalog plugin, together with some publication plugin:

plugins {
    `version-catalog`
    `maven-publish`
}

group = "tech.zone84.examples.sharedcatalog"
version = "1.0-SNAPSHOT"

repositories {
    mavenLocal()
}

catalog {
    versionCatalog {
        version("kotlin", "1.6.21")
        version("jvm", "17")

        plugin("kotlin-lang", "org.jetbrains.kotlin.jvm").versionRef("kotlin")
        plugin("kotlin-kapt", "org.jetbrains.kotlin.kapt").versionRef("kotlin")
        plugin("kotlin-allopen", "org.jetbrains.kotlin.plugin.allopen").versionRef("kotlin")

        library("kotlin-reflect", "org.jetbrains.kotlin", "kotlin-reflect")
           .versionRef("kotlin")
        library("kotlin-stdlib", "org.jetbrains.kotlin", "kotlin-stdlib-jdk8")
           .versionRef("kotlin")
        library("kotlinlogging", "io.github.microutils:kotlin-logging-jvm:2.1.23")
        library("logback", "ch.qos.logback:logback-classic:1.2.11")
    }
}

publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["versionCatalog"])
        }
    }
}

That’s the complete build script. Notice the catalog DSL element, provided by the version-catalog plugin. Why do we need a plugin? Basically, we need to tell Gradle what this project is about. It’s not a Java application, a library, but a version catalog. This is our output artifact. To build it, simply invoke publishMavenPublicationToMavenLocal task.

Warning

Note that I used the publication to Maven local in this example, to be able running it locally. In a typical company, you will likely use Artifactory or a similar repository.

Importing the version catalog

OK, we created a shared Gradle version catalog project, and published it. Now, we are going to import it into another project. Let’s open some Java/Kotlin project with the existing version catalog, and go to settings.gradle.kts. All we need to do here is simply changing the local list of versions to a single entry:

dependencyResolutionManagement {
    repositories {
        mavenLocal()
    }
   versionCatalogs {
      create("libs") {
         from("tech.zone84.examples.sharedcatalog:shared-version-catalog-demo:1.0-SNAPSHOT")
      }
   }
}

Of course, we also need repositories section. Otherwise, Gradle would not know, where to download our version catalog from!

Overwriting versions in shared version catalogs

Suppose that you moved all your projects A, B, and C to use a shared Gradle version catalog. You start bumping versions regularly, but after a while you notice a problem. One of the libs has a small bug that occurs in some corner case. It turns out that projects A and B don’t run into the issue, because they don’t use that feature. Sadly, project C is affected and the tests fail. What to do? Back off from the version bumping? Leave project C with an older version catalog? It would be a huge problem, if the whole migration were blocked by one dependency. Version catalog design notices that and offers the way to overwrite individual versions locally.

Let’s notice that in settings.gradle.kts we still create a local version catalog. All that we do is importing external stuff to it. This means that we can still use the version catalog DSL to add additional versions, or… overwrite them. For demo purposes, we are going to overwrite Kotlin version. The shared version catalog shown above uses Kotlin 1.6.21. But right now, Kotlin 1.7 is available. We need just one extra line to use it:

dependencyResolutionManagement {
    repositories {
        mavenLocal()
    }
   versionCatalogs {
      create("libs") {
         from("tech.zone84.examples.sharedcatalog:shared-version-catalog-demo:1.0-SNAPSHOT")
         version("kotlin", "1.7.0")
      }
   }
}

In the shared Gradle version catalog, the version called “kotlin” is used by both Kotlin plugins, and the standard library. If we overwrite just this single definition locally, Gradle applies the change to all the dependencies which use this version. This is the reason why it’s good to use version references, and dependency bundles in our version catalogs. It makes overwriting much easier.

Version management requires human attention

One common misconception is that tools for version management free humans from dealing with that. I can understand that. For many people, matching many numbers to work together is simply boring or leaves trauma. It’s true that tools like Dependabot aid humans in this process. But there will be always cases, where human attention is needed. The API may change. The default settings we relied on may change (this one is tricky to notice). The undocumented behavior may change. This is why I advise either to bump the versions by hand on regular basis, or combine the automated tools with manual bumping.

The role of tools

I think that the main role of automatic tools is responding quickly to security issues. This kind of bugs usually needs quick attention (especially if we are affected). Within the limits of semantic versioning, those tools can actually do the whole work for us, reducing the response time to the minimum. At the same time, those tools have their own limits. First of all, they can’t fix issues with API changes. Here, a real human needs to sit down, read the change log and apply the necessary changes manually. Regular version bumping prevents accumulating them.

Secondly, tools like Dependabot and Renovate don’t actually run Gradle. Instead, they parse the build files, find common constructs and do their own reasoning. This means that they may miss some dependency that in fact requires the attention. At the moment of writing this article, Renovate supports version catalogs, Dependabot – not yet. Why moving to version catalogs then, if we use such tools? Because they are a native Gradle solution, and they have much bigger chance to be supported correctly than in-house custom solutions for managing versions.

Version catalogs are not a silver bullet, too. They offer help by reducing repetitive tasks and avoiding certain mistakes. But they will not do the whole work for you. And there is no tool which will.

In short…

Don’t rely solely on automatic tools when bumping versions.

Summary

We can see that moving from local catalogs to a shared Gradle version catalog is an easy process. The DSL remains the same. We can also deal with corner cases thanks to version overriding. One more question may arise at the end. What should be the right granulation of version catalogs? I suggest the teams to maintain their own catalogs for their own projects. This way supports the team autonomy. In addition, certain company-wide projects can also publish thematic version catalogs (e.g. a common customization to the framework). I would not definitely go into a company-wide version catalog. At some level of centralization, the growing number of local version overwrites may defeat the profits of keeping versions in a single place.

Sample code

Find a complete example on Github: shared-version-catalog-demo and version-catalog-consumer-demo.

Subscribe
Notify of
guest

0 Comments
Inline Feedbacks
View all comments