10 min. reading time

Usually automating things on GitHub means using GitHub Actions. This is GitHub's analogon to GitLab's CI-scripts and Jenkins' jobs. But how to combine these with Maven to release artifacts into Maven Central?

The other day I got the task of migrating the release jobs of all my itemis OSS artifacts to GitHub. Since I could not really find an up to date answer via the usual web resources, I thought it was a good idea to write down my current solution for others to have a starting point.

Deploying to Maven Central means publishing your artifacts onto Sonatype's central repository (OSSRH - Open Source Software Repository Hosting) which is usually available to all Maven builds, thus making your artifacts available to all those builds as well.

Prerequisites

To release artifacts into OSSRH you'll need:

It may be possible to checkout your sources from a repository not hosted by GitHub but for the scope of this blog post, we are going to assume your project resided on GitHub.

How it works

In general the process works like this:

  1. Call Maven's deploy goal.
  2. Maven will build the artifacts as specified in the pom.xml.
  3. Maven will generate GPG signatures for the built artifacts.
    1. Maven Central will only accept signed artifacts.
    2. The GPG key used is the default key of the GPG keyring.
    3. Since we are going to import exactly one key into an empty key ring, this will automatically be the default key.
  4. The artifacts are published to the nexusUrl configured with the nexus-staging-maven-plugin.
    1. The server credentials are taken from settings.xml, where a server with an id matching the serverId configuration of the nexus-staging-maven-plugin must have been configured.

How it's done

Maven

I prefer to use profiles for the deployment work so you may just want to copy the following one into pom.xml:

    
<profile>
    <id>release</id>
    <properties>
        <version.maven-release-plugin>3.0.0-M7</version.maven-release-plugin>
        <version.maven-gpg-plugin>3.0.1</version.maven-gpg-plugin>
        <version.nexus-staging-maven-plugin>1.6.13</version.nexus-staging-maven-plugin>
    </properties>
    <build>
        <pluginManagement>
            <plugins>
                <plugin>
                    <artifactId>maven-release-plugin</artifactId>
                    <version>${version.maven-release-plugin}</version>
                    <configuration>
                        <tagNameFormat>@{project.version}</tagNameFormat>
                    </configuration>
                </plugin>
                <!-- The key's name & passphrase are configured via GitHub's setup-java action. -->
                <plugin>
                    <artifactId>maven-gpg-plugin</artifactId>
                    <version>${version.maven-gpg-plugin}</version>
                    <executions>
                        <execution>
                            <id>sign-artifacts</id>
                            <phase>verify</phase>
                            <goals>
                                <goal>sign</goal>
                            </goals>
                            <configuration>
<!-- This is required to make sure the plugin does not stop asking for -->
<!-- user input on the passphrase --> <gpgArguments> <arg>--pinentry-mode</arg> <arg>loopback</arg> </gpgArguments> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.sonatype.plugins</groupId> <artifactId>nexus-staging-maven-plugin</artifactId> <version>${version.nexus-staging-maven-plugin}</version> <extensions>true</extensions> <configuration> <serverId>ossrh</serverId>
<!-- For pre 2021 legacy projects you may use https://oss.sonatype.org
<!-- See https://central.sonatype.org/publish/release/#login-into-ossrh for details -->
                      <nexusUrl>https://s01.oss.sonatype.org</nexusUrl> <autoReleaseAfterClose>true</autoReleaseAfterClose> </configuration> </plugin> </plugins> </pluginManagement> <plugins> <plugin> <artifactId>maven-gpg-plugin</artifactId> </plugin> <plugin> <groupId>org.sonatype.plugins</groupId> <artifactId>nexus-staging-maven-plugin</artifactId> </plugin> </plugins> </build> </profile>

For this to work, you'll need:

  • A server entry in your settings.xml file with id ossrh
  • Another server entry where the id is gpg.passphrase and the password is the key's passphrase.

The last requirement is due to the kind of weird way of how the maven-gpg-plugin works. Luckily this will all be taken care of by our GitHub Actions setup.

GitHub

Create the file release-workflow.yml in <REPO_ROOT>/.github/workflows with the following contents:

name: release-workflow # You may choose a different name
run-name: Release run ${{ github.run_number }} # Enumerates entries in the "workflow runs" view
on: 
  workflow_dispatch: # Only run when manually started
jobs:
release: # Arbitrarily chosen
  name: Release # Arbitrarily chosen
    runs-on: ubuntu-22.04 # May also run on other kinds of distros
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Setup Java
        uses: actions/setup-java@v3 # Does also set up Maven and GPG
        with:
          distribution: 'temurin' # As good as any other, see: https://github.com/actions/setup-java#supported-distributions
          java-package: 'jdk'
          java-version: '11'
          check-latest: true
        server-id: 'ossrh' # must match the serverId configured for the nexus-staging-maven-plugin
          server-username: OSSRH_USERNAME # Env var that holds your OSSRH user name
          server-password: OSSRH_PASSWORD # Env var that holds your OSSRH user pw
        gpg-private-key: ${{ secrets.YOUR_GPG_PRIVATE_KEY }} # Substituted with the value stored in the referenced secret
          gpg-passphrase: SIGN_KEY_PASS # Env var that holds the key's passphrase
          cache: 'maven'
      - name: Build & Deploy
        run: |
        # -U force updates just to make sure we are using latest dependencies
          # -B Batch mode (do not ask for user input), just in case
# -P activate profile
        mvn -U -B clean deploy -P release
        env:
          SIGN_KEY_PASS: ${{ secrets.YOUR_GPG_PRIVATE_KEY_PASSPHRASE }}
        OSSRH_USERNAME: ${{ secrets.YOUR_SONATYPE_USER }}
        OSSRH_PASSWORD: ${{ secrets.YOUR_SONATYPE_PW }}

Explanation

The setup-java-action will take care of importing the GPG key from a keyserver into the job's GPG keyring and generates a working settings.xml. It will look like this:

<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 https://maven.apache.org/xsd/settings-1.0.0.xsd">
  ...
  <servers>
    <server>
      <id>ossrh</id>
      <username>${env.OSSRH_USERNAME}</username>
      <password>${env.OSSRH_PASSWORD}</password>
    </server>
    <server>
      <id>gpg.passphrase</id>
      <password>${env.SIGN_KEY_PASS}</password>
    </server>
  </servers>
  ...
</settings>

This means, all is automatically set up for our Maven configuration to work right out of the box.

As you can see, the settings.xml will only hold "references" to environment variables. These variables need to be defined in the deploy step of your workflow.

In order to not expose sensitive information in your clear text workflow files, these environment variables are best tied to the secrets that you have set up for your GitHub project. Secrets will not be exposed in any logs. Just think of them as project global environment variables that are not exposed outside a job.

The maven-gpg-plugin will look up the passphrase by looking up the "password" of the "server" with id gpg.passphrase in the settings.xml. This is the default and does not require any configuration of the maven-gpg-plugin.

You rock!

That's it for today. You may go on and experiment a bit. There are of course other solutions possible. For example not using any passphrase or a maven-gpg-plugin alternative like the sign-maven-plugin by simplify4u. Please don't forget to share any discoveries you make with the community.

Comments