Publishing an Android library to MavenCentral in 2019

Introduction

Creating a library is challenging enough on its own. Coming up with the idea, implementing it, making sure you have a nice, stable public API that you maintain... 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 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 requires you to register a Bintray account, and request for your package to be included in the Jcenter repository. You also have to set up the Bintray publication plugin in your project, which can take some time when you're doing it for the first time. Jcenter is still to this day included by default in every new Android project, but unfortunately, it is far from perfect.

Finally, the fanciest place you can be in is The Central Repository by Sonatype, which I'll refer to as MavenCentral from here on out. 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.

If you do feel ready 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 the summer of 2019.

Overview

A quick overview of the steps to go through:

  • Registering a Jira account with Sonatype, and asking for publish rights into the repository under your own group ID.
  • Generating a GPG key pair for signing your artifacts. Exporting your private key.
  • Setting up Gradle scripts that can upload your signed artifacts.
  • Manually going through the process of checking your artifacts and releasing them via Sonatype.
  • Adding another Gradle plugin to automate the process in the previous point.
  • Wrapping up all the publication tasks in CI jobs.

Get a drink and strap in, this is gonna be a long ride.

Our setup & 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.

  • Gradle as our build system. This is the de-facto choice on Android.
  • Kleopatra to create and manage our GPG keys with a simple GUI.
  • GitHub as the public host of the library's repository.
  • GitLab CI as our continuous integration solution that will let us publish our library in an automated manner.

The last two points above make for an odd pair. We use GitHub because whether we like it or not, it's the go-to platform for open source projects. We also use GitLab CI, because that's our internal CI setup that we're used to. Thankfully, the configuration steps will be very similar for whatever CI solution you're using.

For the purposes of 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 open source library Krate in our examples, which we've written about in one of our previous posts already. Krate is a SharedPreferences wrapper based on convenient Kotlin delegates.

Getting a repository to publish to

First things first, you'll need an account in the Sonatype Jira. Head over there and hit Sign up. Registration is straightfoward, it just requires a username, an email, and a password.

The Sonatype Jira login page

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, our group ID is hu.autsoft. As you'll see in a moment, it's best to choose a group ID that matches a domain that you own, otherwise you'll have to stick with having a GitHub-based group ID (see details here).

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

The Sonatype Jira landing page

Select Community Support - Open Source Project Repository Hosting and then New Project:

Creating a new issue, basics

On the next page, fill out the following fields:

  • Summary: Create repository for <your group id, perhaps artifact ID here>
  • Description: An optional, quick summary of what your project is.
  • Group ID: Your group ID.
  • Project URL: If your project has a webpage, the URL of that page. May also be just the GitHub repository.
  • SCM url: Your source control URL, i.e. the GitHub repository.
  • 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.

Creating an issue, details

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

A comment asking for domain verification

To comply with this, add the required TXT record to your domain (replace the issue number with your own!):

@    TXT    1800    OSSRH-12345

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!

Confirmation of the domain verification

Creating 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.

What we'll show you here is a GUI client, Kleopatra, which helps you manage your keys with ease. You could also generate your key from the command line using gpg, as described here, for example.

To get started, go to File -> New Key Pair...

Selecting the new key pair menu

In the first step of the wizard, choose the personal OpenPGP key pair option.

Choosing personal key pair

Fill out at least one of the name and email fields.

The name and email fields

You can also check the Advanced settings for the key that's being generated here. The defaults should be all good, but here's our settings - notably, there is no expiration date set in the Valid until field.

A glimpse at the advanced settings

A quick review of the parameters, and you're ready to generate your keys!

Parameter review page

Finally, your keys have to be sealed with a passphrase. This should be a strong, secure password that you're not using elsewhere. Whoever has access to the private key and this passphrase will be able to sign in your name.

Passphrase entry

After the key pair has been generated, you'll see the following confirmation window. This contains the fingerprint of the key pair, which you'll use to identify it. You also have the option to back up your key and to upload the public key to a public directory.

Success dialog

You should perform this upload by choosing Upload Public Key To Directory Service..., and confirming the action in the dialog that appears:

Uploading the public key to a directory

Note that if you ever want to retract this key (perhaps because your private key was exposed), Kleopatra can also generate a revocation certificate for you, which essentially acts as a kill switch by marking the public key as no longer valid. You can see this option on the Details page of the key pair.

Key pair details page

Exporting your GPG key

To sign your artifacts, you'll need to have your private GPG key handy as a file. Exporting it takes just a couple quick steps. Right click your key, and choose Export Secret Keys...

Selecting the export option

Select the destination of the exported key. It's a good idea to name it after its fingerprint - or the last couple characters of the fingerprint, the key's ID.

Choosing the export destination file

Confirm the passphrase of the key that's being exported, and you're done.

Passphrase confirmation

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. You're going to add some Gradle scripts that set up the publication plugin required to push artifacts to a repository, configure the properties of the library you're releasing, and grab the necessary authentication details along with the private key you've just exported.

To start, create a new file called publish-mavencentral.gradle in a new scripts folder inside your project. All the publication logic can go here, and then you can reuse it in multiple modules if your library has multiple artifacts to publish. We'll go through the contents of this script part by part, with explanations. You can always find the complete, up-to-date file here in the Krate repository.

First, you declare the sources artifact for the library. This 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.

task androidSourcesJar(type: Jar) {  
    classifier = 'sources'
    from android.sourceSets.main.java.source
}

artifacts {  
    archives androidSourcesJar
}

You'll be making use of two plugins for the publication, maven-publish and signing. Both of these are built-in, so they don't require any new dependencies.

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

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 values come from later on, when you apply this publication script in the module level build.gradle files.

group = PUBLISH_GROUP_ID  
version = PUBLISH_VERSION  

Next, let's grab a whole bunch of configuration parameters. In 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 set up, which would otherwise be an issue for your contributors.

Then, 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.

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.secretKeyRingFile: the location of the exported private key on disk.

The rest (ossrhUsername and ossrhPassword) will authenticate you to MavenCentral. These are the credentials that you've chosen for your Sonatype Jira registration.

ext["signing.keyId"] = ''  
ext["signing.password"] = ''  
ext["signing.secretKeyRingFile"] = ''  
ext["ossrhUsername"] = ''  
ext["ossrhPassword"] = ''

File secretPropsFile = project.rootProject.file('local.properties')  
if (secretPropsFile.exists()) {  
    println "Found secret props file, loading props"
    Properties p = new Properties()
    p.load(new FileInputStream(secretPropsFile))
    p.each { name, value ->
        ext[name] = value
    }
} else {
    println "No props file, loading env vars"
    ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
    ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
    ext["signing.secretKeyRingFile"] = System.getenv('SIGNING_SECRET_KEY_RING_FILE')
    ext["ossrhUsername"] = System.getenv('OSSRH_USERNAME')
    ext["ossrhPassword"] = System.getenv('OSSRH_PASSWORD')
}

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=7ACB2D2A  
signing.password=signingPass123  
signing.secretKeyRingFile=C\:/gpg-keys/7ACB2D2A.gpg  
ossrhUsername=yourSonatypeUser  
ossrhPassword=yourSonatypePassword  

Here comes the complicated part, providing all the metadata for the library we're releasing, as well as the repository address that you'll upload it to. See the comments for the play-by-play explanation here.

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

            // Two artifacts, the `aar` and the sources
            artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
            artifact androidSourcesJar

            // Self-explanatory metadata for the most part
            pom {
                name = PUBLISH_ARTIFACT_ID
                description = 'A Kotlin SharedPreferences wrapper'
                // If your project has a dedicated site, use its URL here
                url = 'https://github.com/autsoft/krate'
                licenses {
                    license {
                        name = 'The Apache License, Version 2.0'
                        url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
                    }
                }
                developers {
                    developer {
                        id = 'zsmb13'
                        name = 'Márton Braun'
                        email = 'braun.marton@autsoft.hu'
                    }
                }
                // Version control info, if you're using GitHub, follow the format as seen here
                scm {
                    connection = 'scm:git:github.com/autsoft/krate.git'
                    developerConnection = 'scm:git:ssh://github.com/autsoft/krate.git'
                    url = 'https://github.com/autsoft/krate/tree/master'
                }
                // A slightly hacky fix so that your POM will include any transitive dependencies
                // that your library builds upon
                withXml {
                    def dependenciesNode = asNode().appendNode('dependencies')

                    project.configurations.implementation.allDependencies.each {
                        def dependencyNode = dependenciesNode.appendNode('dependency')
                        dependencyNode.appendNode('groupId', it.group)
                        dependencyNode.appendNode('artifactId', it.name)
                        dependencyNode.appendNode('version', it.version)
                    }
                }
            }
        }
    }
    repositories {
        // The repository to publish to, Sonatype/MavenCentral
        maven {
            // This is an arbitrary name, you may also use "mavencentral" or
            // any other name that's descriptive for you
            name = "sonatype"

            def releasesRepoUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
            def snapshotsRepoUrl = "https://oss.sonatype.org/content/repositories/snapshots/"
            // You only need this if you want to publish snapshots, otherwise just set the URL
            // to the release repo directly
            url = version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl

            // The username and password we've fetched earlier
            credentials {
                username ossrhUsername
                password ossrhPassword
            }
        }
    }
}

Finally, this small piece of code tells the signing plugin to sign the artifacts we've defined above.

signing {  
    sign publishing.publications
}

That's the publish-mavencentral.gradle script all built up, ready to use. Time to include it in a module! Head to the build.gradle file of your library module (in our case, this is the krate module), and add the following code:

ext {  
    PUBLISH_GROUP_ID = 'hu.autsoft'
    PUBLISH_ARTIFACT_ID = 'krate'
    PUBLISH_VERSION = android.defaultConfig.versionName
}

apply from: "${rootProject.projectDir}/scripts/publish-mavencentral.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.

Your first release, manually

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 krate, 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 krate:publishReleasePublicationToSonatypeRepository  

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

To view the repository, go to https://oss.sonatype.org/ and log in. In the menu on the left, select Staging repositories.

The Sonatype menu

Scroll around the list until you find your own repository, 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.

List of staging repos

If you have multiple modules to publish, at this point you could keep invoking their Gradle upload tasks, and collect all the uploaded files in this staging repository. When you're done uploading files to the repository, you have to Close it. With the repository selected, hit the Close button in the toolbar. Confirm your action in the dialog (you don't need to provide a description here).

Closing your staging repository

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

Observing the activity of the staging repository

With the repository closed, you now have two final options available to you. Drop will throw away the repository, and cancel the publication entirely. 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.

Releasing the staging repository

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. Note that search indexing is a separate, even longer process, so it can take about two hours for your artifact to show up on 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 closing and releasing

That was quite the adventure, wasn't it? To make things smoother for subsequent releases, you can automate the entire release flow using an additional Gradle plugin.

You'll have to add a new plugin, which will perform the closing and releasing of your staging repository for you via Gradle tasks. This one does come from an external source, so add it to your dependencies in your project level build.gradle file:

buildscript {  
    dependencies {
        classpath "io.codearte.gradle.nexus:gradle-nexus-staging-plugin:0.21.0"
    }
}

In this same build.gradle file, apply the plugin:

apply plugin: 'io.codearte.nexus-staging'  

Next, add the following configuration to your publish-mavencentral.gradle script, anywhere after you've fetched the username and password variables. Don't forget to replace the stagingProfileId with your own:

nexusStaging {  
    packageGroup = PUBLISH_GROUP_ID
    stagingProfileId = 'bcea62bcea28e7' // dummy example, replace with your own!
    username = ossrhUsername
    password = ossrhPassword
}

The packageGroup will just match your group ID again. The stagingProfileId 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. You can find this by going to Staging profiles, selecting your profile, and looking at the ID in the URL.

Finding the staging profile ID in the URL

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

gradlew closeAndReleaseRepository  

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 (with GitLab)

In our case, the tool for this happens to be GitLab CI. Whatever you're using, setting up publication with it will consist of two main steps:

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

Most of your secret variables - for the list of these, look at the publishing script again - can simply go into protected variables, which you'll find under Settings > CI/CD > Variables within your project in the case of GitLab:

Setting secret GitLab variables

However, your private GPG key is harder to inject into the build. It needs to be present as a file, but you should never commit it into a public repository.

You could technically commit the private key into a public repository, since it is protected by its passphrase. At that point, your key is only as secure as the strength of your passphrase (see more discussion here and here). It's much more secure to keep the key entirely private.

The workaround for this is to add its contents as a secret variable, and then write those contents into a temporary file during your build. Since it's a binary file, you need to first convert its contents into text form - base 64 encoding comes to the rescue.

Convert your secret key file into base 64 with the following command (if you're on Windows, you can use a Git or Ubuntu bash for this):

base64 7ACB2D2A.gpg > 7ACB2D2A.txt  

Place the contents of this file into yet another protected variable, and name it GPG_KEY_CONTENTS. You'll be writing these contents back into a file called /secret.gpg during the pipeline. Make sure that the value you're setting for SIGNING_SECRET_KEY_RING_FILE matches that path, as that's how the publication script will be able to find your private key.

We won't go into the CI job configuration in too much detail. You can always look at the full, up-to-date CI config file for Krate here.

Most importantly, this config needs to include the following steps (replace the module name with your own):

echo $GPG_KEY_CONTENTS | base64 -d > /secret.gpg  
gradlew krate:publishReleasePublicationToSonatypeRepository  
gradlew closeAndReleaseRepository  

First, it creates the /secret.gpg file by taking the environment variable, and performing a base64 decode on it. Next, it uploads a module's artifacts to the staging repository. If you have multiple modules, invoke this task for each module. Finally, the script closes and releases the staging repository.

It's recommended that you perform these Gradle tasks in a single job, on a single machine, and it might even help if it happens in a single Gradle invocation. Otherwise you might see problems such as multiple staging repositories being created for you with your files scattered all over them. At this point, being able to look at the staging repository and manually close/drop/release repositories will come in handy to fix things up.

Conclusion

Well, that was quite a journey. We hope that this detailed guide helped you get up and running with MavenCentral publication. If you have questions, you can contact and follow us on Twitter @autsoftltd.

If you're interested in library development, we recommend that you check out this article showcasing how we've designed Krate, and this article about maintaining compatibility between library versions in Kotlin libraries. You can also read about some important security concerns we ran into when using Jcenter.