Jago de Vreede, Author at foojay https://foojay.io/today/author/jago-de-vreede/ a place for friends of OpenJDK Fri, 12 Dec 2025 12:03:48 +0000 en-US hourly 1 https://wordpress.org/?v=6.9.4 https://foojay.io/wp-content/uploads/2020/04/Favicon-3-2-150x150.png Jago de Vreede, Author at foojay https://foojay.io/today/author/jago-de-vreede/ 32 32 How to publish a Java Maven project to Maven Central using JReleaser and GitHub Actions (2025 Guide) https://foojay.io/today/how-to-publish-a-java-maven-project-to-maven-central-using-jreleaser-and-github-actions-2025-guide/ https://foojay.io/today/how-to-publish-a-java-maven-project-to-maven-central-using-jreleaser-and-github-actions-2025-guide/#respond Fri, 12 Dec 2025 07:42:00 +0000 https://foojay.io/?p=121932 Table of Contents PreconditionsGPG keyCoordinate (group-id)Preparing your projectJReleaserInstalling locallyJReleaser configurationThe actual local releaseStagingReleaseCheck progress in Maven Central repository This article is a tutorial that guides you through the process of releasing a Java module with JReleaser to Maven Central with ...

The post How to publish a Java Maven project to Maven Central using JReleaser and GitHub Actions (2025 Guide) appeared first on foojay.

]]>
Table of Contents
PreconditionsJReleaserThe actual local releaseGitHub actionMaven pluginResources used

This article is a tutorial that guides you through the process of releasing a Java module with JReleaser to Maven Central with Github Actions. It is a 2025 update of the foojay.io/today/how-to-release-a-java-module-with-jreleaser-to-maven-central-with-github-actions article.

JReleaser is a tool that streamlines the release process for Java projects, allowing developers to quickly and efficiently publish their modules to Maven Central.

This article will use the SemVer Check project as an example project that uses Maven as a build tool.

Preconditions

In order to publish to Maven central, you will need to have a GPG key and have a group-id (coordinate) registered.

GPG key

You will need a GPG key to sign the artifacts, this will allow users to verify that they have the correct package.

  • Download GPG key or install it with your favorite package manager.
  • Generate a public key with (remember the password as we're going to need it later)
    gpg --gen-key
  • Now find the id of your key with
    gpg --list-keys --keyid-format=long

    The output should look something like this:

    /Users/jagodevreede/.gnupg/pubring.kbx
    --------------------------------------
    pub   rsa4096/XXXXXXXX9925B017 2022-11-17 [SC] [expires: 2026-11-17]
          C20FC085CF5B0D4D861E8CEDXXXXXXXX9925B017
    uid                 [ultimate] Jago de Vreede <redacted@mail.com>
    sub   rsa4096/XXXXXXXXXXXXFC74 2022-11-17 [E] [expires: 2026-11-17]

    In this case, the id of the public key is XXXXXXXXXXXXFC74

  • Publish your public key to a public server, for example Ubuntu:
    gpg --keyserver keyserver.ubuntu.com --send-keys XXXXXXXXXXXXFC74

Coordinate (group-id)

This process is actually very well documented at https://central.sonatype.org/publish/requirements/coordinates/.

Your group id can be your domain name (reverse) if you have that. Also, many Code Hosting services are supported like GitHub, GitLab, Gitee, Bitbucket, and SourceForge. In this example, we will use GitHub, so our group-id will be io.github.jagodevreede.

In order to "claim" this group-id you will need to create an account on the Central Portal at https://central.sonatype.com. During registration, you'll verify your namespace ownership. For GitHub-based namespaces like io.github.jagodevreede, verification is done by adding the provided verification key to a public repository. The portal will automatically verify ownership once the key is detected.

Preparing your project

Javadoc and sources

A project that is released to Maven Central requires that you attach javadoc and the sources.

This can be done by adding the 2 plugins to your build. These plugins don't need to run every time, so it's recommended to put them in a profile so they will only run when you need them to, or when you build a release.

A Maven example is as follows:

<profiles>
    <profile>
        <id>publication</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-javadoc-plugin</artifactId>
                    <!-- 3.12.0 is the current version at the time of writing, please check if there is a newer version -->
                    <version>3.12.0</version>
                    <executions>
                        <execution>
                            <id>attach-javadocs</id>
                            <goals>
                                <goal>jar</goal>
                            </goals>
                            <configuration>
                                <attach>true</attach>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-source-plugin</artifactId>
                    <!-- 3.4.0 is the current version at the time of writing, please check if there is a newer version -->
                    <version>3.4.0</version>
                    <executions>
                        <execution>
                            <id>attach-sources</id>
                            <goals>
                                <goal>jar</goal>
                            </goals>
                            <configuration>
                                <attach>true</attach>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Meta information in pom

Maven Central also requires metadata in your pom like a description, inception year, license, list of developers, and scm location.

Example configurations is:

<description>This is the root pom for the semver-check maven plugin</description>
<inceptionYear>2022</inceptionYear>

<licenses>
    <license>
        <name>Apache License, Version 2.0</name>
        <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
        <distribution>repo</distribution>
    </license>
</licenses>

<developers>
    <developer>
        <id>jagodevreede</id>
        <name>Jago de Vreede</name>
    </developer>
</developers>

<scm>
    <connection>scm:git:https://github.com/jagodevreede/semver-check.git</connection>
    <developerConnection>scm:git:https://github.com/jagodevreede/semver-check.git</developerConnection>
    <url>https://github.com/jagodevreede/semver-check.git</url>
    <tag>HEAD</tag>
</scm>

Deploy plugin version

We need to have at least version 3.0.0 of the Maven deploy version, so add the following to the root pom

<pluginManagement>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-deploy-plugin</artifactId>
            <version>3.1.4</version>
        </plugin>
    </plugins>
</pluginManagement>

JReleaser

Installing locally

Now with the preconditions out of the way, it is time to install JReleaser locally to verify that everything is working before we switch to GitHub Actions. I would recommend that you do the first release locally; that way you can easily fix any errors.

Go to jreleaser.org/guide/latest/install.html and follow the instructions to install the latest stable version of JReleaser.

We need to create a configuration file for JReleaser, all this config will be put into secrets on GitHub later. Create a file in your home folder ~/.jreleaser/config.properties

An example file will look something like this:

JRELEASER_GITHUB_TOKEN=ghp_eWVzIGFsc28gc2VjcmV0==
JRELEASER_GPG_SECRET_KEY=something_base64_with_around_6500+_chars
JRELEASER_GPG_PASSPHRASE=secret
JRELEASER_GPG_PUBLIC_KEY=something_base64_with_around_3000+_chars
JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_RELEASE_DEPLOY_USERNAME=p72a6s
JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_RELEASE_DEPLOY_PASSWORD=also-secret

GitHub token

First, we need a GitHub token that has access to the repository.

Go to Settings -> Developer settings -> tokens (classic) in your GitHub profile and create a token that has "repo" access. And place this in the JReleaser config file under the key JRELEASER_GITHUB_TOKEN.

GPG keys

When we generated the keys we also listed the keys, we need to have the id of the public key, in the example above it was XXXXXXXXXXXXFC74.

First, we need to export our private key as a base64 string and put it in the config file under JRELEASER_GPG_SECRET_KEY, we can do that with (note you will need the password that you used when you created the key):

gpg --export-secret-keys XXXXXXXXXXXXFC74 | base64

The JRELEASER_GPG_PASSPHRASE is the password we used when we exported the secret key.

Next is the public key, also a base64 encoded string 

gpg --export XXXXXXXXXXXXFC74 | base64

This we put in the config file under JRELEASER_GPG_PUBLIC_KEY.

Portal tokens

Lastly, we need to put the credentials that we used to upload to the portal. There is an excellent official guide on how to obtain a token here https://central.sonatype.org/publish/generate-portal-token/ put the username and password obtained in the last step of that guide in JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_RELEASE_DEPLOY_USERNAME and JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_RELEASE_DEPLOY_PASSWORD.

JReleaser configuration

We need to create a jreleaser.yml file for the project. This can be done with the cli we installed before, with the following command:

jreleaser init --format yml

You will need to edit the file and fill in the yml. This is where your copy-paste skills will shine, as almost all information can be found in the pom.xml.

Next, remove the distributions part and version from the yml. As the distribution will be added later, the version will be set via an environment variable.

Finally, add the configuration to push to Maven Central:

signing:
  active: ALWAYS
  armored: true

deploy:
  maven:
    mavenCentral:
      release-deploy:
        active: RELEASE
        url: https://central.sonatype.com/api/v1/publisher
        applyMavenCentralRules: true
        stagingRepositories:
          - target/staging-deploy

The actual local release

Now that all the preconditions and plumbing is out of the way it is time for the actual release

Prepare your Maven project to be released, so remove the -SNAPSHOT from your versions.

You can do that with the Maven versions plugin for example

mvn versions:set -DnewVersion=0.0.1

Staging

The release needs to be uploaded from a staging directory, to create that invoke the following command:

mvn -Ppublication deploy -DaltDeploymentRepository=local::file:./target/staging-deploy

Release

First, set the version that you will be releasing (this must be the same as what you got in your pom.xml)

export JRELEASER_PROJECT_VERSION=0.0.1

Then do the actual release with (note there is no staging area or anything so there is no way back after running this command):

jreleaser full-release

Check progress in Maven Central repository

You can login in to the Maven Central repository to see the progress. 

GitHub action

Now that we can release by hand it is time to automate this entire process!

Secrets

Before we can run JReleaser on GitHub, we first need to set our secrets in the secrets of the repository.

To keep things simple just copy all the key values from the JReleaser config file that was used locally.

And you will end up with something like this:

Note that in this example JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_RELEASE_DEPLOY_USERNAME
has been shortened to JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME

Workflow

First, create a release workflow by creating a release.yml file in your repository under .github/workflows/.

The first bit of the file is the name of the workflow and the input parameters used when you start the workflow.

name: Release
on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Release version'
        required: true
      nextVersion:
        description: 'Next version after release (-SNAPSHOT will be added automatically)'
        required: true

This will look something like this when you start the release workflow

Next up is just your default build setup, in this example, java 11 is used, but this is the same as for your normal build. Except  fetch-depth as JReleaser will use the git log to create the changelog it will need the full history, and thus we set the fetch-depth to 0. As it defaults to 1.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'
          cache: maven

Next, we need to set the version that we will be releasing, we can do that with the Maven versions plugin.

- name: Set release version
  run: mvn --no-transfer-progress --batch-mode versions:set -DnewVersion=${{ github.event.inputs.version }}

This change will be the code that will be released, so we want to commit that change. A tag will be created in the release process by JReleaser

- name: Commit & Push changes
  uses: actions-js/push@master
  with:
    github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }}
    message: Releasing version ${{ github.event.inputs.version }}

Now its time to stage the release, as we did manually

- name: Stage release
  run: mvn --no-transfer-progress --batch-mode -Ppublication clean deploy -DaltDeploymentRepository=local::default::file://`pwd`/target/staging-deploy

Then we can call JReleaser this is where we use the secrets we set up before.


- name: Run JReleaser
 uses: jreleaser/release-action@v2
 with:
   setup-java: false
   version: 1.20.0
 env:
   JRELEASER_PROJECT_VERSION: ${{ github.event.inputs.version }}
   JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }}
   JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
   JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}
   JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
   JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_RELEASE_DEPLOY_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_SONATYPE_USERNAME }}
   JRELEASER_DEPLOY_MAVEN_MAVENCENTRAL_RELEASE_DEPLOY_PASSWORD: ${{ secrets.JRELEASER_MAVENCENTRAL_SONATYPE_PASSWORD }}

When we are done we need to set the next development version and push that.

- name: Set release version
  run: mvn --no-transfer-progress --batch-mode versions:set -DnewVersion=${{ github.event.inputs.nextVersion }}
- name: Commit & Push changes
  uses: actions-js/push@master
  with:
    github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }}
    message: Setting SNAPSHOT version ${{ github.event.inputs.nextVersion }}-SNAPSHOT
    tags: true

The full file can be found here: https://github.com/jagodevreede/semver-check/blob/c9353fa86eb9ae8f6b309057748672a6c1e0f435/.github/workflows/release.yml

And with that, we are done! Now we can easily release our module to Maven Central with the press of a button in GitHub.

Maven plugin

There is also a JReleaser Maven plugin available that offers a Maven DSL to configure JReleaser. With that the jreleaser.yml file can be omitted as information can be read from the pom file instead. 

The use of the Maven DSL offers these benefits:

  • Reduce duplication in the release configuration
  • No need to install JReleaser's CLI
  • No need to use jreleaser/release-action on GitHub as invoking the Maven plugin is enough

So why is the CLI used in this article, you might ask? Well, the CLI can be used for projects other than Maven as well, and it demonstrates the capabilities of JReleaser.

Resources used

 

The post How to publish a Java Maven project to Maven Central using JReleaser and GitHub Actions (2025 Guide) appeared first on foojay.

]]>
https://foojay.io/today/how-to-publish-a-java-maven-project-to-maven-central-using-jreleaser-and-github-actions-2025-guide/feed/ 0
Video series “JavaFX In Action”, Part 3 https://foojay.io/today/video-series-javafx-in-action-part-3/ https://foojay.io/today/video-series-javafx-in-action-part-3/#respond Fri, 03 Jan 2025 09:38:59 +0000 https://foojay.io/?p=115050 Table of Contents Özkan Pakdil: Swaggerific, an open-source Postman alternative written in JavaFXClément de Tastes: QuarkusFX, combining the strengths of Quarkus and JavaFXAlmas Baim: FXGL, a multipurpose game library for JavaFXSteve Hannah: jDeploy, to distribute your Java app as a ...

The post Video series “JavaFX In Action”, Part 3 appeared first on foojay.

]]>
Table of Contents
Özkan Pakdil: Swaggerific, an open-source Postman alternative written in JavaFXClément de Tastes: QuarkusFX, combining the strengths of Quarkus and JavaFXAlmas Baim: FXGL, a multipurpose game library for JavaFXSteve Hannah: jDeploy, to distribute your Java app as a native bundleJago de Vreede: SDKman UI, a user interface on top of SDKMAN for all platforms

This is the next part in the series of "JavaFX in Action" interviews published in the last part of 2024. Are you working on a fantastic JavaFX application? Let me know, and let's discuss it in the new year!

Özkan Pakdil: Swaggerific, an open-source Postman alternative written in JavaFX

Özkan Pakdil is a seasoned full-stack developer with over 15 years of experience, deeply rooted in both back-end and front-end technologies. His expertise spans from Varnish cache servers and New Relic to Node.js, Quarkus, Micronaut, Rust, Java, JavaFX,...

Özkan decided to create an open-source alternative to Postman. He didn't have much experience yet with JavaFX and decided to use this project to learn more about it. The tool can open a Swagger URL and displays a tree view with the available calls. In the UI you can define paramaters and get a nice visual representation of the JSON output.

More info in this blog post.

Clément de Tastes: QuarkusFX, combining the strengths of Quarkus and JavaFX

Clément de Tastes is Technical Lead at SCIAM in France. He has a long history in software development and uses Quarkus in his daytime job. Out of personal interest, he created a Quarkus extension that allows him to combine it with JavaFX to use dependency injection and the many other extensions available for the Quarkus system.

QuarkusFX is a Quarkus extension that allows you to use JavaFX in your Quarkus application. It provides component injection in FX Controllers, allows you to use CDI events to register on primary stage creation, make use of CSS live reloading, provides annotations to write cleaner code, etc.

More info in this blog post.

Almas Baim: FXGL, a multipurpose game library for JavaFX

Almas Baim is Computing and Maths Department Lead at the University of Brighton and the creator of the JavaFX game library FXGL.

FXGL is a JavaFX Game Development Framework with the following highlights:

  • No installation or setup is required
  • "Out of the box": Java 8-21, Win/Mac/Linux/Android 8+/iOS 11.0+/Web
  • Simple and clean API, higher level than other engines
  • Superset of JavaFX: no need to learn new UI API
  • Real-world game development techniques: Entity-Component, interpolated animations, particles, and many more
  • Games are easily packaged into a single executable .jar, or native images

More info in this blog post.

Steve Hannah: jDeploy, to distribute your Java app as a native bundle

Steve Hannah is the creator of jDeploy, Xataface, SWeTE, PDF OCR X, and Java-Objective-C bridge. He's also the co-Founder of Web Lite Translation Corp. and software engineer at Codename One.

jDeploy helps you distribute your Java app as a native bundle to macOS, Linux, and Windows without the usual hassles. First, you create an executable JAR and then use jDeploy to publish it. Your users can then download a native app installer which also guarantees that they are on the latest version.

More info in this blog post.

Jago de Vreede: SDKman UI, a user interface on top of SDKMAN for all platforms

Jago de Vreede is a full-stack software engineer at OpenValue. As a software engineer he has seen a broad-spectrum of projects and he has been working on multiple large scale educational software and banking projects for the last years. His work is not exclusive to Java and Scala development but also does front-end development, and the integration between these.

SDKman UI aims to offer a (cross-platform) Graphical User Interface for SDKMAN. It extends the functionality of the terminal-tool SDKMAN with a user interface, but also makes the tool available for Window systems.

More info in this blog post.

The post Video series “JavaFX In Action”, Part 3 appeared first on foojay.

]]>
https://foojay.io/today/video-series-javafx-in-action-part-3/feed/ 0
(Semantic) Versioning your Java libraries https://foojay.io/today/semantic-versioning-your-java-libraries/ https://foojay.io/today/semantic-versioning-your-java-libraries/#comments Wed, 03 Jan 2024 15:10:35 +0000 https://foojay.io/?p=103575 Refine Java library versions seamlessly with Semantic Versioning, ensuring compatibility and efficient upgrades. Learn More!

The post (Semantic) Versioning your Java libraries appeared first on foojay.

]]>

Table of Contents


There are a lot of ways to version your library but the semantic versioning scheme is the most used and for a good reason, by looking at the version change you can already defer if you can upgrade the dependency without any problems or if you might have to do some refactoring. Semantic versioning proposes a simple set of rules and requirements that dictate how version numbers are assigned and incremented.

Given a version number MAJOR.MINOR.PATCH, increment the:

  • MAJOR version when you make incompatible API changes
  • MINOR version when you add functionality in a backward compatible manner
  • PATCH version when you make backward compatible bug fixed

If you do semantic versioning by hand then mistakes could slip in. I can’t remember how many times I've updated a library with a patch version that would then break because the API had changed significantly. For this reason, I’ve created the semver-check maven plugin. This plugin can check if the version of your library is set correctly.

The Semantic Versioning Specification gives us generic guidelines on how Semantic Versioning must be done. Still, it needs to be tailored for a specific language so it leaves room for mistakes when translating to a specific language.

For example, changing the Java compiler output from 11 to 17 without changing any code is often mistaken for a patch version, as no code has been changed. However, this is a breaking change for those who still use your library and are still on Java 11, so therefore, this change must be handled as a major change.

Versioning

Major
When you make incompatible API changes, this is implemented with the following checks:

  • Changing the java version of the compiled classes to a higher version.
  • Removal of a public class, method, field or static variable.
  • Removal of a resource file
  • Removal of an annotation on a public API

Minor
When you add functionality in a backward compatible manner, this is implemented with the following checks:

  • Addition of a public class, method, annotation, field or static variable.
  • Removal of an annotation on a non public API

Patch
When you make backward compatible bug fixes, this is implemented with the following checks:

  • Any change that changes the byte code
  • Any change in a resource file (Note that files in META-INF/maven/ are ignored as they are generated by maven)
  • Any change in a dependency

Integrating

There are two ways of integrating the automatic version check in your current pipeline build.

  1. With commandline arguments for example:
    mvn package io.github.jagodevreede:semver-check-maven-plugin:check
  2. and/or add it to the pom.xml (and replace VERSION_NUMBER with the latest released version)
<build>
    ...
    <plugins>
        ...
        <plugin>
            <artifactId>semver-check-maven-plugin</artifactId>
            <groupId>io.github.jagodevreede</groupId>
            <version>VERSION_NUMBER</version>
            <configuration>
                <haltOnFailure>true</haltOnFailure>
                <outputFileName>nextVersion.txt</outputFileName>
            </configuration>
            <executions>
                <execution>
                    <id>check</id>
                    <phase>verify</phase>
                    <goals>
                        <goal>check</goal>
                    </goals>
                </execution>
            </executions>
        </plugin>
        ...
    </plugins>
    ...
</build>

You need to run at least the package phase to be able to have valid results of the semver-check

The output of this run will be something like this:

[INFO] --- semver-check-maven-plugin:0.4.1:check (default-cli) @ some-module-core ---
Downloading from central: https://.../some-module/0.4.1/some-module-core-0.4.1.pom
Downloaded from central: https://.../some-module/0.4.1/some-module-core-0.4.1.pom (1.4 kB at 14 kB/s)
Downloading from central: https://.../some-module/0.4.1/some-module-core-0.4.1.jar
Downloaded from central: https://.../some-module/0.4.1/some-module-core-0.4.1.jar (16 kB at 156 kB/s)
[INFO] Looking up versions of org.acme.example:some-module-core
[INFO] Checking SemVer against last known version 0.4.1
[INFO] Class org.acme.example.MyClass has been changed on byte level
[INFO] Class org.acme.example.OtherClass has been changed on byte level
[INFO] File META-INF/MANIFEST.MF has been changed
[INFO] Determined SemVer type as patch and is currently none, next version should be: 0.4.2

The output gives us a summary of what the next version should be and a summary of why. Please note that the plugin stops searching for patch changes if it already detects a minor update for example. This keeps execution time as low as possible.

There are many configuration options available for the plugin check out the readme for all available options.

The plugin also writes a file in the target folder called nextVersion.txt by default. You can also make your pipeline read this file and set the next version to be released with for example the maven versions:set plugin

Both multimodule projects and single modules are supported. The plugin will suggest the highest version bump encountered in a project.

So try it out in your library and if you encounter any issues please raise them on github.

The post (Semantic) Versioning your Java libraries appeared first on foojay.

]]>
https://foojay.io/today/semantic-versioning-your-java-libraries/feed/ 4
How to Release a Java Module with JReleaser to Maven Central with GitHub Actions https://foojay.io/today/how-to-release-a-java-module-with-jreleaser-to-maven-central-with-github-actions/ https://foojay.io/today/how-to-release-a-java-module-with-jreleaser-to-maven-central-with-github-actions/#respond Wed, 18 Jan 2023 08:55:48 +0000 https://foojay.io/?p=61688 Learn from scratch about how to get started releasing a Java module with JReleaser to Maven Central with Github Actions.

The post How to Release a Java Module with JReleaser to Maven Central with GitHub Actions appeared first on foojay.

]]>
Table of Contents
PreconditionsJReleaserThe actual local releaseGitHub actionMaven pluginResources used

This guide is outdated since 2024. There is a new version here: https://foojay.io/today/how-to-publish-a-java-maven-project-to-maven-central-using-jreleaser-and-github-actions-2025-guide/

This article is a tutorial that guides you through the process of releasing a Java module with JReleaser to Maven Central with Github Actions.

JReleaser is a tool that streamlines the release process for Java projects, allowing developers to quickly and efficiently publish their modules to Maven Central.

If you just want to publish your Maven project by hand, then you can follow How to Publish a Java Maven Project to the Maven Central Repository by @Tobias Briones.

This article will use SemVer Check project as an example project that used Maven as a build tool.

Preconditions

In order to publish to Maven central, you will need to have a GPG key and have a group-id (coordinate) registered.

GPG key

You will need a GPG key to sign the artifacts, this will allow users to verify that they have the correct package.

  • Download GPG key or install it with your favorite package manager.
  • Generate a public key with (remember the password as we going to need it later)
    gpg --gen-key
  • Now find the id of your key with
    gpg --list-keys --keyid-format=long

    The output should look something like this:

    /Users/jagodevreede/.gnupg/pubring.kbx
    --------------------------------------
    pub   rsa4096/XXXXXXXX9925B017 2022-11-17 [SC] [expires: 2026-11-17]
          C20FC085CF5B0D4D861E8CEDXXXXXXXX9925B017
    uid                 [ultimate] Jago de Vreede <redacted@mail.com>
    sub   rsa4096/XXXXXXXXXXXXFC74 2022-11-17 [E] [expires: 2026-11-17]

    In this case, the id of the public key is XXXXXXXXXXXXFC74

  • Publish your public key to a public server for example ubuntu, for example
    gpg --keyserver keyserver.ubuntu.com --send-keys XXXXXXXXXXXXFC74

Coordinate (group-id)

This guide is outdated since 2024. There is a new version here: https://foojay.io/today/how-to-publish-a-java-maven-project-to-maven-central-using-jreleaser-and-github-actions-2025-guide/

This process is actually very well documented at https://central.sonatype.org/publish/requirements/coordinates/.

Your group id can be your domain name (reverse) if you have that. Also, many Code Hosting services are supported like GitHub, GitLab, Gitee, Bitbucket, and SourceForge. In this example, we will use GitHub, so our group-id will be io.github.jagodevreede.

In order to "claim" this group-id you will need to create a Jira account and create a New project ticket and fill out the details of your project, as an example see OSSRH-86928 for the ticket used in this tutorial. Please note that the Jira account is also used for the login of nexus (the artifact repository used).

When you have created the ticket you need to show ownership of the username, this is done by creating a temporary empty repository with the ticket name. When you have done this then the bot will automatically update the Jira ticket.

Preparing your project

Javadoc and sources

A project that is released to Maven central requires that you attach javadoc and the sources. 

This can be done by adding the 2 plugins to your build, these plugins don't need to run every time so it's recommended to put them in a profile, so they will only run when you need to, or when you build a release.

A Maven example is as follows:

<profiles>
    <profile>
        <id>publication</id>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-javadoc-plugin</artifactId>
                    <!-- 2.9.1 is the current version at the time of writing, please check if there is a newer version -->
                    <version>2.9.1</version>
                    <executions>
                        <execution>
                            <id>attach-javadocs</id>
                            <goals>
                                <goal>jar</goal>
                            </goals>
                            <configuration>
                                <attach>true</attach>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-source-plugin</artifactId>
                    <!-- 3.2.1 is the current version at the time of writing, please check if there is a newer version -->
                    <version>3.2.1</version>
                    <executions>
                        <execution>
                            <id>attach-sources</id>
                            <goals>
                                <goal>jar</goal>
                            </goals>
                            <configuration>
                                <attach>true</attach>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
</profiles>

Meta information in pom

Maven central also requires metadata in your pom like a description, inception year, license, list of developers and scm location. 

Example configurations is:

<description>This is the root pom for the semver-check maven plugin</description>
<inceptionYear>2022</inceptionYear>

<licenses>
    <license>
        <name>Apache License, Version 2.0</name>
        <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
        <distribution>repo</distribution>
    </license>
</licenses>

<developers>
    <developer>
        <id>jagodevreede</id>
        <name>Jago de Vreede</name>
    </developer>
</developers>

<scm>
    <connection>scm:git:https://github.com/jagodevreede/semver-check.git</connection>
    <developerConnection>scm:git:https://github.com/jagodevreede/semver-check.git</developerConnection>
    <url>https://github.com/jagodevreede/semver-check.git</url>
    <tag>HEAD</tag>
</scm>

Deploy plugin version

We need to have at least version 3.0.0 of the Maven deploy version, so add the following to the root pom

<pluginManagement>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-deploy-plugin</artifactId>
            <version>3.0.0</version>
        </plugin>
    </plugins>
</pluginManagement>

JReleaser

Installing locally

Now with the preconditions out of the way, it is time to install JReleaser locally to verify that everything is working before we switch to GitHub actions. I would recommend that you do the first release locally, that way you can easily fix any errors. 

Go to jreleaser.org/guide/latest/install.html and follow the instructions to install the latest stable version of JReleaser.

We need to create a configuration file for JReleaser, all this config will be put into secrets on GitHub later. Create a file in your home folder ~/.jreleaser/config.properties

An example file will look something like this:

JRELEASER_GITHUB_TOKEN=ghp_eWVzIGFsc28gc2VjcmV0==
JRELEASER_GPG_SECRET_KEY=something_base64_with_around_6500+_chars
JRELEASER_GPG_PASSPHRASE=secret
JRELEASER_GPG_PUBLIC_KEY=something_base64_with_around_3000+_chars
JRELEASER_NEXUS2_MAVEN_CENTRAL_USERNAME=jagodevreede
JRELEASER_NEXUS2_MAVEN_CENTRAL_PASSWORD=also-secret

GitHub token

First, we need a GitHub token that has access to the repository.

Go to Settings -> Developer settings -> tokens (classic) in your GitHub profile and create a token that has "repo" access. And place this in the JReleaser config file under the key JRELEASER_GITHUB_TOKEN.

GPG keys

When we generated the keys we also did a listing of the keys, we need to have the id of the public key, in the example above it was XXXXXXXXXXXXFC74.

First, we need to export our private key as a base64 string and put it in the config file under JRELEASER_GPG_SECRET_KEY, we can do that with (note you will need to password that you used when you created the key):

gpg --export-secret-keys XXXXXXXXXXXXFC74 | base64

The JRELEASER_GPG_PASSPHRASE is the password we used when we exported the secret key.

Next is the public key, also a base64 encoded string 

gpg --export XXXXXXXXXXXXFC74 | base64

This we put in the config file under JRELEASER_GPG_PUBLIC_KEY.

Nexus credentials

Lastly, we need to put the credentials that we used to login to Jira in as nexus credentials under JRELEASER_NEXUS2_MAVEN_CENTRAL_USERNAME and JRELEASER_NEXUS2_MAVEN_CENTRAL_PASSWORD.

JReleaser configuration

We need to create a jreleaser.yml file for the project. This can be done with the cli we installed before, with the following command:

jreleaser init --format yml

You will need to edit the file and fill in the yml, this is where your copy-paste skills will shine, as almost all information can be found in the pom.xml.

Next, remove the distributions part and version from the yml. As the distribution will be added later, and the version will be set via an environment variable.

Finally, add the configuration to push to Maven central:

signing:
  active: ALWAYS
  armored: true

deploy:
  maven:
    nexus2:
      maven-central:
        active: ALWAYS
        url: https://s01.oss.sonatype.org/service/local
        closeRepository: true
        releaseRepository: false
        stagingRepositories:
          - target/staging-deploy

The actual local release

Now that all the preconditions and plumbing is out of the way it is time for the actual release

Prepare your Maven project to be released, so remove the -SNAPSHOT from your versions.

You can do that with the Maven versions plugin for example

mvn versions:set -DnewVersion=0.0.1

Staging

The release needs to be uploaded from a staging directory, to create that invoke the following command:

mvn -Ppublication deploy -DaltDeploymentRepository=local::file:./target/staging-deploy

Release

First set the version that you will be releasing (this must be the same as what you got in your pom.xml)

export JRELEASER_PROJECT_VERSION=0.0.1

Then do the actual release with:

jreleaser full-release

Finalize in nexus

You will need to log in to nexus and go to staging repositories  after JReleaser is done. This is your final stop, after this, there is no turning back or removing it.

Example of staged release, ready to be released

Even this step can be automated by setting the releaseRepository property to true in the jrelease.yml. You can do that when you trust the process and have done some successful releases.

GitHub action

Now that we can release by hand it is time to automate this entire process!

Secrets

Before we can run JReleaser on GitHub, we first need to set our secrets in the secrets of the repository.

To keep things simple just copy all the key values from the JReleaser config file that was used locally.

And you will end up with something like this:

Workflow

First, create a release workflow by creating a release.yml file in your repository under .github/workflows/.

The first bit of the file is the name of the workflow and the input parameters used when you start the workflow.

name: Release

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Release version'
        required: true
      nextVersion:
        description: 'Next version after release (-SNAPSHOT will be added automatically)'
        required: true

This will look something like this when you start the release workflow

Next up is just your default build setup, in this example, java 11 is used, but this is the same as for your normal build. Except  fetch-depth as JReleaser will use the git log to create the changelog it will need the full history, and thus we set the fetch-depth to 0. As it defaults to 1.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Set up JDK 11
        uses: actions/setup-java@v3
        with:
          java-version: '11'
          distribution: 'temurin'
          cache: maven

Next, we need to set the version that we will be releasing, we can do that with the Maven versions plugin.

- name: Set release version
  run: mvn --no-transfer-progress --batch-mode versions:set -DnewVersion=${{ github.event.inputs.version }}

This change will be the code that will be released, so we want to commit that change. A tag will be created in the release process by JReleaser

- name: Commit & Push changes
  uses: actions-js/push@master
  with:
    github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }}
    message: Releasing version ${{ github.event.inputs.version }}

Now its time to stage the release, as we did manually

- name: Stage release
  run: mvn --no-transfer-progress --batch-mode -Ppublication clean deploy -DaltDeploymentRepository=local::default::file://`pwd`/target/staging-deploy

Then we can call JReleaser this is where we use the secrets we set up before.

- name: Run JReleaser
  uses: jreleaser/release-action@v2
  with:
    setup-java: false
    version: 1.4.0
  env:
    JRELEASER_PROJECT_VERSION: ${{ github.event.inputs.version }}
    JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }}
    JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }}
    JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }}
    JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }}
    JRELEASER_NEXUS2_MAVEN_CENTRAL_USERNAME: ${{ secrets.JRELEASER_NEXUS2_MAVEN_CENTRAL_USERNAME }}
    JRELEASER_NEXUS2_MAVEN_CENTRAL_PASSWORD: ${{ secrets.JRELEASER_NEXUS2_MAVEN_CENTRAL_PASSWORD }}

When we are done we need to set the next development version and push that.

- name: Set release version
  run: mvn --no-transfer-progress --batch-mode versions:set -DnewVersion=${{ github.event.inputs.nextVersion }}
- name: Commit & Push changes
  uses: actions-js/push@master
  with:
    github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }}
    message: Setting SNAPSHOT version ${{ github.event.inputs.nextVersion }}-SNAPSHOT
    tags: true

The full file can be found here: https://github.com/jagodevreede/semver-check/blob/f3fab073107ce6691c1b0bff25f7df8ecf2165aa/.github/workflows/release.yml

And with that we are done, now we can easily release our module to maven central with a press of a button in GitHub.

Maven plugin

There is also a JReleaser Maven plugin available that offers a Maven DSL to configure JReleaser. With that the jreleaser.yml file can be omitted as information can be read from the pom file instead. 

The use of the Maven DSL offers these benefits:
  • reduce duplication in release configuration
  • no need to install JReleaser’s cli
  • no need to use jreleaser/release-action on Github as invoking the Maven plugin is enough

So why is the cli used in this article you might ask, well the cli can be used for other than Maven projects as well and it demonstrates the capabilities of JReleaser.

Resources used

The post How to Release a Java Module with JReleaser to Maven Central with GitHub Actions appeared first on foojay.

]]>
https://foojay.io/today/how-to-release-a-java-module-with-jreleaser-to-maven-central-with-github-actions/feed/ 0