Ken Muse

GitHub, Maven, and Packages


Java makes it surprisingly easy to manage and package complex projects using Apache Maven. In fact, many modern package management systems borrowed from approaches pioneered by this project. With so many projects relying on Maven, you would think that a CI/CD system like GitHub Actions would have to include robust support for integrating Maven package repositories. You’d be right. 😄

Java Repositories

If you’re not familiar with creating or consuming packages on Java, no worries! The overall process is surprisingly easy. From a coding perspective, Maven relies on a build lifecycle approach. That is, you integrate your project into the lifecycle and the build command line tells it where in the lifecycle process to stop. This can be anything from just validating the project is properly configured (validate) to executing a complete lifecycle that builds, tests, and publishes a package (deploy). The project configuration details are all stored in the file pom.xml.

Within all of that are two basic configurations. The first is repositories. This is the main one most teams are concerned with configuring. This is used to setup the servers that store the packages used by the project. By default, this includes Maven Central ( https://repo.maven.apache.org/maven2/ ). The second is distributionManagement. These are the servers where Maven publishes packages.

There’s a lot more to Maven and its repository system, but for our purposes today, we’ll just look at these two.

Repositories

The repository configuration section in the POM will usually look something like this:

 1<repositories>
 2    <repository>
 3        <id>github</id>
 4        <url>https://maven.pkg.github.com/kenmuse/my-project</url>
 5        <releases>
 6            <enabled>true</enabled>
 7        </releases>
 8        <snapshots>
 9            <enabled>true</enabled>
10        </snapshots>
11    </repository>
12</repositories>

This has three primary responsibilities:

  • Configure a unique ID for referencing the repository (in this case, github)
  • Configure the URL for the repository
  • Configure which types of packages can be retrieved from the repository

Similarly, if you’re publishing packages, you can configure a distributionManagement repository:

 1<distributionManagement>
 2    <repository>
 3        <id>github</id>
 4        <url>https://maven.pkg.github.com/kenmuse/my-project</url>
 5        <releases>
 6            <enabled>true</enabled>
 7        </releases>
 8        <snapshots>
 9            <enabled>true</enabled>
10        </snapshots>
11    </repository>
12</distributionManagement>

These two sections in the pom.xml define the specific repository (or repositories) that will be used to support the project for retrieving or publishing packages. They are not required to exist in the pom.xml. If the settings are private to individual users, then a second file is used, settings.xml. This file is generally stored on the user’s machine and contains their specific settings and credentials. For this reason, it’s normally not included as part of the source code.

Secrets

The settings file can contain the repositories, but more commonly it contains the user-specific credentials for accessing the repositories. These details are stored under servers. When Maven authenticates with a repository, it looks for a matching id to provide the credentials. For example:

1<servers>
2  <server>
3    <id>github</id>
4    <username>kenmuse</username>
5    <password>my-super-secret-password!</password>
6  </server>
7</servers>

Of course, keeping these files on the file system can be a security risk. Ideally, we’d prefer to keep the secrets from being stored in a file. Maven supports storing values outside of the file and passing those from the command line (through a -Define parameter) or, more helpfully, by reading an environment variable. To use environment variables, you just use the syntax ${env.VARIABLE_NAME}. For example:

1<servers>
2  <server>
3    <id>github</id>
4    <username>${env.USERNAME}</username>
5    <password>${end.PASSWORD}</password>
6  </server>
7</servers>

Now there are no secrets stored as part of the file system!

Putting it all together

So now that you know the basics of how it works, let’s dive into how to implement this in Actions. There are two approaches I frequently see:

  1. Include the settings.xml in source code. I don’t recommend this approach. It can be confusing and frequently leads to someone checking passwords into the codebase.
  2. Generate a settings.xml dynamically which contains the configuration. Use environment variables as placeholders for the user name and password. During the actual build or deploy steps, provide those environment variables to complete the configuration. This is a better approach … especially if we let Actions do it for us!

GitHub provides a special action – actions/setup-java – that is used to configure Java on a runner. It ensures the right version of the SDK is installed in the tool cache, adds it to the path, and optionally configures caching to improve the performance when loading packages. It also has another great feature built into it. It can write the settings file. There are four properties that control the processing of creating a server entry:

  • server-id: The ID of the server. This should match the repository/distributionManagement’s id.
  • server-username: The name of the environment variable that will be used for the username field (defaults to GITHUB_ACTOR)
  • server-password: The name of the environment variable that will be used for the password field (defaults to GITHUB_TOKEN).
  • settings-path: Where the generated settings.xml file will be stored (defaults to ~/.m2)

This will create a settings.xml that properly uses environment variables. It will also allow you to write the file in a specific location on the file system for more complex scenarios. The nice thing about this Action – you can run it multiple times to create multiple configurations. If the Java SDK is already installed, it will automatically skip that step!

So, we can use the an Action step like this to configure the file:

1- name: Configure Java
2  uses: actions/setup-java@v3
3  with:
4    distribution: 'temurin'
5    java-version: '11'
6    server-id: github
7    server-username: THE_USER_NAME
8    server-password: THE_PASSWORD

This will write a settings.xml with those environment variables:

1<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">
2  <servers>
3    <server>
4      <id>github</id>
5      <username>${env.THE_USER_NAME}</username>
6      <password>${env.THE_PASSWORD}</password>
7    </server>
8  </servers>
9</settings>

Now, we execute a Maven command and provide the specified environment variables. In this case, we can read the values from two secrets and make them available as environment variables:

1- name: Publish a package
2  run: mvn deploy
3  env:
4    THE_USERNAME: ${{ secrets.MAVEN_REPO_USERNAME }}
5    THE_PASSWORD: ${{ secrets.MAVEN_REPO_PASSWORD }}

And with that, you’re now successfully (and securely) generating a settings.xml with your credentials as part of the CI/CD process.

Happy DevOp’ing!