Get Approved by Sonatype

The first step is to create an account with their Jira as that is how their request process is managed. Once you have successfully created the account, you will need to store these credentials so that our Gradle script can access them. To do this we need to create (or edit if it already exists) your user’s gradle.properties. You can find this in your user’s home directory in a folder named .gradle, e.g. for my account it would be /Users/mannan/.gradle/gradle.properties

> vi /Users/mannan/.gradle/gradle.properties
sonatypeUsername=YourUsername
sonatypePassword=YourPassword

The next step is to create a JIRA request to get access to publish artefacts. You have to use a domain name as your group, so for this website it would be com.mannanlive. If you don’t have a domain, don’t panic. You can use where your source code is stored, so if your on GitHub you could use either io.github.mannanm or com.github.mannanm. Fill in all the details and submit your ticket. This can take up to three days so it is good to do this early. In the mean time you can continue with the rest of the steps so you are ready-to-go when you are approved.

Publishing to Maven Central

Now we have got our authentication request underway, the next step is to configure Gradle to publish artefacts to Nexus. Lucky for us Gradle has a nifty plugin called maven-publish. We need to add this and then configure the task.

plugins {
    id 'java'
    id 'maven-publish'
}
 
group = 'com.mannanlive'
version = '0.1'
rootProject.description = 'Your project\'s description.'
 
publishing {
    publications {
        mavenJava(MavenPublication) {
            from components.java
        }
    }
 
    repositories {
        maven {
            url 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
            credentials {
                username sonatypeUsername
                password sonatypePassword
            }
        }
    }
}

However, this is not enough to satisfy Maven Central’s requirements. They want additional some of your details in the .pom file so lets add those.

def customizePom(pom, gradleProject) {
    pom.withXml {
        def root = asNode()
 
        // eliminate test-scoped dependencies (no need in maven central POMs)
        root.dependencies.removeAll { dep ->
            dep.scope == 'test'
        }
 
        // add all items necessary for maven central publication
        root.children().last() + {
            resolveStrategy = Closure.DELEGATE_FIRST
 
            description gradleProject.description
            name 'BTCMarkets-Java'
            url 'https://github.com/MannanM/btcmarkets-java'
            organization {
                name 'Mannan Live'
                url 'http://mannanlive.com'
            }
            licenses {
                license {
                    name 'The New BSD License'
                    url 'https://github.com/MannanM/btcmarkets-java/blob/master/LICENSE'
                    distribution 'repo'
                }
            }
            scm {
                url 'https://github.com/MannanM/btcmarkets-java'
                connection 'scm:git:git://github.com/MannanM/btcmarkets-java.git'
                developerConnection 'scm:git:ssh://git@github.com/MannanM/btcmarkets-java.git'
            }
            developers {
                developer {
                    id 'Mannan'
                    name 'Mannan Mackie'
                    email 'whatabout@gmail.com'
                    timezone '+10'
                }
            }
        }
    }
}

To call this method we just need to add customizePom(pom, rootProject) above the from components.java. But wait, there’s more. They also mandate that javadocs and source code is also upload. Don’t worry if you have annotated your classes with proper javadoc comments, it will work anyway. So let’s define a couple more artifacts.

task sourcesJar(type: Jar, dependsOn: classes) {
    classifier = 'sources'
    from sourceSets.main.allSource
}
 
task javadocJar(type: Jar, dependsOn: javadoc) {
    classifier = 'javadoc'
    from javadoc.destinationDir
}
 
artifacts {
    archives jar
    archives sourcesJar
    archives javadocJar
}

We still have to tell the publish task to include these two archives, we don’t need to mention the core jar as that is included by default. Your publication closure should now look like this.

    publications {
        mavenJava(MavenPublication) {
            customizePom(pom, rootProject)
            from components.java
 
            artifact sourcesJar
            artifact javadocJar
        }
    }

The final hurdle is to also include a signed file for each upload artefact. Don’t panic, below is a step-by-step guide on how to do it.

Signing the Artifacts

Another requirement to uploading to Maven Central is to sign your artefacts. If you don’t have gpg2 on the command line you will need to download it. If you open up a new terminal window.

> ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
> brew install gnupg

The next step is to generate a key.

> sudo gpg2 --full-generate-key
Please select what kind of key you want:
   (1) RSA and RSA (default)
   (2) DSA and Elgamal
   (3) DSA (sign only)
   (4) RSA (sign only)
Your selection? 1
 
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048)
Requested keysize is 2048 bits
 
Please specify how long the key should be valid.
         0 = key does not expire
        = key expires in n days
      w = key expires in n weeks
      m = key expires in n months
      y = key expires in n years
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: Mannan Mackie
Email address: email@email.com
Comment:
You selected this USER-ID:
    "Mannan Mackie <email@email.com>"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? O

You will also need to set a password to go along with the key. Make sure you record this somewhere safe as it can’t be reset.

The next step is to get the short id of the key as we will also need this for later.

> sudo gpg2 --list-keys --keyid-format short

You are looking for the 8 characters after rsa2048. For example rsa2048/ABCDEFGH
We then need to upload this to a keyserver. Note the ipv4 sub-domain, most tutorials don’t have this and I couldn’t get it working without.

> sudo gpg2 --keyserver hkp://ipv4.pool.sks-keyservers.net --send-keys ABCDEFGH

The final gpg command is to export the keys to a secure file which is need for the Gradle script. On old versions of gpg this file was generated automatically but in new versions you have to manually export it.

> sudo gpg2 --export-secret-keys -o secring.gpg

Now we need to let Gradle know how to access this key. We need to edit the gradle.properties file again and add the details from above in three new properties.

> vi /Users/mannan/.gradle/gradle.properties
signing.keyId=ABCDEFGH
signing.password=YourPassword
signing.secretKeyRingFile=/Users/mannan/.gnupg/secring.gpg

Add Signing to Gradle

We now need to add a signing plugin to our our build.gradle, configure it to sign our archives and ensure it runs before the publish task.

plugins {
    id 'java'
    id 'signing'
    id 'maven-publish'
}
 
signing {
    sign configurations.archives
}
 
model {
    tasks.publishMavenJavaPublicationToMavenLocal {
        dependsOn(project.tasks.signArchives)
    }
    tasks.publishMavenJavaPublicationToMavenRepository {
        dependsOn(project.tasks.signArchives)
    }
}

The final change (finally) is to include each signed file in our pom by modifying our mavenJava to include each one.

        mavenJava(MavenPublication) {
            customizePom(pom, rootProject)
            from components.java
 
            artifact sourcesJar
            artifact javadocJar
 
            // sign the pom
            pom.withXml {
                def pomFile = file("${project.buildDir}/generated-pom.xml")
                writeTo(pomFile)
                def pomAscFile = signing.sign(pomFile).signatureFiles[0]
                artifact(pomAscFile) {
                    classifier = null
                    extension = 'pom.asc'
                }
                pomFile.delete()
            }
 
            // sign the artifacts
            project.tasks.signArchives.signatureFiles.each {
                artifact(it) {
                    def matcher = it.file =~ /-(sources|javadoc)\.jar\.asc$/
                    println it.file
                    if (matcher.find()) {
                        classifier = matcher.group(1)
                    } else {
                        classifier = null
                    }
                    extension = 'jar.asc'
                }
            }
        }

Done! Now we should be able to run ./gradlew publish and up up and away our archives go to Nexus. One trouble I ran into was a permission/file not found issue with secring.gpg so I had to change the user on the file from root to my account using the chown command.

Releasing the Artifact

Once the publish task is successful, your artifacts are now uploaded. However they are only uploaded to a staging repository so we will need to go ahead and release them. To do this, log into Nexus using the same Sonatype JIRA account credentials from the very beginning. Click on the Staging Repositories link on the left, then search for your group, e.g. com.mannanlive. Once you have found it, check it and click “Close” (seems counter-intuitive) and then hit “Confirm”. Then click on the Activity tab and hopefully see lots of passed checks. If you see any failures, make sure you have followed all of the steps above.

Once everything has passed, check your repository again and click “Release” and “Confirm”. Once this is finished, it doesn’t appear straight away, give it some time. Eventually though you should be able to Maven Search and find your artifact. Congratulations on getting published, you deserve it.

If you want to view a sample build.gradle file, you can check out btcmarkets-java which I used as a guinea pig for this tutorial.

Leave a Reply


%d bloggers like this: