In this article, I want to take a detailed look at the process of publishing a Java artifact from scratch through Github Actions to the Sonatype Maven Central Repository using the Gradle builder.
I decided to write this article due to the lack of a normal tutorial in one place. All the information had to be collected piece by piece from various sources, moreover, not entirely fresh. Who cares, welcome under cat.
Creating a repository in Sonatype
The first step is to create a repository in Sonatype Maven Central. For this we go , register and create a new task, asking us to create a repository. We drive in our GroupId project Project URL project link and SCM url a link to the version control system in which the project is located. GroupId here should be of the form com.example, com.example.domain, com.example.testsupport, and can also be in the form of a link to your github: -> io.github.yourusername. In any case, you will need to verify ownership of this domain or profile. If you specified a github profile, you will be asked to create a public repository with the desired name.
Some time after confirmation, your GroupId will be created and we can move on to the next step, Gradle configuration.
Configuring Gradle
At the time of writing, I did not find Gradle plugins that could help with publishing the artifact. the only plugin that I found, however, the author refused to further support it. Therefore, I decided to do everything myself, since it is not too difficult to do this.
The first thing to figure out is Sonatype's requirements for publishing. They are the following:
- Availability of source codes and JavaDoc, ie. must attend
-sources.jarи-javadoc.jarfiles. As stated in the documentation, if it is not possible to provide source codes or documentation, you can make a dummy-sources.jaror-javadoc.jarwith a simple README inside to pass the test. - All files must be signed with
GPG/PGPand.ascthe file containing the signature must be included for each file. - Availability
pomFile - Correct values
groupId,artifactIdиversion. The version can be an arbitrary string and cannot end with-SNAPSHOT - Presence required
name,descriptionиurl - The presence of information about the license, developers and version control system
These are the basic rules that must be followed when publishing. Full information available .
We implement these requirements in build.gradle file. First, let's add all the necessary information about the developers, licenses, version control system, and also set the url, name and description of the project. Let's write a simple method for this:
def customizePom(pom) {
pom.withXml {
def root = asNode()
root.dependencies.removeAll { dep ->
dep.scope == "test"
}
root.children().last() + {
resolveStrategy = DELEGATE_FIRST
description 'Some description of artifact'
name 'Artifct name'
url 'https://github.com/login/projectname'
organization {
name 'com.github.login'
url 'https://github.com/login'
}
issueManagement {
system 'GitHub'
url 'https://github.com/login/projectname/issues'
}
licenses {
license {
name 'The Apache License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
scm {
url 'https://github.com/login/projectname'
connection 'scm:https://github.com/login/projectname.git'
developerConnection 'scm:git://github.com/login/projectname.git'
}
developers {
developer {
id 'dev'
name 'DevName'
email 'email@dev.ru'
}
}
}
}
}Next, you need to specify that during the assembly generated -sources.jar и-javadoc.jar files. For this section java you need to add the following:
java {
withJavadocJar()
withSourcesJar()
}Let's move on to the last requirement, setting up a GPG/PGP signature. To do this, connect the plugin signing:
plugins {
id 'signing'
}And add a section:
signing {
sign publishing.publications
}Finally, let's add a section publishing:
publishing {
publications {
mavenJava(MavenPublication) {
customizePom(pom)
groupId group
artifactId archivesBaseName
version version
from components.java
}
}
repositories {
maven {
url "https://oss.sonatype.org/service/local/staging/deploy/maven2"
credentials {
username sonatypeUsername
password sonatypePassword
}
}
}
}Here sonatypeUsername и sonatypePassword variables containing the login and password created during registration on .
Thus the final build.gradle will look like this:
Full build.gradle code
plugins {
id 'java'
id 'maven-publish'
id 'signing'
}
java {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
withJavadocJar()
withSourcesJar()
}
group 'io.github.githublogin'
archivesBaseName = 'projectname'
version = System.getenv('RELEASE_VERSION') ?: "0.0.1"
repositories {
mavenCentral()
}
dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.2'
}
test {
useJUnitPlatform()
}
jar {
from sourceSets.main.output
from sourceSets.main.allJava
}
signing {
sign publishing.publications
}
publishing {
publications {
mavenJava(MavenPublication) {
customizePom(pom)
groupId group
artifactId archivesBaseName
version version
from components.java
}
}
repositories {
maven {
url "https://oss.sonatype.org/service/local/staging/deploy/maven2"
credentials {
username sonatypeUsername
password sonatypePassword
}
}
}
}
def customizePom(pom) {
pom.withXml {
def root = asNode()
root.dependencies.removeAll { dep ->
dep.scope == "test"
}
root.children().last() + {
resolveStrategy = DELEGATE_FIRST
description 'Some description of artifact'
name 'Artifct name'
url 'https://github.com/login/projectname'
organization {
name 'com.github.login'
url 'https://github.com/githublogin'
}
issueManagement {
system 'GitHub'
url 'https://github.com/githublogin/projectname/issues'
}
licenses {
license {
name 'The Apache License, Version 2.0'
url 'http://www.apache.org/licenses/LICENSE-2.0.txt'
}
}
scm {
url 'https://github.com/githublogin/projectname'
connection 'scm:https://github.com/githublogin/projectname.git'
developerConnection 'scm:git://github.com/githublogin/projectname.git'
}
developers {
developer {
id 'dev'
name 'DevName'
email 'email@dev.ru'
}
}
}
}
}I want to note that we get the version from the environment variable: System.getenv('RELEASE_VERSION'). We will expose it during assembly and take it from the tag name.
PGP key generation
One of Sonatype's requirements is that all files be signed with a GPG/PGP key. For this we go and download the GnuPG utility for your operating system.
- We generate a key pair:
gpg --gen-key, enter a username, e-mail, and also set a password. - Find out
idour key with the command:gpg --list-secret-keys --keyid-format short. Id will be specified after the slash, for example: rsa2048/9B695056 - Publishing the public key to the server command:
gpg --keyserver [https://keys.openpgp.org](https://keys.openpgp.org/) --send-keys 9B695056 - We export the secret key to an arbitrary place, we will need it in the future:
gpg --export-secret-key 9B695056 > D:\gpg\9B695056.gpg
Setting up Github Actions
Let's move on to the final stage, set up the build and auto-publish using Github Actions.
Github Actions is a feature that allows you to automate the workflow by implementing a full CI / CD cycle. Build, test, and deploy can be triggered by various events: code push, release creation, or issues. This functionality is absolutely free for public repositories.
In this section, I'll show you how to set up build and push code and deploy to the Sonatype repository on release, as well as set up secrets.
We set secrets
For automatic assembly and deployment, we need a number of secret values, such as the key id, the password that we entered when generating the key, the PGP key itself, and the Sonatype login/password. You can set them in a special section in the repository settings:

We set the following variables:
- SONATYPE_USERNAME / SONATYPE_PASSWORD - login / password that we entered when registering with Sonatype
- SIGNING_KEYID/SIGNING_PASSWORD — PGP key id and password set during generation.
I want to dwell on the GPG_KEY_CONTENTS variable in more detail. The fact is that for publication we need a private PGP key. In order to post it in the secrets, I used and additionally made a number of actions.
- Let's encrypt our key with gpg:
gpg --symmetric --cipher-algo AES256 9B695056.gpgby entering a password. It should be placed in a variable: SECRET_PASSPHRASE - Let's translate the received encrypted key into a text form using base64:
base64 9B695056.gpg.gpg > 9B695056.txt. The content will be placed in the variable: GPG_KEY_CONTENTS.
Build setup when pushing code and creating PR
First you need to create a folder in the root of your project: .github/workflows.
In it, mark up the file, for example, gradle-ci-build.yml with the following content:
name: build
on:
push:
branches:
- master
- dev
- testing
pull_request:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 8
uses: actions/setup-java@v1
with:
java-version: 8
- name: Build with Gradle
uses: eskatos/gradle-command-action@v1
with:
gradle-version: current
arguments: build -PsonatypeUsername=${{secrets.SONATYPE_USERNAME}} -PsonatypePassword=${{secrets.SONATYPE_PASSWORD}}This workflow will be executed when pushing to branches master, dev и testing, also when creating pull requests.
The jobs section specifies the steps to be executed on the specified events. In this case, we will build on the latest version of ubuntu, use Java 8, and also use the plugin for Gradle eskatos/gradle-command-action@v1which, using the latest version of the builder, will run the commands specified in arguments. Variables secrets.SONATYPE_USERNAME и secrets.SONATYPE_PASSWORD these are the secrets we asked earlier.
The build results will be reflected in the Actions tab:

Auto-deploy when a new release is released
Let's create a separate workflow file for autodeploy gradle-ci-publish.yml:
name: publish
on:
push:
tags:
- 'v*'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
- name: Set up JDK 8
uses: actions/setup-java@v1
with:
java-version: 8
- name: Prepare to publish
run: |
echo '${{secrets.GPG_KEY_CONTENTS}}' | base64 -d > publish_key.gpg
gpg --quiet --batch --yes --decrypt --passphrase="${{secrets.SECRET_PASSPHRASE}}"
--output secret.gpg publish_key.gpg
echo "::set-env name=RELEASE_VERSION::${GITHUB_REF:11}"
- name: Publish with Gradle
uses: eskatos/gradle-command-action@v1
with:
gradle-version: current
arguments: test publish -Psigning.secretKeyRingFile=secret.gpg -Psigning.keyId=${{secrets.SIGNING_KEYID}} -Psigning.password=${{secrets.SIGNING_PASSWORD}} -PsonatypeUsername=${{secrets.SONATYPE_USERNAME}} -PsonatypePassword=${{secrets.SONATYPE_PASSWORD}}The file is almost identical to the previous one, except for the event in which it will be triggered. In this case, this is the event of creating a tag with a name starting with v.
Before deployment, we need to extract the PGP key from the secrets and place it in the root of the project, as well as decrypt it. Next, we need to set a special environment variable RELEASE_VERSION which we refer to gradle.build file. All this is done in the section Prepare to publish. We get our key from the GPG_KEY_CONTENTS variable, translate it into a gpg file, then decrypt it by putting it in the file secret.gpg.
Next, we turn to a special variable GITHUB_REF, from which we can get the version that we set when creating the tag. This variable is relevant in this case. refs/tags/v0.0.2 from which we cut off the first 11 characters to get a specific version. Next, we use the standard Gradle commands for publishing: test publish
Checking deployment results in Sonatype repository
After the release is created, the workflow described in the previous section should start. To do this, create a release:

the tag name must start with v. If, after clicking Publish release, the workflow successfully completes, we can go to to make sure:

The artifact appeared in the Staging repository. It immediately appears in the Open status, then it must be manually transferred to the Close status by pressing the appropriate button. After checking that all requirements are met, the artifact goes into the Close status and is no longer available for modification. In this form, it will end up in MavenCentral. If all is well, you can press the button Release, and the artifact will end up in the Sonatype repository.
In order for the artifact to get into MavenCentral, you need to ask for it in the task that we created at the very beginning. You only need to do this once, so we publish for the first time. In subsequent times, this is not required, everything will be synchronized automatically. They turned on synchronization for me quickly, but it took about 5 days for the artifact to become available in MavenCentral.
That's all, we have published our artifact in MavenCentral.
Useful links
- Similar , only publish via maven
- Staging sonatype
- Sonatype in which to create the task
- repository where it's all set up
Source: habr.com
