GitHub Actions and Maven releases
Tags: actionshackathon, maven, ci, releasemanagement
Published: 2020-08-30 17:08:28

I like GitLab a lot. Yet, there's no denying that GitHub has become the _de facto_ standard to host Open Source projects. With GitHub Actions, it's now possible to implement entire Continuous Integration pipelines without leaving GitHub.

In this post, I'd like to highlight how to release Maven _artifacts_ using GitHub Actions.

## Maven prerequisites

Before creating the <abbr title="Continuous Integration">CI</abbr> pipeline - and running it, it's necessary to configure the underlying Maven project.

### A quick Maven primer

Maven is a large subject, that deserves more than this post can cover. But to understand the rest of this post, here are the most important parts.

Maven references projects by a triplet of unique coordinates composed of:

1. A group ID
2. An artifact ID
3. A version

For example, the sample project's coordinates are:

```xml
<groupId>ch.frankel.blog.renamer</groupId>
<artifactId>renamer-swing</artifactId>
<version>1.0-SNAPSHOT</version>
```

The output of a project is an artifact _e.g._ a JAR.

You can append the version of a project with `-SNAPSHOT`. A _snapshot_ version is under development; a non-snapshot one, a regular release.

Some projects provide milestones, and/or release candidates before the final release. In that case, Maven doesn't natively offer a naming scheme for that. For example, the Spring framework append `.Mx`, `.RCx`, and `.RELEASE` to the version: they are **not** considered _snapshots_.

### Release management

The [Maven Release plugin](https://maven.apache.org/maven-release/maven-release-plugin/) manages the release process. It provides two complementary goals, `prepare` and `perform`. They do the following:

**Prepare:**

1. Change the version in the <abbr title="Project Object Model"">POM</abbr> from `x-SNAPSHOT` to a new version
2. Transform the <abbr title="Source Control Management">SCM</abbr> information in the POM to include the final destination of the tag
3. Commit the modified POM
4. Tag the code in the SCM with a version name
5. Bump the version in the POM to a new value `y-SNAPSHOT`
6. Commit the modified POM

**Perform:**

1. Checkout from an SCM URL with optional tag
2. Run the predefined Maven `deploy` goal

For the required SCM information, the Maven POM offers a dedicated section to configure it. With our sample, it looks like:

```xml
<scm>
  <developerConnection>scm:git:https://github.com/ajavageek/renamer-swing.git</developerConnection>
</scm>
```

### Authentication

The above snippet allows for different protocols: `git`, `http`, `https`, `ssh`, or even `file`. Authenticating with the `git` protocol requires a SSH key. In the context of a CI pipeline, this is not desirable.

Thus, the above configuration snippet needs to use the `http` protocol. This requires credentials in the form of a user/password pair. Maven decouples project-specific data, from data shared across projects, such as security-related data. The POM is responsible for the former, while the `$HOME/.m2/settings.xml` file holds the latter. Each credentials pair requires a unique identifier.

```xml
<settings>
  <servers>
    <server>
      <id>github</id>                     <!-- 1 -->
      <username>my_usernam</username>
      <password>my_password</password>
    </server>
  </servers>
</settings>
```

1. Unique identifier

To configure a Maven project to use a specific `server`, add a property with the `project.scm.id` key and the server's `id` as the value.

```xml
<properties>
  <project.scm.id>github</project.scm.id>  <!-- 1 -->
</properties>
```

1. The project will use the `github` server configured un the settings file

### Distribution management

Maven authors designed Maven around a plugin architecture. By itself, Maven just provides a build lifecycle, with a set of build phases. The Maven engine calls each phase in order; the cycle cannot advance to a later phase without having run through earlier phases. For example, phases in the build lifecycle include `compile`, `test`, `integration-test`, `package` and `deploy`. `test` cannot run without first running `compile`.

You can bind plugin goals to a specific phase. By default, Maven binds a couple of goals. For example, the `package` phase binds the `maven-jar-plugin:jar` goal. Running `mvn package` will execute all goals bounds to all phases in order, up until `package`.

Calling the `release:perform` goal launches a Maven fork that runs the `deploy` phase. By default, Maven binds the `deploy:deploy` goal of the Maven Deploy plugin to `deploy`. This implies Maven has run goals bound to the `package` phase **before**. For this reason, the build artifact is available when `deploy` starts. Deploying means that Maven will push the artifact to a _registry_ that needs to be explicitly configured.

In the context of this project, the artifact is a JAR, and the registry, GitHub. This translates into the following configuration snippet:

```
<distributionManagement>
  <repository>
    <id>github</id>
    <name>GitHub</name>
    <url>https://maven.pkg.github.com/ajavageek/renamer-swing</url>  <!-- 1 -->
  </repository>
</distributionManagement>
```

1. The root of GitHub registry is at https://maven.pkg.github.com

### The Maven command

The build should execute both the `release:prepare` and `release:perform` goals.

By default, `release:prepare` is interactive: the plugin asks for the release version, as well as for the new snapshot version. For an automated build, this is not possible.

It's possible to run Maven in batch mode by using the command-line `-B` flag. In that case, the plugin automatically infers both release and snapshot versions.

The command becomes:

```bash
mvn -B release:prepare release:perform
```

### The Maven wrapper

The final preparation step is to become fully independent of the Maven version.

In the future, the specific Maven version used to build the project might not be available anymore. Versions that are available at that time might not be compatible with it. For this reason, a Maven plugin provides a _wrapper_. A wrapper self-executable JAR that can kickstart a specific version of the `mvn` tool regardless of the environment. To create such a wrapper is straightforward:

```bash
mvn -N io.takari:maven:wrapper
```

This creates a couple of files, including a `.mvn` folder, inside the project. You need to add all those files in the SCM.

From this point on, instead of using the `mvn` command, you should use the `mvnw` script located at the root of the project. The build doesn't require a local Maven installation anymore.

## Building on GitHub

Now that we configured the POM, it's time to integrate Maven in the GitHub build.

The idea is to have two branches: `master` and `release`. Every time a commit happens on the `release` branch, GitHub should create a new release and a new deployment. At a later time, a manual step can then rebase/merge the `master` and `release` branches.

Remember that the `release:prepare` goal actually creates a tag. As an alternative, it's possible to configure Maven to reverse the default flow, so that it triggers the build on a tag, and then pushes to a branch.

### GitHub Actions introduction

Existing CI tools make use of a configuration file stored at the root of the repository:

| Tool | File |
|---|---|
| Jenkins | `Jenkinsfile` |
| GitLab | `.gitlab-ci.yml` |
| Travis CI | `.travis.yml` |

GitHub Actions follows a similar approach, with two important differences:

1. The project can make use of more than one build file - known as a _workflow_
2. They must be located in a `$PROJECT_ROOT/.github/workflows` folder

### Build steps

The first step is obviously to checkout the repo:

```yaml
- name: Checkout project
  uses: actions/checkout@v2
```

The next step is to install a JDK:

```yaml
- name: Setup Java JDK
  uses: actions/setup-java@v1.4.2
  with:
    java-version: 14                    # 1
    server-id: github                   # 2
```

1. Install Java 14
2. Create a Maven `settings.xml` file, and a server configured with the following value:
    * `id`: `github`
    * `username`: user that launches the build
    * `password`: `$GITHUB_TOKEN`

I lost quite some time trying to find an action that creates the `settings.xml` file. The `setup-java` action is actually the simplest way to achieve that. Just remember that it does more than configure just Java.

Because `release:prepare` actually `commit`, it's necessary to configure the user and email to use:

```yaml
- name: Configure Git user
  run: |
    git config user.email "actions@github.com"
    git config user.name "GitHub Actions"
```

The last step is actually the most straightforward.
Just run the Maven command from above.

```yaml
- name: Publish JAR
  run: ./mvnw -B release:prepare release:perform
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}     #1
```

1. `release:prepare` uses the credentials found in the `settings.xml` file, but GitHub Actions makes `deploy:deploy` use the `GITHUB_TOKEN` environment variable value.

## Proof-Of-Concept

When the `master` branch is ready for a release, just push it to the `release` branch:

```bash
git push origin master:release
```

This triggers the [workflow](https://github.com/ajavageek/renamer-swing/runs/1047299519?check_suite_focus=true).

![Screenshot of the log of a job well-done](https://dev-to-uploads.s3.amazonaws.com/i/zv2f92gbe1j64yw3s7y0.jpg)

The successful run of the workflow has created a release, a tag, and a deployment:

![Screenshot proving that the build created the release, the tag, and the deployment](https://dev-to-uploads.s3.amazonaws.com/i/62d9lmclz1rb8qex4xjt.jpg)

The final Git graph looks like this:

![Screenshot of the final Git graph](https://dev-to-uploads.s3.amazonaws.com/i/1ax7adz0hz5ni21leqls.jpg)

## Conclusion

Managing Maven releases with GitHub Actions is not trivial. Yet, it requires just a bit of experience to make it work. It's my hope that this post can help Maven users leverage the full power of GitHub Actions by providing this experience.

**To go further:**

* [Maven Release Plugin](https://maven.apache.org/maven-release/maven-release-plugin/)
* [Maven Deploy Plugin](https://maven.apache.org/plugins/maven-deploy-plugin/)
* [Publishing Java packages with Maven](https://docs.github.com/en/actions/language-and-framework-guides/publishing-java-packages-with-maven)
* [Setup Java JDK GitHub Action](https://github.com/marketplace/actions/setup-java-jdk)

_Originally published at [A Java Geek](https://blog.frankel.ch/github-actions-maven-releases/) on August 30<sup>th</sup>, 2020_