Publish a library to MavenCentral

Birds-eye instructions on how to publish an Android library to MavenCentral.

Based on these detailed instructions.

Get publishing rights

This part stinks, but you only have to do it once. Create an account on Sonatype’s Jira. Then open a new issue. Select Community Support > Open Source Project Repository Hosting > New Project.

As the Group ID you should use the one you plan to use for your library. In our case this is always com.pixplicity. To close the issue you will be requested to configure a TXT DNS record or set up a redirect to your Github, to prove that you have ownership over either your domain name or Github repo.

Create keypair

Use gpg to generate a keypair. Choose RSA two times and 0 to make it never expire.

gpg --full-gen-key

Then view the generated key and note down the last 8 characters of the hexadecimal fingerprint. This is the Key ID.

Next, split the key into a private and public key:

gpg --keyserver keyserver.ubuntu.com --send-keys <insert Key ID>
gpg --export-secret-keys <insert key ID> > <insert key ID>.gpg

Copy the keys into a folder in your project. We’ll call it .signing. Do not ever commit these files to git!. In that folder folder also create a file mavencentral.properties to hold the credentials:

signing.keyId=<insert key ID>
signing.password=<key password>
signing.secretKeyRingFile=../.signing/<key ID>.gpg
ossrhUsername=<your sonatype username>
ossrhPassword=<and password>

We advise to only set these credentials on the CI/CD that should deploy the libraries to MavenCentral. You can also use environment variables instead of this properties file, but that is not covered in this recipe.

Configure properties

Add a bunch of environment variables to your gradle.properties. Below is an example. It assumes there is only one developer to be listed. This file may contain the non-sensitive data only and should be commited to git.

GROUP=com.pixplicity.example
ARTIFACT_NAME=SampleProj
ARTIFACT_ID=examplemento

VERSION_NAME=1.3.0

POM_NAME=SampleProj
POM_DESCRIPTION=Lorem ipsum dolor sit amet
POM_URL=https://github.com/Pixplicity/example
POM_SCM_URL=https://github.com/Pixplicity/example
POM_ISSUE_URL=https://github.com/Pixplicity/example/issues
POM_SCM_CONNECTION=scm:git@github.com:Pixplicity/example.git
POM_SCM_DEV_CONNECTION=scm:git@github.com:Pixplicity/example.git
POM_LICENCE_NAME=The Apache Software License, Version 2.0
POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
POM_LICENCE_DIST=repo
POM_DEVELOPER_ID_0=developergithubusername
POM_DEVELOPER_NAME_0=Pixel Simplicity
POM_DEVELOPER_EMAIL_0=example@pixplicity.com

Don’t forget to update the VERSION_NAME before deploying!.

Gradle

In the library’s build.gradle, import the file we’re about to create:

apply from: "${rootProject.projectDir}/mavencentral_publish.gradle"

Then create it in the root of the project:

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

task androidSourcesJar(type: Jar) {
    archiveClassifier.set('sources')
    if (project.plugins.findPlugin("com.android.library")) {
        from android.sourceSets.main.java.srcDirs
        // Enable if you have this folder. Not always
        // applicable; Kotlin project often have the Kotlin
        // code in the Java folder
        //from android.sourceSets.main.kotlin.srcDirs
    } else {
        from sourceSets.main.java.srcDirs
        from sourceSets.main.kotlin.srcDirs
    }
}

artifacts {
    archives androidSourcesJar
}

// Note: these lines are important, even though they're not used in this file!
// The plugin looks for it.
group = GROUP
version = VERSION_NAME

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

File secretPropsFile = project.rootProject.file('.signing/mavencentral.properties')
if (secretPropsFile.exists()) {
    Properties p = new Properties()
    p.load(new FileInputStream(secretPropsFile))
    p.each { name, value ->
        ext[name] = value
    }
} else {
    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')
    ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
}

publishing {
    publications {
        release(MavenPublication) {
            groupId GROUP
            artifactId ARTIFACT_ID
            version VERSION_NAME

            if (project.plugins.findPlugin("com.android.library")) {
                artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
            } else {
                artifact("$buildDir/libs/${project.getName()}-${version}.jar")
            }

            artifact androidSourcesJar

            pom {
                name = ARTIFACT_ID
                description = POM_DESCRIPTION
                url = POM_SCM_URL
                licenses {
                    license {
                        name = POM_LICENCE_NAME
                        url = POM_LICENCE_URL
                    }
                }
                developers {
                    developer {
                        id = POM_DEVELOPER_ID_0
                        name = POM_DEVELOPER_NAME_0
                        email = POM_DEVELOPER_EMAIL_0
                    }
                }
                scm {
                    connection = POM_SCM_CONNECTION
                    developerConnection = POM_SCM_DEV_CONNECTION
                    url = POM_SCM_URL
                }
                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 {
        maven {
            name = "sonatype"
            url = "https://oss.sonatype.org/service/local/staging/deploy/maven2/"

            credentials {
                username ossrhUsername
                password ossrhPassword
            }
        }
    }
}

signing {
    sign publishing.publications
}

Deploy

Now you can use Gradle to deploy: (assuming the library module to deploy is called library)

gradlew library:build library:publishReleasePublicationToSonatypeRepository

Publish

Log in to Sonatype, and find the library under Staging repositories where you can Close it. After that, you can Release it to publish it, or Drop it to remove it.

You can read further here to automate these steps as well.

Find it

Once published, it will become available on search.maven.org, and developers can include it using something like this as long as the mavenCentral() repo is included:

implementation 'com.pixplicity.example:example:1.3.+'

Next steps

Follow the steps here to configure your CI to deploy.

Troubleshooting

Gradle is unable to sign

The error is a bit incomprehensible from Gradle, but it means it cannot locate the ./signing folder.

Signing error in Sonatype’s portal

The keyserver hkp://pool.sks-keyservers.net is deprecated, and Sonatype can’t verify your gpg key anymore. Switch to keyserver.ubuntu.com in the very first command in this recipe and upload your key again.