Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save sualeh/ae78dc16123899d7942bc38baba5203c to your computer and use it in GitHub Desktop.
Save sualeh/ae78dc16123899d7942bc38baba5203c to your computer and use it in GitHub Desktop.
How to Sign and Release to The Central Repository with GitHub Actions

How to Sign and Release to The Central Repository with GitHub Actions

GitHub allows automated builds using GitHub Actions. A commonly asked question is how to release artifacts (packaged Java jars) built by Maven and Gradle to The Central Repository. The GitHub Actions documentation provides only part of the answer.

So, first, configure your Maven project for staging artifacts to The Central Repository, by reading through Configuring Your Project for Deployment and following those steps. Please make sure that the maven-gpg-plugin is configured to prevent gpg from using PIN entry programs, as follows:

<configuration>
  <gpgArguments>
      <arg>--pinentry-mode</arg>
      <arg>loopback</arg>
  </gpgArguments>
</configuration>

At this point, you should be able to manually stage your artifacts to The Central Repository.

Next, set up a basic GitHub Actions workflow to build your project. Take a look at Publishing Java packages with Maven, and complete all the steps there.

At this point, you will find that you are missing one step - being able to sign your Maven-built jar files within your GitHub Actions workflow. You can follow the steps below to sign artifacts in GitHub actions. The trick involves loading in your private key into GitHub Actions using the gpg command-line commands.

  1. Export your gpg private key from the system on which you have created it.
    1. Find your key-id (using gpg --list-secret-keys --keyid-format=long)
    2. Export the gpg secret key to an ASCII file using gpg --export-secret-keys -a <key-id> > secret.txt
    3. Edit secret.txt using a plain text editor, and replace all newlines with a literal "\n" (backslash + n) until everything is on a single line
  2. Set up GitHub Actions secrets
    1. Create a secret called OSSRH_GPG_SECRET_KEY using the text from your edited secret.txt file (the whole text should be in a single line) as the value
    2. Create a secret called OSSRH_GPG_SECRET_KEY_PASSWORD containing the password for your gpg secret key
  3. Create a GitHub Actions step to install the gpg secret key
    1. Add an action similar to:
      - id: install-secret-key
        name: Install gpg secret key
        run: |
          # Install gpg secret key
          cat <(echo -e "${{ secrets.OSSRH_GPG_SECRET_KEY }}") | gpg --batch --import
          # Verify gpg secret key
          gpg --list-secret-keys --keyid-format LONG
    2. Verify that the secret key is shown in the GitHub Actions logs
    3. You can remove the output from list secret keys if you are confident that this action will work, but it is better to leave it in there
  4. Bring it all together, and create a GitHub Actions step to publish
    1. Add an action similar to:
      - id: publish-to-central
        name: Publish to Central Repository
        env:
          MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
          MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
        run: |
          mvn \
            --no-transfer-progress \
            --batch-mode \
            -Dgpg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} \
            clean deploy
    2. After a couple of hours, verify that the artifact got published to The Central Repository
@keith-miller
Copy link

Thank you for this!

@tryptichon
Copy link

Stuff like this saves other people a lot of fiddling around for hours. So a heartfelt "Thanks" from me as well. :D

@poikilotherm
Copy link

Instead of manual text editing, you may either pipe the output of the command or the file content through ... | tr '\n' ',' | sed -e 's#,#\\n#g' to achieve the same.

@micheljung
Copy link

micheljung commented Oct 19, 2021

Thanks! Fyi replacing newlines is not required.

Any idea why I get:

Failed to execute goal org.apache.maven.plugins:maven-gpg-plugin:3.0.1:sign (sign-artifacts) on project ***: Unable to decrypt gpg passphrase: org.sonatype.plexus.components.sec.dispatcher.SecDispatcherException: java.io.FileNotFoundException: /home/runner/.m2/settings-security.xml (No such file or directory)

I read actions/setup-java#91 but --pinentry-mode was already configured, and with 3.0.1 it's obsolete anyway.

Update:

My environment variables were misconfigured. A working example can be found here: https://github.com/microsoft/playwright-java/blob/29c0df6443666d9534121428255a5e9a4d7143a4/.github/workflows/publish.yml

@ThePrez
Copy link

ThePrez commented Nov 23, 2021

Echoing others. Thank you for this!!! I struggled to get this working yesterday, but it seems to be working on the first try after following your guide.

I did get stuck at the "find your key-id" step as I am new to GPG and hadn't done any key creation. This link helped fill that gap: https://docs.github.com/en/authentication/managing-commit-signature-verification/generating-a-new-gpg-key

@sualeh
Copy link
Author

sualeh commented Nov 23, 2021

Thanks, @ThePrez - I updated the text above, and a little more detail on the "find your key-id" step as well.

@ErShakirAnsari
Copy link

ErShakirAnsari commented Sep 6, 2022

I'm facing below error:

Failed to execute goal org.sonatype.plugins:nexus-staging-maven-plugin:1.6.7:deploy (injected-nexus-deploy) on project project-parent: Execution injected-nexus-deploy of goal org.sonatype.plugins:nexus-staging-maven-plugin:1.6.7:deploy failed: Server credentials with ID "ossrh-user-in" not found! -> [Help 1]

@kilmajster
Copy link

Hi, maybe GH changed the way of dealing with envs/secrets but in my case \ns on the beginning of each line were causing an issue, without them all works great

@sualeh
Copy link
Author

sualeh commented Oct 1, 2022

Hi, maybe GH changed the way of dealing with envs/secrets but in my case \ns on the beginning of each line were causing an issue, without them all works great

Thanks for sharing.

@pawellabaj
Copy link

Thanks @sualeh
It saved me a night :-)

@lbruun
Copy link

lbruun commented Jun 8, 2024

This is appreciated.

HOWEVER: At time of writing this functionality is now build into the official setup-java action. Since you are more than likely to always need setup-java in your GitHub workflow there is no need anymore for the recipe in this gist.

Detailed instructions can be found HERE. One advantage to using GitHub's own action is that cleans up after itself, so that the private key is not leaked between jobs.

@keith-miller
Copy link

That's great news!

@soubhik-c
Copy link

Thank you for sharing this. Very useful and succinct.

@asarkar
Copy link

asarkar commented May 10, 2025

@lbruun setup-java is a third-party action, what if they phone home with my private key? I'm not saying they do, but humans are always the weakest link in the security chain.

@lbruun
Copy link

lbruun commented May 11, 2025

@asarkar No, setup-java is an action from GitHub Inc itself. This can hardly be called a third-party action. All actions in the namespace http://github.com/actions are GitHub's own.

@asarkar
Copy link

asarkar commented May 11, 2025

@lbruun But in your earlier comment you said:

One advantage to using GitHub's own action

That seems to imply setup-java isn’t owned by GitHub.

@lbruun
Copy link

lbruun commented May 11, 2025

setup-java is GitHub's own action. The sentence could also be phrased:

One advantage to using the setup-java action

@lbruun
Copy link

lbruun commented May 11, 2025

In any case I have later become aware that the whole concept of using an external tool, GnuPG, has become redundant. It was never a good idea to begin with and has always caused grief. In particular because of the many hoops that GnuPG wants you to jump through.

So what is the new thing? Well, the Maven GPG Plugin now supports use of a BouncyCastle as an alternative to using GnuPG. Thus, you will no longer have to rely on an external tool. Also, the concept of putting your private key into a special encrypted store on your disk (something that GnuPG required, in your recipe known as "install the gpg secret key") is no longer needed. You can simply use an environment variable. All in all: much, much simpler. And frankly even safer because putting some secret onto disk in a CI pipeline is a bad idea, even if it is encrypted-at-rest as is the case the for GnuPG keyring.

I recommend the new BouncyCastle alternative and haven't been able to fault it in any way.

@asarkar
Copy link

asarkar commented May 11, 2025

"install the gpg secret key" is no longer needed

Funny you should mention that, since only today I wrote an answer on Stackoverflow saying the same thing. I quote part of it below.


A straightforward approach is to store your GPG key and passphrase as GitHub secrets. This method has several advantages:

  1. No Base64 Encoding: Directly use the key without extra encoding steps.

  2. Secure Logging: GitHub will automatically mask these secrets in your logs, reducing the risk of accidental exposure.

Here's a sample configuration for a Gradle project using the vanniktech/gradle-maven-publish-plugin:

- name: Publish to Maven Central
  env:
    ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }}
    ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_KEY_PASSPHRASE }}
    ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_USERNAME }}
    ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_PASSWORD }}
  run: |
    ./gradlew publishAndReleaseToMavenCentral --no-configuration-cache

This setup ensures the private key is securely used in memory without the need for intermediate files.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment