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:
- Your project should be hosted on GitHub.
- An OSSRH account. See: https://central.sonatype.org/publish/publish-guide.
- A working GPG-keypair of which the public key has been published on either keys.openpgp.org or keyserver.ubuntu.com. See: https://blogs.itemis.com/en/openpgp-on-the-job-part-1-what-is-it.
- Action-secrets for the OSSRH user name, password, GPG private key and its passphrase.
How it works
In general the process works like this:
- Call Maven's deploy goal.
- Maven will build the artifacts as specified in the pom.xml.
- Maven will generate GPG signatures for the built artifacts.
- Maven Central will only accept signed artifacts.
- The GPG key used is the default key of the GPG keyring.
- Since we are going to import exactly one key into an empty key ring, this will automatically be the default key.
- The artifacts are published to the nexusUrl configured with the nexus-staging-maven-plugin.
- 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