Publishing to Maven Repositories with GitLab CI, with Signed Artefacts

Featured image for sharing metadata for article

Preamble

I maintain a number of Open Source projects, of which a handful are Java projects which are released to Maven Central for consumption by other users.

I utilise the Sonatype OSSRH (OSS Repository Hosting) as the means to deploy to Maven Central more easily, which enforces checks before the artefact is published, such as all arefacts have a corresponding GPG signature.

It's a little awkward to perform a release locally, as even though I'm using the Gradle Nexus Publish Plugin, I still have to remind myself of the process, and make sure that my local machine is set up with all the right configuration.

I use GitLab and GitLab CI where possible, and couldn't find a lot of folks documenting how they'd managed to get a GitLab CI configuration working, so the artefacts could be signed and uploaded correctly, so in the spirit of blogumentation, I thought I'd work out how to do it and document it.

Prerequisites

Although I have a signing key that I'm currently using to release my libraries, I decided that continuing to use this key for CI purposes wasn't a good idea.

I followed this article to set up a GPG sub-key to use this new sub-key for the purpose of automation.

Current CI setup

Let's say that we've got the following .gitlab-ci.yml:

image: openjdk:11

stages:
  - test

variables:
  # NOTE this is required to allow for caching, but isn't required for this
  # process to work. If you don't set this variable, you'll need to replace
  # references to `$GRADLE_USER_HOME` with `~/.gradle`
  GRADLE_USER_HOME: '.gradle'
  GIT_DEPTH: "0"  # Tells git to fetch all the branches of the project, required by the analysis task

cache:
  paths:
    - .gradle/wrapper
    - .gradle/caches

test:
  stage: test
  script:
    - ./gradlew clean build
    - ./gradlew sonarqube -Dsonar.qualitygate.wait=true
  only:
    - branches
    - merge_requests

Setting up branch + tag protection

Because we're going to restrict the usage of the CI/CD secret variables to protected branches and tags, we need to make sure that any branches we're expecting to perform releases from are correctly set up in GitLab's UI, as well as setting up tag protection (which is a separate step).

If you don't have this set up, you may receive errors like:

Example GPG error when not running on a protected branch
$ gpg --pinentry-mode loopback --passphrase $GPG_PASSPHRASE --import $GPG_USER_KEY
gpg: directory '/root/.gnupg' created
gpg: keybox '/root/.gnupg/pubring.kbx' created
gpg: no valid OpenPGP data found.
gpg: Total number processed: 0

Gradle Configuration

To use the GPG signing with the Gradle Nexus Publish Plugin, we need to set the following configuration in a gradle.properties, as per the Gradle docs:

signing.gnupg.keyName=6B82211F5E150224CB970404DF7507BC5D21FAD0
signing.gnupg.passphrase=this-would-be-the-actual-passphrase

I also found that on the Docker image I was running, the GPG command was gpg, not gpg2 which is the default, so I needed to add the following:

signing.gnupg.executable=gpg

Finished Product

The solution I've come to is as follows, and allows limiting the secrets to protected branches and tags, and handles fully automated signing and publishing of artefacts.

You can see the final version of the pipeline below:

image: openjdk:11

stages:
  - test
  - deploy

variables:
  # NOTE this is required to allow for caching, but isn't required for this
  # process to work. If you don't set this variable, you'll need to replace
  # references to `$GRADLE_USER_HOME` with `~/.gradle`
  GRADLE_USER_HOME: '.gradle'
  GIT_DEPTH: "0"  # Tells git to fetch all the branches of the project, required by the analysis task

cache:
  paths:
    - .gradle/wrapper
    - .gradle/caches

test:
  stage: test
  script:
    - ./gradlew clean build
    - ./gradlew sonarqube -Dsonar.qualitygate.wait=true
  only:
    - branches
    - merge_requests

deploy:
  stage: deploy
  before_script:
    - gpg --pinentry-mode loopback --passphrase $GPG_PASSPHRASE --import $GPG_USER_KEY
    - mkdir -p $GRADLE_USER_HOME
    - cat "$GRADLE_PROPERTIES" > $GRADLE_USER_HOME/gradle.properties
    - echo signing.gnupg.passphrase=$GPG_PASSPHRASE >> $GRADLE_USER_HOME/gradle.properties
  script:
    - ./gradlew publish closeAndReleaseSonatypeStagingRepository
  only:
    # TODO: you may want to make this only trigger on `main` if you're using a
    # SNAPSHOT, especially if you're using tags for release uploads, or only
    # push releases from `main` to avoid cases where we have a tag and `main`
    # pushing a release at the same time, and clashing
    - main
    - tags

Which requires the following variables set in the GitLab UI:

TypeKeyValueProtected
VariableGPG_PASSPHRASE(passphrase for the key)
FileGPG_USER_KEYi.e. gpg2 --armor --export-secret-keys 6B82211F5E150224CB970404DF7507BC5D21FAD0
FileGRADLE_PROPERTIESthe gradle.properties as mentioned above, with newline at the end of file / appended when cating it
VariableORG_GRADLE_PROJECT_sonatypeUsernameThe password retrieved using your User Token
VariableORG_GRADLE_PROJECT_sonatypePasswordThe password retrieved using your User Token

This now allows you to handily publish our binaries to Maven Central - for example.

Written by Jamie Tanna's profile image Jamie Tanna on , and last updated on .

Content for this article is shared under the terms of the Creative Commons Attribution Non Commercial Share Alike 4.0 International, and code is shared under the Apache License 2.0.

#blogumentation #gradle #java #gitlab-ci #gpg.

This post was filed under articles.

Interactions with this post

Interactions with this post

Below you can find the interactions that this page has had using WebMention.

Have you written a response to this post? Let me know the URL:

Do you not have a website set up with WebMention capabilities? You can use Comment Parade.