Publishing Android libraries to MavenCentral in 2021
Tags: android, library, mavencentral
Published: 2021-04-21 14:29:27

# Introduction

> This is an updated version of an article published two years ago on [blog.autsoft.hu](https://blog.autsoft.hu/publishing-an-android-library-to-mavencentral-in-2019/). This new version supports non-Android libraries, uses command line GPG handling instead of a specific Windows GUI app, includes new best practices, and uses GitHub Actions for its CI integration.

**Update, April 2021: addressed [new Sonatype infra](https://central.sonatype.org/news/20210223_new-users-on-s01/), moved to using [AGP Maven Publish plugin integration](https://developer.android.com/studio/build/maven-publish-plugin) for release components, and to using the new [gradle-nexus/publish-plugin](https://github.com/gradle-nexus/publish-plugin/).**

**Update, July 2021: fixed keyserver being used ([thanks Chetan Gupta](https://twitter.com/Ch8n2/status/1412782176702058496)), moved to using in-memory GPG keys for signing ([thanks Arkadii Ivanov](https://twitter.com/arkann1985/status/1412751173698965510)).**

**Update, July 2021: use JDK11 in GitHub Actions configuration to support new AGP versions.**

---

Creating a great library is hard work. Coming up with the idea, implementing it, making sure you have a nice, stable public API that you [control carefully](https://zsmb.co/mastering-api-visibility-in-kotlin/) and [maintain](https://zsmb.co/maintaining-compatibility-in-kotlin-libraries/)… That’s already lots to do.

After all that, you need to make your library available to the public. Technically, you could distribute the `.aar` file any way you want, but the norm is publishing it to a publicly available Maven repository. It’s a good idea to use one of the well-established repositories that people are already likely to have in their projects, to make getting started with your library as easy as possible.

The simplest choice would be **JitPack**, which might not give you much in terms of customization or control, but is very easy to [get started](https://jitpack.io/) with. All you have to do is publish your project on GitHub, and JitPack should be able to build and distribute it immediately. If you’re new to libraries, this is a great choice for getting your code out there.

The next step up is **Jcenter**… [Which no longer accepts new packages and will stop serving them in February 2022](https://jfrog.com/blog/into-the-sunset-bintray-jcenter-gocenter-and-chartcenter/). Using it [had its issues](https://blog.autsoft.hu/a-confusing-dependency/) anyway.

Finally, the fanciest place you can be in is The Central Repository via Sonatype OSSRH (OSS Repository Hosting), which I’ll refer to as simply **MavenCentral** from here on. This is *the* place to be if you’re a Maven dependency. Artifacts on MavenCentral are well trusted, and their integrity can be verified, as they are all required to be signed by the author.

The publication process, however, and especially automating it, can be quite a headache. It’s easy to get stuck at many of the various steps no matter what tutorials you’re following, especially if they’re out of date, and this can get demotivating very quickly. It’s not uncommon to [give up and just use Bintray/Jcenter instead](https://issues.sonatype.org/browse/OSSRH-38347?focusedCommentId=505686&page=com.atlassian.jira.plugin.system.issuetabpanels%3Acomment-tabpanel#comment-505686) – which is not an option anymore.

So, if you feel up for a bit of a challenge, and want to do things the right way, here’s how you can get a library into MavenCentral, in 2021.

## Overview

Here's a quick overview of the steps we'll go through:

1. Registering a Jira account with Sonatype, and verifying your ownership of the group ID you want to publish your artifact with
2. Generating a GPG key pair for signing your artifacts, publishing your public key, and exporting your private key
3. Setting up Gradle tasks that can sign upload your artifacts to a staging repository
4. Manually going through the process of checking your artifacts in the staging repo and releasing them via the Sonatype web UI
5. Automating the close & release flow with a Gradle plugin
6. Configuring CI workflows with GitHub Actions to automate all of the above

A lot of ground to cover - let’s go!

## Prerequisites

We’ll be using the following tools for this tutorial. You are free to use alternatives, but these are our favourites, and they work well for us.

- The command line `gpg` tool
    - [GPG Suite](https://gpgtools.org/) for macOS or [Gpg4win](https://www.gpg4win.org/download.html) for Windows are great choices, which also come with helpful GUIs. Many alternatives for different platforms are available [here](https://gnupg.org/download/).
- GitHub as the public host of the library’s repository
- GitHub Actions as the continuous integration solution

For this article, we’ll assume that you already have your library developed, and have uploaded it to a public GitHub repository.

We’ll use [our very own Android Chat SDK](https://github.com/GetStream/stream-chat-android) in our examples. This [SDK](https://getstream.io/chat/sdk/android/) is made up of multiple artifacts, but for simplification, we’ll just talk about publishing the low-level networking client, which lives in the `stream-chat-android-client` module of the GitHub repository.

> Shameless plug: if you need a chat solution in your Android app, check out [our documentation](https://getstream.io/chat/docs/android/?language=kotlin) and our [Android Chat tutorial](https://getstream.io/tutorials/android-chat/).

## Registering a Sonatype account

First things first, you’ll need an account in the [Sonatype Jira](https://issues.sonatype.org/secure/Dashboard.jspa). Head over there and hit *Sign up*. Registration is straightforward, it just requires a username, an email, and a password.

![The login page to Sonatype Jira](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/iuri8a9vlo2rmc6ls4p6.PNG)

After you’ve logged in, you’ll need to open an issue, asking for access to the group ID that you’ll want to publish your project under. For us, based on our domain name (`getstream.io`), our group ID is `io.getstream`. If you own a domain, it’s best to choose the reversed version of that as your group ID. Otherwise, you’ll have to stick with having a GitHub-based group ID (see [Choosing your Coordinates](https://central.sonatype.org/pages/choosing-your-coordinates.html) for more details).

After choosing a language and an avatar, you’ll end up on this landing page - click on *Create an issue*:

![Sonatype Jira landing page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/50y3gntbi545vp3k790j.PNG)
 
Select *Community Support - Open Source Project Repository Hosting* and then *New Project*:

![Creating a new issue in Jira](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rncqgg6bqrmd1rowdkzo.PNG)

On the next page, fill out the following fields:

- **Summary:** *Create repository for your.group.id.here*
- **Description:** An optional, quick summary of what your project is.
- **Group Id:** Your group ID, as described a few sections earlier.
- **Project URL:** If your project has a webpage, the URL of that page. This can also be just the GitHub repository.
- **SCM url:** Your source control URL, i.e. the GitHub repository link.
- **Username(s):** If you want additional users (on top of the one you’re using for this process) to have deploy access for your group ID, you can list them here.
- **Already Synced to Central:** If you’re just getting started, this should be *No*.

![The filled out issue details page for a new repository](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5r4kqjm14gc0f77h9m5i.PNG)

Soon after opening it, your issue will get a comment telling you to verify that you own the domain corresponding to your group ID:

![Comment requesting domain verification](https://stream-blog-v2.imgix.net/blog/wp-content/uploads/da8ea3aa9e7861342114d89a78f7e7ef/please_verify_2-0-00-00-00.png?auto=compress%2Cformat&ixlib=php-3.3.0](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hgsfecmkirg0f8bmf18g.PNG)

To comply with this, add the required TXT record to your domain - how to do this will depend on where your domain is registered, but it should be a fairly simple task.

```
@    TXT    1800    OSSRH-12345
```

*Make sure to replace the ticket number above with your actual number.*

When done, don’t forget to leave a comment on the issue so that Sonatype knows to check the record. You’ll eventually get a response telling you that you now have deploy rights - congrats!

![The Sonatype comment confirming domain verification]](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/niwgnp6n317gkapz14cb.PNG)

## Generating a GPG key pair

As we eluded to earlier, artifacts published on MavenCentral have to be signed by their publishers. You’ll need a GPG key for this.

*MavenCentral also has its own documentation for [Working with PGP Signatures](https://central.sonatype.org/pages/working-with-pgp-signatures.html) which you can reference if you get stuck along the way.*

This part requires access to the `gpg` command. There are several ways to install this via package managers, and there are many distributions available for different platforms on [gnupg.org](https://gnupg.org/download/).

[GPG Suite](https://gpgtools.org/) is an easy-to-use distribution for macOS, and you can use [Gpg4win](https://www.gpg4win.org/download.html) if you’re on Windows. Both of these come with GUI tools that make managing keys easier.

We'll stick to the command line here, but note that the [old article](https://blog.autsoft.hu/publishing-an-android-library-to-mavencentral-in-2019/#creatingagpgkeypair) shows how to use the Kleopatra GUI (included in Gpg4win) on Windows for generating and managing keys.

To generate a new key, run the following command:

```
gpg --full-gen-key
```

You’ll be prompted to enter a few details:

- **Kind of key:** Accept the default value, which is `(1) RSA and RSA`.
- **Key size:** `4096`.
- **Expiration:** You can input `0` to generate a key that never expires. You can also create a key that has an expiry date and then [renew it](https://central.sonatype.org/pages/working-with-pgp-signatures.html#dealing-with-expired-keys) periodically, if you prefer to do so.
- **Real name, email:** Should be obvious.
- **Comment:** Freeform text, can be left empty.

After entering these details, you’ll be prompted to enter a passphrase to secure your key with.

Here’s the full flow you'll go through, with a bit of truncation:

```
Please select what kind of key you want:
   (1) RSA and RSA (default)
   ...
Your selection? 1

RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (3072) 4096
Requested keysize is 4096 bits

Please specify how long the key should be valid.
         0 = key does not expire
         ...
Key is valid for? (0) 0
Key does not expire at all
Is this correct? (y/N) y

GnuPG needs to construct a user ID to identify your key.

Real name: Marton Braun
Email address: marton@getstream.io
Comment: Example key for tutorial
You selected this USER-ID:
    "Marton Braun (Example key for tutorial) <marton@getstream.io>"

Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O

gpg: key 36271B955BEF072A marked as ultimately trusted
gpg: revocation certificate stored as '.../gnupg/openpgp-revocs.d\7A5D73CFEDDDBC915986998A36271B955BEF072A.rev'
public and secret key created and signed.

pub   rsa4096 2021-02-03 [SC]
      7A5D73CFEDDDBC915986998A36271B955BEF072A
uid                      Marton Braun (Example key for tutorial) <marton@getstream.io>
sub   rsa4096 2021-02-03 [E]
```

You can always check the keys you have on your system by running `gpg --list-keys`:

```
gpg: checking the trustdb
.../gnupg/pubring.kbx
-----------------------------------------------
pub   rsa4096 2021-02-03 [SC]
      7A5D73CFEDDDBC915986998A36271B955BEF072A
uid           [ultimate] Marton Braun (Example key for tutorial) <marton@getstream.io>
sub   rsa4096 2021-02-03 [E]
```

Your key’s ID is the last eight digits of its fingerprint (the long hexadecimal string above). In this case, this is `5BEF072A` - take note of this, as you’ll use this later.

You’ve generated a pair of keys - a *private* and a *public* key. You’ll keep the *private* one hidden and use it to sign your artifacts. The public key has to be uploaded to the server so that anyone can check that it belongs to you, which you can do by running the following (*use your own key ID!*):

```
gpg --keyserver keyserver.ubuntu.com --send-keys 5BEF072A
```

*See [Distributing Your Public Key](https://central.sonatype.org/publish/requirements/gpg/#distributing-your-public-key) for the list of supported keyservers.*

Your private key will need to be referenced by your project when it signs the artifacts. You can get a base 64 export of it with the following:

```
gpg --export-secret-keys 5BEF072A | base64
```

Enter your passphrase that you’ve set earlier when prompted. You can either copy-paste this key from the terminal output later on, or save it to a file temporarily.

## Setting up publication in your project

That’s a lot of work without touching your project, but the time has come to do that now. In the next few steps, you will:

1. Add Gradle scripts that set up the publication plugin required to push artifacts to a repository.
2. Configure the properties of the library you’re releasing.
3. Grab the necessary authentication details along with the private key you’ve just exported.

### Root project Gradle configuration

To easily automate publishing later, you'll use the [gradle-nexus/publish-plugin](https://github.com/gradle-nexus/publish-plugin/) tool. This has to be added in your project level (root) `build.gradle` file as a dependency.

You can do this _either_ with a `plugins` block:

```groovy
plugins {
    id("io.github.gradle-nexus.publish-plugin") version "1.1.0"
}
```

_Or_ with the classic buildscript dependency syntax, and then applying it in your project:

```groovy
buildscript {
    dependencies {
        classpath 'io.github.gradle-nexus:publish-plugin:1.1.0'
    }
}

apply plugin: 'io.github.gradle-nexus.publish-plugin'
```

> Check the [releases page](https://github.com/gradle-nexus/publish-plugin/releases) of the plugin to get the latest version.

Next, create a new file called `publish-root.gradle` in a new `scripts` folder inside your project. This will contain global configuration you need for publishing, grabbing input values for your scripts, and defining the MavenCentral repository.

Note that here we’ll go through the contents of these scripts part by part, with explanations. **You can always find the complete, up-to-date files we use in production [in the Stream Chat repository](https://github.com/GetStream/stream-chat-android/blob/develop/scripts/).**

In this file, you'll grab some configuration parameters. Using the script below, you’ll first set all the variables to a dummy empty string. This will let the project sync and build without the publication values set up, which would otherwise be an issue for your contributors.

```groovy
// Create variables with empty default values
ext["signing.keyId"] = ''
ext["signing.password"] = ''
ext["signing.key"] = ''
ext["ossrhUsername"] = ''
ext["ossrhPassword"] = ''
ext["sonatypeStagingProfileId"] = ''
```

The first three variables will be used to sign the artifacts after they’re built:

- `signing.keyId`: the ID of the GPG key pair, the last eight characters of its fingerprint
- `signing.password`: the passphrase of the key pair
- `signing.key`: the base64 encoded private key you exported earlier

`osshrUsername` and `ossrhPassword`: are your account details for MavenCentral, which you’ve chosen at the Jira registration step. We’ll get back to where `sonatypeStagingProfileId` comes from later on.

Next, you’ll try to fetch the values of the variables from a `local.properties` file in the root of the project if it exists, otherwise you’ll look for them in the environment variables. The former lets you easily input these values locally on your machine, while the latter will help with setting up CI.

```groovy
File secretPropsFile = project.rootProject.file('local.properties')
if (secretPropsFile.exists()) {
    // Read local.properties file first if it exists
    Properties p = new Properties()
    new FileInputStream(secretPropsFile).withCloseable { is -> p.load(is) }
    p.each { name, value -> ext[name] = value }
} else {
    // Use system environment variables
    ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
    ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
    ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
    ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
    ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
    ext["signing.key"] = System.getenv('SIGNING_KEY')
}
```

Make sure that you’ve set these variables either in the aforementioned `local.properties` file or in your environment variables. If you want to use the property file, the syntax for it should look something like this (*replace all the data here with your own!*):

```
signing.keyId=5BEF072A
signing.password=signingPass123
signing.key=bm90IGEgcmVhbCBrZXksIHdoYXQgd2VyZSB5b3UgZXhwZWN0aW5n
ossrhUsername=yourSonatypeUser
ossrhPassword=yourSonatypePassword
```

The last piece of code to add to this file will define the MavenCentral (Sonatype) repository where publishing should upload the artifacts. This relies on parameters fetched above to authenticate:

```groovy
// Set up Sonatype repository
nexusPublishing {
    repositories {
        sonatype {
            stagingProfileId = sonatypeStagingProfileId
            username = ossrhUsername
            password = ossrhPassword
        }
    }
}
```

**Important:** If you're on the new Sonatype infrastructure (happens if you've registered after 2021-02-24 or requested it specifically), you have to add explicit URLs pointing to `s01.oss.sonatype.org` in this config block next to the existing parameters, like so:

```groovy
nexusPublishing {
    repositories {
        sonatype {
            /* Existing params here... */
            nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
            snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
        }
    }
}
```

As the last step, apply this script in your root `build.gradle` file like so:

```groovy
apply from: "${rootDir}/scripts/publish-root.gradle"
```

This completes the configuration needed in the root project. [This gist](https://gist.github.com/zsmb13/56ed98c8fe916de441f2a9d8e060cd4a) contains the code blocks detailed above in a single snippet. The final, up-to-date script we use for publishing our SDK can be found **[in the Stream Chat repository](https://github.com/GetStream/stream-chat-android/blob/develop/scripts/publish-root.gradle)**.

### Per-module Gradle setup

With the repository defined and parameters fetched, it's time to create a script that will define the artifacts in each module you want to publish. Create a new file called `publish-module.gradle` in the `scripts` folder. As its name suggests, this will be applied to each _module_ that you want to publish an artifact from.

In this file, first you’ll apply two plugins for publishing and signing.

```groovy
apply plugin: 'maven-publish'
apply plugin: 'signing'
```

*Our publishing file also applies the [Dokka plugin](https://github.com/Kotlin/dokka) in this block, which you have to configure separately, if you want to use it.*

Then you declare the sources artifact for the library. This is really important - it will make sure that the source files are packaged along with the executable, compiled code, so that your users can easily jump to the definitions that they’re calling into within their IDE.

```groovy
task androidSourcesJar(type: Jar) {
    archiveClassifier.set('sources')
    if (project.plugins.findPlugin("com.android.library")) {
        // For Android libraries
        from android.sourceSets.main.java.srcDirs
        from android.sourceSets.main.kotlin.srcDirs
    } else {
        // For pure Kotlin libraries, in case you have them
        from sourceSets.main.java.srcDirs
        from sourceSets.main.kotlin.srcDirs
    }
}

artifacts {
    archives androidSourcesJar
}
```

*Once again, our real publishing file also adds an additional `javadocJar` task which ships Dokka-generated documentation - again, you can skip this if you want to.*

You’ll set two properties on the Gradle project itself here, the group ID and the version of the artifact. You’ll see where these uppercase values come from later on, when you apply this publication script in the module level `build.gradle` files.

```groovy
group = PUBLISH_GROUP_ID
version = PUBLISH_VERSION
```

Here comes the complicated part, providing all the metadata for the library you’re releasing. See the inline comments for the play-by-play explanation.

```groovy
afterEvaluate {
    publishing {
        publications {
            release(MavenPublication) {
                // The coordinates of the library, being set from variables that
                // we'll set up later
                groupId PUBLISH_GROUP_ID
                artifactId PUBLISH_ARTIFACT_ID
                version PUBLISH_VERSION

                // Two artifacts, the `aar` (or `jar`) and the sources
                if (project.plugins.findPlugin("com.android.library")) {
                    from components.release
                } else {
                    from components.java
                }

                artifact androidSourcesJar
                artifact javadocJar

                // Mostly self-explanatory metadata
                pom {
                    name = PUBLISH_ARTIFACT_ID
                    description = 'Stream Chat official Android SDK'
                    url = 'https://github.com/getstream/stream-chat-android'
                    licenses {
                        license {
                            name = 'Stream License'
                            url = 'https://github.com/GetStream/stream-chat-android/blob/main/LICENSE'
                        }
                    }
                    developers {
                        developer {
                          id = 'zsmb13'
                          name = 'Márton Braun'
                          email = 'marton@getstream.io'
                        }
                        // Add all other devs here...
                    }
  
                    // Version control info - if you're using GitHub, follow the 
                    // format as seen here
                    scm {
                        connection = 'scm:git:github.com/getstream/stream-chat-android.git'
                        developerConnection = 'scm:git:ssh://github.com/getstream/stream-chat-android.git'
                        url = 'https://github.com/getstream/stream-chat-android/tree/main'
                    }
                }
            }
        }
    }
}
```

Finally, this piece of code grabs signing related values from the root project, and then tells the signing plugin to sign the artifacts you’ve defined above as a publication.

```groovy
signing {
    useInMemoryPgpKeys(
            rootProject.ext["signing.keyId"],
            rootProject.ext["signing.key"],
            rootProject.ext["signing.password"],
    )
    sign publishing.publications
}
```

That’s the `publish-module.gradle` script all built up, ready to use. [This gist](https://gist.github.com/zsmb13/56ed98c8fe916de441f2a9d8e060cd4a) contains the code blocks shown above in a single snippet. The final, up-to-date script we use for publishing our SDK can be found **[in the Stream Chat repository](https://github.com/GetStream/stream-chat-android/blob/develop/scripts/publish-module.gradle)**.

Time to include this script in a module! Head to the `build.gradle` file of your library module - in our case, this is the `stream-chat-android-client` module - and add the following code:

```groovy
ext {
    PUBLISH_GROUP_ID = 'io.getstream'
    PUBLISH_VERSION = '4.5.2'
    PUBLISH_ARTIFACT_ID = 'stream-chat-android-client'
}

apply from: "${rootProject.projectDir}/scripts/publish-module.gradle"
```

Here you finally see the group ID, artifact ID, and version being set, so that the publication script can make use of them. Then, the script itself is applied. This is all the code you need to add per-module if you are publishing your library in multiple artifacts, everything else is done by the common script.

One last configuration step: you'll need to set the `sonatypeStagingProfileId` variable in your project. This value is an ID that Sonatype assigns to you, which the plugin uses to make sure all the artifacts end up in the right place during the upload.

Go to [https://oss.sonatype.org/](https://oss.sonatype.org/) and log in. In the menu on the left, select *Staging profiles*, select your profile, and then look for the ID in the URL.

![The Sonatype staging profiles screen and the staging profile ID shown in the URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r9h8zemd3o9a3e97wim7.PNG)

Either set this value in your `local.properties` file, or set the corresponding environment variable.

## Your first release

With all of that set up, you’re now ready to publish the first version of your library!

For each repository you have defined in the publishing script, a Gradle task will be created to publish to that repository. In our example, our first module to publish is `stream-chat-android-client`, and we’ve named the repository `sonatype`. Therefore, we need to execute the following command to start publication (replace the module name with your own here):

```
gradlew stream-chat-android-client:publishReleasePublicationToSonatypeRepository
```

This will create a so-called staging repository for your library, and upload your artifacts (`aar` and sources) to that repository. At this point, you can check that all the artifacts you wanted to upload have made it, before hitting the release button.

Now, go to the Sonatype site, and in the menu on the left, select *Staging repositories*.

![The Sonatype menu](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sbzmk4tmkojusmrnyelg.PNG)

Find your repository (might be the only one in the list, might have to scroll around a bit for it), which has your group ID in its name. If you select it and look at the *Content* tab, you’ll see the files that have been uploaded.

![The contents of the staging repository](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yb2bw6ft36pfs2dp0j4n.PNG)

If everything looks good, and you’re done uploading files to the repository, you have to *Close* it. With the repository selected, hit the *Close* button in the toolbar on top. Confirm your action in the dialog (you don’t need to provide a description here).

![Closing a staging repository](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4r3ugl82lr776wnfnwqc.PNG)

This will take just a few moments, you can follow along with it happening in the *Activity* tab.

![Repository Activity tab showing successful close operation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ay61m9hewfnh10p5ce1c.PNG)

With the repository closed, you now have two final options available to you. *Drop* will throw away the repository, and cancel the publication process. Use this if something went wrong during the upload or you’ve changed your mind.

*Release*, on the other hand, will publish the contents of your staging repository to MavenCentral. Again, you get a confirmation dialog, and you can choose *Automatically Drop* so that the staging repository is cleaned up after the release completes.

![Release dialog for the staging repository](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pwh2i6c7nm6a8d0vue04.PNG)

The time this process takes can vary a bit. If you get lucky, your artifact will show up on MavenCentral in 10-15 minutes, but it could also take an hour or more in other cases. You can check whether your artifact is available by going to [https://repo1.maven.org/maven2/](https://repo1.maven.org/maven2/) and browsing for it.

For example, for our client, we can keep refreshing the following page to see if the new version is there: [https://repo1.maven.org/maven2/io/getstream/stream-chat-android-client/](https://repo1.maven.org/maven2/io/getstream/stream-chat-android-client/).

If you see the package via those links, Gradle will also be able to pull it if `mavenCentral()` is added as a repository and you add your library as a dependency.

*Search indexing is a separate, even longer process, so it can take about two hours for your artifact to show up on [search.maven.org](https://search.maven.org/).*

If this was your first release, you should at this point go back and comment on your original Jira issue, to let them know that your repository setup and publication is working.

## Automating Sonatype actions

That was quite the adventure! To make things smoother for subsequent releases, you can automate the entire release flow with the publishing plugin that you already have configured in the project.

*This, in theory, would mean that you don’t have to revisit the Sonatype UI ever again. In practice, you’ll do that more often than you’d like when the plugin (really, the Sonatype API it calls) breaks or misbehaves, so it's worth being familiar with.*

The plugin provides a Gradle task that you can use to close and then release your staging repository with one simple call:

```
gradlew closeAndReleaseSonatypeStagingRepository
```

At this point, you can upload and publish your library by just invoking these two Gradle tasks in sequence - pretty convenient! As a final step, let’s hook this into a CI pipeline.

## Continuous integration

Since the library is hosted on GitHub anyway, we use GitHub Actions for running the publication Gradle tasks automatically. Whatever CI solution you’re using, setting up publication with it will consist of two main steps:

1. Getting your secret variables in place.
2. Invoking the two Gradle tasks.

Your secret variables - for the list of these, look at the publishing script again - can simply go into Repository secrets (Add each of these by going to *Settings -> Secrets* within your GitHub repository):

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

Now, let’s create the GitHub Actions workflow that will put all of this together. The configuration for this will go in the `.github/workflows/publish.yml` file of the repository. This publish workflow will run every time a new [release](https://github.com/GetStream/stream-chat-android/releases) is created in the repository (you can also change the triggers to run when a tag is created, for example). Again, see the inline comments for some explanation of what each step does.

*If you’re new to GitHub Actions, check out their [introductory documentation](https://docs.github.com/en/actions) first.*

```yaml
name: Publish

on:
  release:
    # We'll run this workflow when a new GitHub release is created
    types: [released]

jobs:
  publish:
    name: Release build and publish
    runs-on: ubuntu-latest
    steps:
      - name: Check out code
        uses: actions/checkout@v2
      - name: Set up JDK 11
        uses: actions/setup-java@v2
        with:
          distribution: adopt
          java-version: 11

        # Builds the release artifacts of the library
      - name: Release build
        run: ./gradlew :stream-chat-android-client:assembleRelease

        # Generates other artifacts (javadocJar is optional)
      - name: Source jar and dokka
        run: ./gradlew androidSourcesJar javadocJar

        # Runs upload, and then closes & releases the repository
      - name: Publish to MavenCentral
        run: ./gradlew publishReleasePublicationToSonatypeRepository --max-workers 1 closeAndReleaseSonatypeStagingRepository
        env:
          OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
          OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
          SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
          SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
          SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
          SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
```

MavenCentral uploads tend to be… Flaky, to put it nicely. Here are some recommendations to have fewer issues with them:

- Perform all your Gradle upload tasks in a single job, on a single machine.
- The `--max-workers 1` option used above guarantees that the upload task runs on a single thread, even if parallel builds are otherwise configured.
- It’s also a good idea to put the upload task (all of them, if you’re uploading multiple modules’ worth of artifacts) in a different Gradle invocation than the assemble & signing tasks, as you see it done above, so that they run closely together.

If a long time passes between your upload calls, or if they originate from different CI machines, you might see problems such as multiple staging repositories being created for you on Sonatype with your files scattered all over them.

Even if you do everything described above right, you might still get random upload errors, or timeouts either during the upload or while the plugin is trying to close and release the repository. At this point, being able to look at the staging repository on the Sonatype UI and manually closing/dropping/releasing repositories will come in handy to fix things up.

If your staging repository seems okay (has correct contents), and it just failed to close, you can close and release it yourself. If it looks messed up somehow, or you ended up with multiple staging repositories, it’s best to drop them all and then run the upload workflow again.

> Reminder: our up-to-date publishing workflow is always available [in our GitHub repository](https://github.com/GetStream/stream-chat-android/blob/master/.github/workflows/publish.yml).

## Conclusion

Well, that was quite a journey. We hope that this detailed guide helped you get up and running with MavenCentral publication. You can follow us on Twitter [@getstream_io](https://twitter.com/getstream_io) for more great technical content - **please tweet at us if you've successfully set up publishing following these steps!** You can also reach the author [@zsmb13](https://twitter.com/zsmb13) if you have any questions.

> If you're looking for a Kotlin-first, powerful [Chat SDK](https://getstream.io/chat/sdk/android/) to use in your Android app, check out [our Android documentation](https://getstream.io/chat/docs/android/?language=kotlin) and our [Android Chat tutorial](https://getstream.io/tutorials/android-chat/).

If you're interested in library development, you'll find these articles helpful as well:

- [Maintaining Compatibility in Kotlin Libraries](https://zsmb.co/maintaining-compatibility-in-kotlin-libraries/)
- [Mastering API Visibility in Kotlin](https://zsmb.co/mastering-api-visibility-in-kotlin/)