Deploying Java Artifacts to Multiple Maven Repositories with GitHub Actions
Tags: java, github, tutorial, programming
Published: 2022-11-02 16:46:31

I use GitHub Actions to manage the CI/CD workflows for all of my projects. In this post, I explain my deployment workflow for Java projects. It is launched upon a GitHub release. It deploys the artifacts to both the Maven Central repository as well as to GitHub Packages. It triggers an ahead-of-time build on JitPack. And it uses the GitHub CLI to also upload the artifacts to the GitHub release.

This post assumes knowledge of configuring your Maven `pom.xml` as necessary for deployment to multiple Maven repositories, using Maven profiles for each of the repositories, as covered in my prior post:

{% link https://dev.to/cicirello/how-to-use-maven-profiles-to-selectively-activate-plugins-and-other-configuration-from-the-command-line-2i2b %}

This post also uses the trick explained in another prior post for triggering an ahead-of-time build on JitPack when using a reverse domain Maven `groupId`, rather than JitPack's default of on-demand builds:

{% link https://dev.to/cicirello/ahead-of-time-jitpack-builds-with-custom-groupid-via-github-actions-55cp %}

My workflow that I explain in this post relies upon GitHub Releases. Creating a GitHub Release is the event that starts the workflow, and the workflow attaches the jar files to the release as one of its steps. For more information on GitHub Releases, and other things you can do with them, see @mishmanners great post from earlier today: 

{% link https://dev.to/github/why-release-a-project-why-not-just-commit-to-main-3ga2 %}

__Table of Contents:__
* [Workflow Step by Step](#workflow-step-by-step) walks you through my deployment workflow one step at a time.
* [Complete Workflow](#complete-workflow).
* [Live Example](#live-example) from one of my projects on GitHub.
* [Where You Can Find Me](#where-you-can-find-me).


## Workflow Step by Step

I'm going to walk through the full workflow from beginning to end.

### On release

GitHub workflows begin with the events that trigger it to run. In this case, the workflow runs when a release is created.

```YAML
name: Maven Package

on:
  release:
    types: [created]
```

### Set up the job

My deployment workflow has a single job and runs on Ubuntu. I define an environment variable `artifact_name` with the Maven `artifactId` that is used in a few of the steps of the workflow. This isn't strictly necessary, but I use the same workflow in several repositories, so it makes it simple to set things up for another repository. I haven't gotten around to consolidating into a reusable workflow since those were recently introduced by GitHub.

```YAML
jobs:
  publish:

    runs-on: ubuntu-latest

    env:
      artifact_name: chips-n-salsa
```

### Checkout the repository

This step is self-explanatory, and is found at the start of most GitHub workflows.

```YAML
    steps:
    - uses: actions/checkout@v3
```

### Process the release tag

I use the common release tag convention that begins with a `v` followed by the Semantic Version. For example, `v1.2.3`. However, Maven versions don't include a `v`. So this step gets the release tag, removes the `v` from it, and sets an output from the step named `VERSION` that can be used by later steps of the workflow.

```YAML
    - name: Get the release version
      id: get_version
      run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT
```

For more information on setting outputs from workflow steps, see my prior DEV post from last week:

{% link https://dev.to/cicirello/how-to-patch-the-deprecated-set-output-in-github-workflows-and-in-container-actions-9co %}


### Inject the release version into the `pom.xml`

We need the `<version></version>` inside the `pom.xml` to correspond to the version we are deploying. Remembering to edit the `pom.xml` manually is error prone, and easily forgotten. Instead, I inject the release version during the deployment workflow by using `mvn versions:set`. This command actually changes the `pom.xml`. If I wanted the `pom.xml` within the repository to be up to date with the latest release, I could add a step to commit the change. However, I leave it as a snapshot version in the repository (e.g., `<version>6-SNAPSHOT</version>`) to avoid confusing anyone who may build from the source into believing they've built a release version. 

The step to inject the release version into the `pom.xml` uses the output from the previous step above and the `mvn versions:set` command as follows:

```YAML
    - name: Update package version
      run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }}
```

### Set up Java and deploy to Maven Central

This pair of steps first uses the `setup-java` GitHub Action to set up what we need to deploy to the Maven Central repository. It is mostly as described in the GitHub Actions documentation. 

The second of this pair of steps runs `mvn deploy -PossrhDeploy`. The command line option `-PossrhDeploy` activates a Maven profile with an id of `ossrhDeploy` that I have defined in the `pom.xml` where I've configured everything needed for deployment to Maven Central. See my prior post on [using Maven profiles](https://dev.to/cicirello/how-to-use-maven-profiles-to-selectively-activate-plugins-and-other-configuration-from-the-command-line-2i2b) for a tutorial on how Maven profiles can be used to selectively activate configuration, along with a specific example of configuring Maven to deploy to multiple repositories.

```YAML
    - name: Set up JDK 17 for deploy to OSSRH
      uses: actions/setup-java@v3
      with:
        distribution: 'adopt'
        java-version: '17'
        server-id: ossrh 
        server-username: MAVEN_USERNAME
        server-password: MAVEN_CENTRAL_TOKEN
        gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
        gpg-passphrase: MAVEN_GPG_PASSPHRASE

    - name: Publish to Apache Maven Central
      run: mvn deploy -PossrhDeploy
      env:
        MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
        MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }}
        MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
```

### Set up Java and deploy to GitHub Packages

This pair of steps is a counterpart to the above pair of steps, but this time for deployment to GitHub Packages. The first step below uses `setup-java`, but this time for deployment to GitHub Packages. It is simpler than what is needed to deploy to Maven Central. The second step below does the actual deployment using: `mvn deploy -PgithubDeploy`. Just like above, I've configured the `<distributionManagement><repository>` in a Maven profile with an id of `githubDeploy`, which is activated by the command line option `-PgithubDeploy`. 

```YAML
    - name: Set up JDK 17 for deploy to github packages
      uses: actions/setup-java@v3
      with:
        distribution: 'adopt'
        java-version: '17'
        server-id: github 

    - name: Publish to GitHub Packages Apache Maven
      run: mvn deploy -PgithubDeploy
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 
```

### Use the GitHub CLI to upload the jar files as release assets

I also upload the various jar files produced during the build as release assets. I use the GitHub CLI to do this, which is always available to use during GitHub workflow runs. Specifically, I use the `gh release upload` command, which requires the tag of the release, as well as the file you are uploading. The project this is based on produces 4 jar files, including the usual 3 (jar of the library, jar of the sources, and jar of the javadocs), and the fourth jar is the library including all dependencies. Since I'm using Maven, the artifacts are created in a `target` directory during the build, and I use the environment variable I set at the very beginning with the Maven `artifactId` and the output from an earlier step with the Maven `<version>` (release tag without the `v`) to form the filenames. 

```YAML
    - name: Upload jar files to release as release assets
      run: |
        TAG=${GITHUB_REF/refs\/tags\//}
        gh release upload ${TAG} target/${{ env.artifact_name }}-${{ steps.get_version.outputs.VERSION }}.jar
        gh release upload ${TAG} target/${{ env.artifact_name }}-${{ steps.get_version.outputs.VERSION }}-sources.jar
        gh release upload ${TAG} target/${{ env.artifact_name }}-${{ steps.get_version.outputs.VERSION }}-javadoc.jar
        gh release upload ${TAG} target/${{ env.artifact_name }}-${{ steps.get_version.outputs.VERSION }}-jar-with-dependencies.jar 
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

### Ahead-of-time JitPack release build 

In a post some time back, I explained [a trick to get JitPack to build ahead-of-time using your reverse domain groupId](https://dev.to/cicirello/ahead-of-time-jitpack-builds-with-custom-groupid-via-github-actions-55cp). JitPack normally build on-demand the first time an artifact is requested. It does so from the GitHub repository of the artifact. Thus, the first time someone imports a version of your library from JitPack, their build will be delayed while waiting for a build of your library. We can eliminate that delay with a simple curl as seen in the step below. The timeout is necessary because you don't actually need to download anything now. You just want JitPack to think you want to download the artifacts. By using a timeout, you avoid wasting cycles on the runner of your workflow.

__IMPORTANT:__ If you use this trick in your own workflow, make sure you change the `org/cicirello` to whatever reverse domain you have configured with JitPack as your `groupId`.

```YAML
    - name: Request release from JitPack to trigger build
      run: |
        JITPACK_URL="https://jitpack.io/org/cicirello/${{ env.artifact_name }}/${{ steps.get_version.outputs.VERSION }}/maven-metadata.xml"
        # timeout in 30 seconds to avoid waiting for build
        curl -s -m 30 ${JITPACK_URL} || true
```


## Complete Workflow

Here's my full Java deployment workflow. 

```YAML
name: Maven Package

on:
  release:
    types: [created]

jobs:
  publish:

    runs-on: ubuntu-latest

    env:
      artifact_name: chips-n-salsa

    steps:
    - uses: actions/checkout@v3

    - name: Get the release version
      id: get_version
      run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_OUTPUT

    - name: Update package version
      run: mvn versions:set -DnewVersion=${{ steps.get_version.outputs.VERSION }}

    - name: Set up JDK 17 for deploy to OSSRH
      uses: actions/setup-java@v3
      with:
        distribution: 'adopt'
        java-version: '17'
        server-id: ossrh 
        server-username: MAVEN_USERNAME
        server-password: MAVEN_CENTRAL_TOKEN
        gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
        gpg-passphrase: MAVEN_GPG_PASSPHRASE

    - name: Publish to Apache Maven Central
      run: mvn deploy -PossrhDeploy
      env:
        MAVEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
        MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }}
        MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}

    - name: Set up JDK 17 for deploy to github packages
      uses: actions/setup-java@v3
      with:
        distribution: 'adopt'
        java-version: '17'
        server-id: github 

    - name: Publish to GitHub Packages Apache Maven
      run: mvn deploy -PgithubDeploy
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 

    - name: Upload jar files to release as release assets
      run: |
        TAG=${GITHUB_REF/refs\/tags\//}
        gh release upload ${TAG} target/${{ env.artifact_name }}-${{ steps.get_version.outputs.VERSION }}.jar
        gh release upload ${TAG} target/${{ env.artifact_name }}-${{ steps.get_version.outputs.VERSION }}-sources.jar
        gh release upload ${TAG} target/${{ env.artifact_name }}-${{ steps.get_version.outputs.VERSION }}-javadoc.jar
        gh release upload ${TAG} target/${{ env.artifact_name }}-${{ steps.get_version.outputs.VERSION }}-jar-with-dependencies.jar 
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

    - name: Request release from JitPack to trigger build
      run: |
        JITPACK_URL="https://jitpack.io/org/cicirello/${{ env.artifact_name }}/${{ steps.get_version.outputs.VERSION }}/maven-metadata.xml"
        # timeout in 30 seconds to avoid waiting for build
        curl -s -m 30 ${JITPACK_URL} || true
```


## Live Example

To see a live example, consult the [maven-publish.yml](https://github.com/cicirello/Chips-n-Salsa/blob/master/.github/workflows/maven-publish.yml) workflow of one of my projects. Here is the GitHub repository:

{% github cicirello/Chips-n-Salsa %}


## Where You Can Find Me

Follow me here on DEV:

{% user cicirello %}

Follow me on GitHub:

{% github cicirello/cicirello %}

Or visit my website:

{% embed https://www.cicirello.org/ %}