An internal need arose for us to publish code related to Respresso on Maven Central. So we started researching the internet to find the right way to deliver our artifacts to the central repository. At the time of writing this article, the following accepted repository providers are available:
On Maven’s website, in addition to these services, the OSSRH (Open Source Software Repository Hosting) is highlighted as the simplest publishing solution, maintained by Sonatype. Based on the documentation found on the Maven website, we concluded that OSSRH would be the most suitable for us, as our project did not fit into any of the previously listed providers. In the following, we will focus on publishing through OSSRH.
OSSRH Requirements
We began familiarizing ourselves with the publication process and its requirements. Currently, every publishing project must comply with the following rules:
- A JavaDoc jar and a sources jar must be generated
- Files must be signed with GPG/PGP
- Publishing information (groupId, artifactId, version) must appear in the Maven/Gradle configuration file
- The project must have a name and description
- A license must be provided for the project
- Developer information must be provided
- An SCM (Source Control Manager) connection must be available where the source code can be read
We were able to meet all requirements except the SCM information. This was a crucial issue because we did not want to make our source code public. Fortunately, we weren’t discouraged and thoroughly reviewed the documentation published by Maven regarding publication.
Our persistence paid off. It turned out that if the publication of the source code is prohibited by license, it is not mandatory to make it public, and publishing only the POM file is sufficient. This meant we could meet the requirement, and we proceeded with the entire process.
You can also read the official documentation about code publication, dear reader. It is especially worth paying attention to the "FAQ and common mistakes" section!
Registration and Project Initialization
Visit https://issues.sonatype.org, where you must register immediately. After logging in, you are directed to the “Create” surface, where the system asks you to create an Issue. If you are not taken there automatically, you can use the Create option. This Issue will be important later, as it is the channel through which you will communicate with the OSSRH administrators. It is advisable to fill out the form with as much detail as possible since other users can see it, comment on it, and this will save us from unnecessary obstacles later.
Sample form:
Project: Community Support - Open Source Project Repository Hosting (OSSRH)
Issue Type: New Project
Summary: A one-line summary of the project you want to publish.
Description: A more detailed description of the project. It’s recommended to specify in several lines what you will publish under the given groupId.
GroupId: the identifier representing the developer (e.g., ponte.hu)
Project url: a link containing the project description
SCM url: the version control location where the project or POM file is available
We filled out every field and waited for OSSRH to respond. Fortunately, we did not have to wait long — within 24 hours we received the necessary instructions. They required us to confirm that the ponte.hu domain was under our ownership. To prove this, we had to set up a DNS record they provided and send them an email from a ponte.hu address. After these steps, we passed the verification within a few days and were ready to publish our code.
First Deploy
We successfully registered and were eager to publish our code. Since OSSRH provides a SNAPSHOT repository, and publishing a SNAPSHOT version only requires registration — without fulfilling the full set of requirements mentioned earlier — we had no obstacles. Publishing the SNAPSHOT version is, of course, optional, but it provides a suitable environment for testing your code before releasing an official version.
Using a SNAPSHOT version allows us to overwrite previous publications while keeping the same version number. Dependency managers check (almost) every build to see if there is a newer version of the dependency. The “almost” only reflects timing considerations, as checking the repository every minute would be unnecessary.
If you use Maven as your dependency manager, configure it as follows:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>hu.ponte.sample</groupId>
<artifactId>sample-artifact</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>Sample deploy project</name>
<description>This is a sample project to introduce a publish process.</description>
<url>https://github.com/pontehu</url>
<inceptionYear>2019</inceptionYear>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
</distributionManagement>
<build>
<plugins>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>current_version</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
</plugins>
</build>
</project>
Next, configure the privileges necessary for publishing. You can do this in your settings.xml file located at USER_HOME/.m2/settings.xml:
<settings>
<servers>
<server>
<id>ossrh</id>
<username>your-jira-id</username>
<password>your-jira-password</password>
</server>
</servers>
</settings>
If you use the Gradle dependency manager, apply a small trick before writing your publishing logic. Separate the publishing process into a dedicated Gradle file, which we will name mavencentral.gradle. Place this file in the same directory as your build.gradle file, which will import it. After the project module type in your build.gradle, insert the following line:
apply from: 'mavencentral.gradle'
From now on, we will only work inside the newly created mavencentral.gradle file. Extend it with the following lines:
apply plugin: 'maven'
def getSnapshotRepositoryUrl() {
return "https://oss.sonatype.org/content/repositories/snapshots/"
}
afterEvaluate { project ->
uploadArchives {
repositories {
mavenDeployer {
pom.groupId = 'hu.ponte.sample'
pom.artifactId = 'sample-artifact'
pom.version = '0.0.1-SNAPSHOT'
snapshotRepository(url: getSnapshotRepositoryUrl()) {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
name = 'Sample deploy project'
packaging = 'jar/aar'
description = 'This is a sample project to introduce a publish process'
url = 'https://github.com/pontehu'
scm { url = 'https://github.com/pontehu'
connection = 'scm:git:git://github.com/pontehu/sample.git'
developerConnection = 'scm:git:git://github.com/pontehu/sample.git'
}
}
}
}
}
}
dependencies { }
Now set the necessary privileges for publishing in your properties.gradle file:
ossrhUsername=sampleUser
ossrhPassword=samplePass
The gradle.properties file can be loaded from several locations:
- the project root directory (where the build script is located)
- a submodule directory
- the Gradle user home (USER_HOME/.gradle)
Then select the uploadArchives task. You can verify the success of your deployment immediately by visiting: https://oss.sonatype.org/content/repositories/snapshots/ All successfully published projects appear here, and you can already use them as dependencies.
<repositories>
<repository>
<id>oss</id>
<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
</repository>
</repositories>
buildscript {
repositories {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}
}
allprojects {
repositories {
maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' }
}
}
Now for the Release Version
We need to configure a few additional settings for the release version. Up to this point, we have not fully met the requirements mentioned earlier, so we will complete what’s missing, as fulfilling all criteria is essential for publishing a release version.
Javadoc
We must generate an uploaded-project-name-javadoc.jar file for the artifact we want to upload. If the published code is written in Java, Maven’s javadoc dependency may be sufficient:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
<version>current_version</version>
<executions>
<execution>
<id>attach-javadocs</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
However, we wrote Kotlin code and wanted to generate Javadoc from it. Accordingly, our package hierarchy was also kotlin/hu/ponte/sample/… In this case, the standard Maven javadoc dependency was not suitable. We needed a tool capable of generating documentation from Kotlin files — and that is when we found Dokka.
<build>
<plugins>
<plugin>
<groupId>org.jetbrains.dokka</groupId>
<artifactId>dokka-maven-plugin</artifactId>
<version>current_version</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>javadocJar</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceDirectories>
<dir>src/main/kotlin</dir>
</sourceDirectories>
</configuration>
</plugin>
</plugins>
</build>
buildscript {
repositories {
jcenter()
}
dependencies {
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
}
}
apply plugin: 'org.jetbrains.dokka'
dokka {
outputFormat = 'javadocJar'
// These tasks will be used to determine source directories and classpath
sourceDirs = files('src/main/kotlin')
}
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:${dokka_version}"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply plugin: 'org.jetbrains.dokka-android'
dokka {
moduleName = 'data'
outputFormat = 'javadoc'
outputDirectory = "$buildDir/javadoc"
sourceDirs = files('src/main/java')
}
task androidJavadocsJar(type: Jar) {
classifier = 'javadoc'
from android.sourceSets.main.java.srcDirs
}
artifacts {
archives androidJavadocsJar
}
If you want to generate documentation from Groovy code:
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:$gradle_version'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.dokka:dokka-gradle-plugin:${dokka_version}"
classpath "org.jetbrains.dokka:dokka-android-gradle-plugin:${dokka_version}"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
apply plugin: 'org.jetbrains.dokka'
dokka {
moduleName = 'data'
outputFormat = 'javadoc'
outputDirectory = "$buildDir/javadoc"
sourceDirs = files('src/main/groovy')
}
task androidJavadocsJar(type: Jar) {
classifier = 'javadoc'
from sourceSets.main.runtimeClasspath
}
artifacts {
archives androidJavadocsJar
}
After Javadoc, a sources jar must also be generated. For this, the standard Maven source plugin works well:
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>current_version</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
task sourcesJar(type: Jar) {
classifier = 'sources'
from sourceSets.main.allSource
}
artifacts {
archives sourcesJar
}
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.sourceFiles
}
artifacts {
archives androidJavadocsJar
archives sourcesJar
}
Encrypting Files Using GPG/PGP
We must sign our source code before publishing it, so we need a GPG client. You can find the version suitable for your operating system at: https://www.gnupg.org/download/.
Detailed instructions for generating and sharing keys can be found here: https://central.sonatype.org/pages/working-with-pgp-signatures.html. After generating your key successfully, use it through your publishing plugins.
On Windows, restart Android Studio or IntelliJ after installing the GPG client.
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>current_version</version>
<executions>
<execution>
<id>ossrh</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
apply plugin: 'signing'
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
useGpgCmd()
sign configurations.archives
}
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
apply plugin: 'signing'
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
useGpgCmd()
sign configurations.archives
}
Insert the following lines into your gradle properties file:
signing.gnupg.executable=gpg
signing.gnupg.useLegacyGpg=true
signing.gnupg.homeDir=../../../../Users/USER_NAME/AppData/Roaming/gnupg
The path is required because the GPG client running under Gradle needs the pubring and associated files to operate correctly. (After importing the key, the client generates these files automatically.) The above signature corresponds to Windows — on Linux and Mac, the files are found in the user's home directory under the appropriate path.
To publish to the release server, we must generate a user token and password. Visit: https://oss.sonatype.org/ and log in. Click your username in the top right corner, select Profile, then choose the User Token option, followed by Access User Token. These credentials will be required for release publication. Keep them secure! Add them to your gradle.properties and settings.xml.
<settings>
<servers>
<server>
<id>ossrh</id>
<username>mavenUsername</username>
<password>mavenPassword</password>
</server>
</servers>
</settings>
mavenUsername=aaa
mavenPassword=bbb
signin.keyId=2134
Adding License and Developer Information
It is worth taking time to choose the right license, as it defines how others can use your code. You can modify the license later — either relaxing or restricting it.
<organization>
<name>Ponte.hu Kft.</name>
<url>https://ponte.hu</url>
</organization>
<licenses>
<license>
<name>Sync client MIT License</name>
<url>https://github.com/pontehu/.../LICENSE.txt</url>
</license>
</licenses>
<developers>
<developer>
<name>Ponte</name>
<email>info@respresso.io</email>
<organization>ponte.hu Kft.</organization>
<organizationUrl>https://ponte.hu</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/pontehu/sample.git</connection>
<developerConnection>scm:git:ssh://github.com:pontehu/sample.git</developerConnection>
<url>https://github.com/pontehu/respresso-sync-for-clients</url>
</scm>
url = 'https://github.com/pontehu/respresso-client-android'
scm {
url = 'https://github.com/pontehu/sample'
connection = 'scm:git:git://github.com/pontehu/sample.git'
developerConnection = 'scm:git:ssh://github.com:pontehu/sample.git'
}
licenses {
license {
name = 'Sync client MIT License'
url = 'https://github.com/pontehu/.../LICENSE.txt'
}
}
developers {
developer {
id = 'pontehu'
name = 'Ponte'
email = 'info@respresso.io'
organization = 'ponte.hu Kft.'
organizationUrl = 'https://ponte.hu'
}
}
Final OSS Steps
After a successful deploy, go to https://oss.sonatype.org. Log in and select Staging Repositories from the left menu. Among the listed elements, you will find your uploaded artifact. Select it and check under the Activity tab whether all actions have run successfully. You can then choose from the following options:
- Use the Drop option to delete your code. This allows you to fix issues and rerun the deployment process.
- Or click Close if you want to proceed with publishing. After closing, select Release to send your artifact to Maven Central. Publishing may take 1–3 hours. After successful publication, verify that everything worked correctly. It is advisable to post an additional comment to OSSRH confirming that the first version has been released so they can activate the synchronization process. After activation, newly published release versions will appear automatically and become usable within about 15 minutes. However, the Maven Central front-end may take several hours to update.
Final Versions
We have not yet added license and developer information to the dependency manager configuration, so let’s finalize these now. Here are the final versions of the files:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<groupId>hu.ponte.sample</groupId>
<artifactId>sample-artifact</artifactId>
<version>0.0.1</version>
<packaging>jar</packaging>
<modelVersion>4.0.0</modelVersion>
<name>Sample deploy project</name>
<description>This is a sample project to introduce a publish process</description>
<url>https://github.com/pontehu</url>
<inceptionYear>2019</inceptionYear>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<organization>
<name>Ponte.hu Kft.</name>
<url>https://ponte.hu</url>
</organization>
<licenses>
<license>
<name>Sync client MIT License</name>
<url>https://github.com/pontehu/.../LICENSE.txt</url>
</license>
</licenses>
<developers>
<developer>
<name>Ponte</name>
<email>info@ponte.hu</email>
<organization>ponte.hu Kft.</organization>
<organizationUrl>https://ponte.hu</organizationUrl>
</developer>
</developers>
<scm>
<connection>scm:git:git://github.com/pontehu/sample.git</connection>
<developerConnection>scm:git:ssh://github.com:pontehu/sample.git</developerConnection>
<url>https://github.com/pontehu/sample</url>
</scm>
<dependencies>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>current_version</version>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<version>current_version</version>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.sonatype.plugins</groupId>
<artifactId>nexus-staging-maven-plugin</artifactId>
<version>current_version</version>
<extensions>true</extensions>
<configuration>
<serverId>ossrh</serverId>
<nexusUrl>https://oss.sonatype.org/</nexusUrl>
<autoReleaseAfterClose>true</autoReleaseAfterClose>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>current_version</version>
<executions>
<execution>
<id>ossrh</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.jetbrains.dokka</groupId>
<artifactId>dokka-maven-plugin</artifactId>
<version>current_version</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>javadocJar</goal>
</goals>
</execution>
</executions>
<configuration>
<sourceDirectories>
<dir>src/main/kotlin</dir>
</sourceDirectories>
</configuration>
</plugin>
</plugins>
</build>
<distributionManagement>
<snapshotRepository>
<id>ossrh</id>
<url>https://oss.sonatype.org/content/repositories/snapshots</url>
</snapshotRepository>
<repository>
<id>ossrh-staging</id>
<url>https://oss.sonatype.org/service/local/staging/deploy/maven2/</url>
</repository>
</distributionManagement>
</project>
apply plugin: 'maven'
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'org.jetbrains.dokka
sourceSets {
main {
java {
srcDir 'src/groovy'
}
resources {
srcDir 'src/resources'
}
}
}
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
def getReleaseRepositoryUrl() {
return "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}
def getSnapshotRepositoryUrl() {
return "https://oss.sonatype.org/content/repositories/snapshots/"
}
afterEvaluate { project ->
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
pom.groupId = 'hu.ponte.sample'
pom.artifactId = 'sample-artifact'
pom.version = '0.0.1'
repository(url: getReleaseRepositoryUrl()) {
authentication(userName: mavenUsername, password: mavenPassword)
}
snapshotRepository(url: getSnapshotRepositoryUrl()) {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
name = 'Sample deploy project'
packaging = 'jar'
description = 'This is a sample project to introduce a publish process'
url = 'https://github.com/pontehu/sample'
scm {
url = 'https://github.com/pontehu/sample'
connection = 'scm:git:git://github.com/pontehu/sample.git'
developerConnection = 'scm:git:git://github.com/pontehu/sample.git'
}
licenses {
license {
name = 'Sync client MIT License'
url = 'https://github.com/pontehu/.../LICENSE.txt'
}
}
developers {
developer {
id = 'pontehu'
name = 'Ponte'
email = 'info@ponte.hu'
organization = 'ponte.hu Kft.'
organizationUrl = 'https://ponte.hu'
}
}
}
}
}
}
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
// useGpgCmd()
sign configurations.archives
}
task androidJavadocs(type: Javadoc) {
source = sourceSets.main.java.srcDirs
classpath += sourceSets.main.runtimeClasspath
}
task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) {
classifier = 'javadoc'
from sourceSets.main.allSource
}
task androidSourcesJar(type: Jar) {
classifier = 'sources'
from sourceSets.main.allSource
}
artifacts {
archives androidSourcesJar
archives androidJavadocsJar
}
}
dependencies {}
apply plugin: 'maven'
apply plugin: 'maven-publish'
apply plugin: 'signing'
apply plugin: 'org.jetbrains.dokka-android'
dokka {
moduleName = 'data'
outputFormat = 'javadoc'
outputDirectory = "$buildDir/javadoc"
sourceDirs = files('src/main/java')
}
task androidJavadocsJar(type: Jar) {
classifier = 'javadoc'
from android.sourceSets.main.java.srcDirs
}
task sourcesJar(type: Jar) {
classifier = 'sources'
from android.sourceSets.main.java.sourceFiles
}
def isReleaseBuild() {
return version.contains("SNAPSHOT") == false
}
def getReleaseRepositoryUrl() {
return "https://oss.sonatype.org/service/local/staging/deploy/maven2/"
}
def getSnapshotRepositoryUrl() {
return "https://oss.sonatype.org/content/repositories/snapshots/"
}
afterEvaluate { project ->
uploadArchives {
repositories {
mavenDeployer {
beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) }
pom.groupId = 'hu.ponte.sample'
pom.artifactId = 'sample-artifact'
pom.version = '0.0.1'
repository(url: getReleaseRepositoryUrl()) {
authentication(userName: mavenUsername, password: mavenPassword)
}
snapshotRepository(url: getSnapshotRepositoryUrl()) {
authentication(userName: ossrhUsername, password: ossrhPassword)
}
pom.project {
name = 'Sample deploy project'
packaging = 'aar'
description = 'This is a sample project to introduce a publish process'
url = 'https://github.com/pontehu/sample'
scm {
url = 'https://github.com/pontehu/sample'
connection = 'scm:git:git://github.com/pontehu/sample.git'
developerConnection = 'scm:git:git://github.com/pontehu/sample.git'
}
}
}
}
}
signing {
required { isReleaseBuild() && gradle.taskGraph.hasTask("uploadArchives") }
// useGpgCmd()
sign configurations.archives
}
artifacts {
archives androidJavadocsJar
archives sourcesJar
}
}
dependencies {}