How to publish a Kotlin/Java Spring Boot library with Gradle to Maven Central - Complete Guide
Tags: gradle, java, kotlin, springboot
Published: 2023-11-08 12:03:35

This is an opinionated **step-by-step guide** on how to **publish a Kotlin/Java library with Gradle to Maven Central**
repository.

It assumes that:

- the project is built with **Gradle** (look at Maciej Guide if you want to do it with Maven)
- the project code is hosted on **GitHub** and **GitHub Actions** are used to trigger the release

It uses [JReleaser](https://jreleaser.org/) - I believe this is the simplest and the most straightforward way of signing
and uploading artifacts.

This guide is based on the excellent
article [How to publish a Java library to Maven Central - Complete Guide](https://maciejwalkowiak.com/blog/guide-java-publish-to-maven-central/)
but uses Gradle instead of Maven.

1. [Create an account in Sonatype JIRA](#1-create-an-account-in-sonatype-jira)
2. [Create a "New Project" ticket](#2-create-a-new-project-ticket)
    1. [If a custom domain is used as a group id](#21-if-a-custom-domain-is-used-as-a-group-id)
    2. [If GitHub is used as a group id](#22-if-github-is-used-as-a-group-id)
    3. [Set ticket to "Open"](#23-set-ticket-to-open)
3. [Create GPG keys](#3-create-gpg-keys)
    1. [Export key to a key server](#31-export-key-to-a-key-server)
4. [Export public and secret key to GitHub secrets](#4-export-public-and-secret-key-to-github-secrets)
    1. [Create GitHub secrets with UI](#41-create-github-secrets-with-ui)
    2. [Create secrets with GitHub CLI](#42-create-secrets-with-github-cli)
5. [Adjust pom.xml](#5-adjust-pomxml)
    1. [Generate javadocs and sources JARs](#51-generate-javadocs-and-sources-jars)
    2. [Configure JReleaser Maven Plugin](#52-configure-jreleaser-maven-plugin)
6. [Create a GitHub action](#6-create-a-github-action)
7. [Get familiar with Sonatype Nexus UI](#7-get-familiar-with-sonatype-nexus-ui)
8. [When is the library actually available to use?](#8-when-is-the-library-actually-available-to-use)

## 1\. Create an account in Sonatype JIRA

[Sign up in Sonatype JIRA](https://issues.sonatype.org/secure/Signup!default.jspa).

You do it **only once** - no matter how many projects you want to release or how many group ids you own.

## 2\. Create a "New Project" ticket

[Create a "New Project ticket](https://issues.sonatype.org/secure/CreateIssue.jspa?pid=10134&#x26;issuetype=21) in
Sonatype JIRA.

This step is done **once per group id**. Meaning, for each domain you want to use as a group id - you must create a new
project request.

Although the official Sonatype guide claims that normally, the process takes less than 2 business days. In my case it
took just a few minutes.

Once the ticket is created, a Sonatype JIRA bot will post comments with instructions what to do next:

![SonaType Bot Comment](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nlkojhvjgc39qq5ig5wh.png)


## 2.1. If a custom domain is used as a group id

When you want to use a domain like `de.tschuehly` as a group id - you must own the domain - and be able to prove it. *
*You must add a DNS TXT record with a JIRA ticket id** to your domain - this is done in the admin panel where your
domain is hosted.

Once you have added the record, verify that it is added with the following command:

```
$ dig -t txt tschuehly.de
```


![dig -t txt output](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4err0h7wqybwekpu9s3f.png)


## 2.2. If GitHub is used as a group id

If you don't own the domain it is possible to use your GitHub coordinates as a group id. For example, my GitHub account
name is `tschuehly`, so I can use `io.github.tschuehly` as a group id.

To prove that you own that GitHub account, create a temporary repository with a name reflecting the JIRA ticket id.

This can be done via [github.com/new](https://github.com/new) or with GitHub CLI:

```
$ gh repo create OSSRH-91026 --public
```

## 2.3. Set ticket to "Open"

The comment posted by Sonatype bot says that once you are done with either creating a DNS record or creating a GitHub
repository, "Edit this ticket and set Status to Open.".

I did not find any way to change status to "Open" in the edit form, but instead I had to click one of the buttons at the
top of JIRA ticket, right next to "Agile Board" and "More" (unfortunately I did not make a screenshot on time).

Once you do it, another comment will be posted by Sonatype bot:

![Sonatype Bot success message](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/awn0qsjf5ltcoikcvzg6.png)


This means that our job in the Sonatype JIRA is done. Congratulations 🎉

(you can now drop the temporary GitHub repository if you've created one)

## 3\. Create GPG keys

Artifacts sent to Maven Central must be signed. To sign artifacts you need to generate **GPG keys**.

**This must be done only once** - all artifacts you publish to Maven Central can be signed with the same pair of keys.

Create a key pair with:

```
$ gpg --gen-key
```

Put your name, email address and passphrase.

List keys with command:

```
$ gpg --list-keys
```

You will see output like this:

```
pub   ed25519 2022-11-05 [SC] [expires: 2024-11-04]
      05342E4134D1F7C1B08F900FC2377C0DD0494024
uid           [ultimate] john@doe.com
sub   cv25519 2022-11-05 [E] [expires: 2024-11-04]
```

In this example - `05342E4134D1F7C1B08F900FC2377C0DD0494024` is the key id. Find your own key id and copy it to
clipboard.

If you can't find it, you probably used a wrong version of gpg. It didn't work on my Windows machine but worked on my
linux server

## 3.1 Export key to a key server

Next you need to export the public key to a key server with command:

```
$ gpg --keyserver keyserver.ubuntu.com --send-keys yourKeyId
```

## 4\. Export public and secret key to GitHub secrets

JReleaser needs public and secret key to sign artifacts. Since signing will be done by a GitHub action, you need to
export these keys as GitHub secrets.

Secrets can be set either on the GitHub repository website or with a GitHub CLI.

## 4.1. Create GitHub secrets with UI

Go to repository `Settings`:


![GitHub secrets ui](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/x7vqs69484fwp7qdhn37.png)


Create a repository secret `JRELEASER_GPG_PUBLIC_KEY` with a value from running:

```
$ gpg --export yourKeyId | base64
```

Create a key `JRELEASER_GPG_SECRET_KEY` with a value from running:

```
$ gpg --export-secret-keys yourKeyId | base64
```

Create a key `JRELEASER_GPG_PASSPHRASE` with a value that is a passphrase you used when creating your gpg key.

Two more secrets unrelated to GPG are needed to release to Maven Central:

Create a key `JRELEASER_NEXUS2_USERNAME` with the username you use to log in to Sonatype JIRA.

Create a key `JRELEASER_NEXUS2_PASSWORD` with the password you use to log in to Sonatype JIRA.

## 4.2. Create secrets with GitHub CLI

If you choose to use the CLI instead, run the following commands (replace things in < brackets > with real values) from the
directory where your project is cloned:

``` 
$ gh secret set JRELEASER_GPG_PUBLIC_KEY -b $(gpg --export <key id> | base64)
$ gh secret set JRELEASER_GPG_SECRET_KEY -b $(gpg --export-secret-keys <key id> | base64)
$ gh secret set JRELEASER_GPG_PASSPHRASE -b <passphrase>
$ gh secret set JRELEASER_NEXUS2_USERNAME -b <sonatype-jira-username>
$ gh secret set JRELEASER_NEXUS2_PASSWORD -b <sonatype-jira-password>
```

## 5\. Create publishing config

Here is an example config you can adjust to your needs:

```
publishing{
  publications {
    create<MavenPublication>("Maven") {
      from(components["java"])
      groupId = "de.tschuehly"
      artifactId = "spring-view-component-thymeleaf"
      description = "Create server rendered components with thymeleaf"
    }
    withType<MavenPublication> {
      pom {
        packaging = "jar"
        name.set("spring-view-component-thymeleaf")
        description.set("Spring ViewComponent Thymeleaf")
        url.set("https://github.com/tschuehly/spring-view-component/")
        inceptionYear.set("2023")
        licenses {
          license {
            name.set("MIT license")
            url.set("https://opensource.org/licenses/MIT")
          }
        }
        developers {
          developer {
            id.set("tschuehly")
            name.set("Thomas Schuehly")
            email.set("thomas.schuehly@outlook.com")
          }
        }
        scm {
          connection.set("scm:git:git@github.com:tschuehly/spring-view-component.git")
          developerConnection.set("scm:git:ssh:git@github.com:tschuehly/spring-view-component.git")
          url.set("https://github.com/tschuehly/spring-view-component")
        }
      }
    }
  }
  repositories {
    maven {
    	url = layout.buildDirectory.dir("staging-deploy").get().asFile.toURI()
    }
  }
}
```

## 5.1. Generate javadocs and sources JARs

Artifacts uploaded to Maven Central must have two extra jars: one with sources and one with Javadocs. Both are created
by Gradle.

```
java {
  withJavadocJar()
  withSourcesJar()
}

tasks.jar{
  enabled = true
  // Remove `plain` postfix from jar file name
  archiveClassifier.set("")
}
```

## 5.2 Configure JReleaser Maven Plugin

JReleaser can be invoked either as a standalone CLI application or a Gradle Plugin. To use the Gradle Plugin you need to add these plugins:

```
plugins {
  id("maven-publish")
  id("org.jreleaser") version "1.5.1"
  id("signing")
}
```

Add following plugin configuration to the plugins section of the `release` profile:

```
jreleaser {
  project {
    copyright.set("Thomas Schuehly")
  }
  gitRootSearch.set(true)
  signing {
    active.set(Active.ALWAYS)
  	armored.set(true)
  }
  deploy {
  	maven {
  	  nexus2 {
  	    create("maven-central") {
  	      active.set(Active.ALWAYS)
  	      url.set("https://s01.oss.sonatype.org/service/local")
  	      closeRepository.set(true)
  	      releaseRepository.set(true)
  	      stagingRepositories.add("build/staging-deploy")
  	    }  
  	  }
    }
  }
}
```

I recommend to set temporarily `closeRepository` and `releaseRepository` to `false`. At the end once you successfully
release the first version to staging repository in Sonatype Nexus you can switch it to `true`.

## 6\. Create a GitHub action

The GitHub action will trigger the release each time a tag that starts with `v` is created, like `v1.0`, `v1.1` etc.

Create a file in your project directory under `.github/workflows/release.yml`:

```
name: Publish package to the Maven Central Repository
on:
  push:
    tags:
      - v*
  pull_request:
    branches: [ main ]
jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Set up Java
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'adopt'
      - name: Run chmod to make gradlew executable
        run: chmod +x ./gradlew
      - name: Publish package to local staging directory
        run: ./gradlew :publish
      - name: Publish package to maven central
        env:
          JRELEASER_NEXUS2_USERNAME: ${{ secrets.JRELEASER_NEXUS2_USERNAME }}
          JRELEASER_NEXUS2_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_PASSWORD }}
          JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
          JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
          JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}
          JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: ./gradlew :jreleaserDeploy -DaltDeploymentRepository=local::file:./build/staging-deploy
```

Adjust the Java version and the distribution to your needs.

The action will [stage artifact](https://jreleaser.org/guide/latest/examples/maven/staging-artifacts.html) and then
run `jreleaser:deploy` goal to publish artifact to Sonatype Nexus.

## 7\. Get familiar with Sonatype Nexus UI

Once you create and push first tag and the GitHub Action finishes with success, you can log in
to [Sonatype Nexus](https://s01.oss.sonatype.org/) **with your Sonatype JIRA credentials** to preview your staging
repository.

In the `Staging Profiles` section you will see all the group ids you own:

![Sonatype Staging Profiles](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/is5e7n3eu4o9d2dtah8h.png)


If you set `closeRepository` and `releaseRepository` to `false` in JReleaser configuration, in
the `Staging Repositories` section you will see an entry for the version that was released with a GitHub action:

![Sonatype Staging Repositories](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/eo5rxtvwo8csj148ijqx.png)
(image
from [https://help.sonatype.com/repomanager2/staging-releases/managing-staging-repositories](https://help.sonatype.com/repomanager2/staging-releases/managing-staging-repositories))


The first time I did it I needed to wait a long time and there were quite a few timeouts.

Here you can `Close` the repository and `Release`. Both actions trigger series of verifications - if your `gradle.build.kts`
meets criteria, if packages are properly signed, if your GPG key is uploaded to the key server.

I recommend triggering these actions manually for the first version you release just to see if everything is fine. Once
the `Release` action finishes with success, your library is considered as **published to Maven Central**.
Congratulations 🎉

You can now set `closeRepository` and `releaseRepository` to `true` in JReleaser configuration.

## 8\. When is the library actually available to use?

The library is not immediately available after it is released. Official documentation says that it may take up to 30
minutes before the package is available, some folks claim that it can take few hours. In my case it took just 10
minutes.

Now your artifact can be referenced in `build.gradle.kts` and Gradle will successfully download it. If you try to do it before it
is available, Gradle will mark this library as unavailable and will not try to re-download it until the cache expires.
Use `--refresh-dependencies` flag to `.\gradlew` command to force Gradle to check for updates:

```
$ ./gradlew build --refresh-dependencies
```

Don't be fooled by the results in [search.maven.org](https://search.maven.org/)
or [mvnrepository.com](https://mvnrepository.com/). Here your artifact or even a new version of the artifact will appear
after around 24 hours.

## Conclusion

I hope this guide was useful, and it helped you to release a library to Maven Central.

If you find anything unclear drop me a message on [Twitter](https://twitter.com/tschuehly).

Most of this guide is based on [Maciej Walkowiak](https://twitter.com/maciejwalkowiak) excellent guide so drop him a thanks!

I would like to thank [Andres Almiray](https://twitter.com/aalmiray) for creating JReleaser. This library significantly simplifes the whole process to the point that it's not terribly overcomplicated anymore.