Using GitHub Actions to Build a Java Project With Pull Request Coverage Commenting and Coverage Badges
Tags: java, github, tutorial, programming
Published: 2022-11-09 17:24:58

This post is the second in a series of GitHub Actions workflows for Java projects. In it, I explain the GitHub Actions workflow that I run on pushes and pull requests, for building, testing, generating coverage badges, and commenting the coverage percentages on pull requests. The status of this workflow is also used as a required pull request check (e.g., the build including unit tests must pass in order to allow merging).

__Table of Contents:__
* [Workflow Step by Step](#workflow-step-by-step) walks you through my workflow one step at a time.
  * [Triggering the workflow](#triggering-the-workflow)
  * [Checking out the repository](#checking-out-the-repository)
  * [Checking out the badges branch](#checking-out-the-badges-branch)
  * [Setup Java](#setup-java)
  * [Build including generating a code coverage report](#build-including-generating-a-code-coverage-report)
  * [Generate coverage badges](#generate-coverage-badges)
  * [Upload the full coverage report as a workflow artifact](#upload-the-full-coverage-report-as-a-workflow-artifact)
  * [Commit and push the coverage badges](#commit-and-push-the-coverage-badges)
  * [Comment the coverage percentages on pull requests](#comment-the-coverage-percentages-on-pull-requests)
* [Complete Workflow](#complete-workflow).
* [Repositories Used in This Post](#repositories-used-in-this-post).
  * [Live Example](#live-example) from one of my projects on GitHub.
  * [The jacoco-badge-generator GitHub Action](#the-jacocobadgegenerator-github-action).
* [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.

### Triggering the workflow

My build workflow is set up to run based on multiple events. This includes the `pull_request` event to run on all pull requests, and `workflow_dispatch` events, enabling me to run it manually if desired. I also have it configured to run on `push` events, but only when the push includes changes to either java files, the Maven `pom.xml` for the project, or when the build workflow itself is changed. The reason I restrict when it runs on pushes is so that I am not wasting cycles on the runner if I'm just making edits to the readme or other similar minor changes that don't impact the build itself. I don't make this restriction on pull requests because I'm using this workflow as a required pull request check, so I need it to run even if the PR doesn't include source code changes to prevent blocking the PR.

```YAML
name: build

on:
  push:
    branches: [ master ]
    paths: [ '**.java', '.github/workflows/build.yml', 'pom.xml' ]
  pull_request:
    branches: [ master ]
  workflow_dispatch:
```

### Checking out the repository

The workflow has a single job `build`, runs on Ubuntu, and begins with the usual checkout step, using the `actions/checkout` action.

```YAML
jobs:
  build:

    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout
      uses: actions/checkout@v3
```

### Checking out the badges branch

One of the later steps of the workflow generates coverage badges that are then stored within the repository. However, the badges are stored in a separate branch that I've named `badges`, rather than the default branch. I do it this way because my default branch is protected. In order to push the badges to the default branch, I'd need to use a personal access token with permissions elevated above the default permissions of the `GITHUB_TOKEN`. I don't want to do that for security reasons. I also don't want to use any of the other approaches I've seen to circumventing branch protection rules in a GitHub Actions workflow.

Instead, I use a dedicated branch whose only purpose is to store coverage badges, which I can still embed in the readme in the default branch. So my workflow has a second `actions/checkout` step to checkout the `badges` branch to a `badges` directory that I've nested within the other, as seen below:

```YAML
    - name: Checkout badges branch to a badges directory nested inside first checkout
      uses: actions/checkout@v3
      with:
        ref: badges
        path: badges
```

### Setup Java

Next, I use the `actions/setup-java` action to setup the Adoptium distribution of Java 17.

```YAML
    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        distribution: 'adopt'
        java-version: '17'
```

### Build including generating a code coverage report

Next, we build the library with Maven. I use JaCoCo for code coverage. I've configured the JaCoCo Maven plugin within a Maven profile with id `coverage` within my `pom.xml`. See my [earlier 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), which included an [example of how I configure code coverage within a Maven profile](https://dev.to/cicirello/how-to-use-maven-profiles-to-selectively-activate-plugins-and-other-configuration-from-the-command-line-2i2b#profile-to-enable-deactivating-a-plugin-at-build-time). To activate that profile and generate the code coverage report, I use the command line option `-Pcoverage`.

```YAML
    - name: Build with Maven
      run: mvn -B package -Pcoverage
```

### Generate coverage badges

To generate coverage badges, I use a GitHub Action that I develop and maintain, the [jacoco-badge-generator](https://github.com/cicirello/jacoco-badge-generator). I've written about that GitHub Action here on DEV in the past. Here is one such post:

{% link https://dev.to/cicirello/the-jacoco-badge-generator-github-action-is-now-also-available-as-a-cli-tool-from-pypi-3ma0 %}

In configuring the use of this action within this workflow, I've used its `badges-directory` input to change the directory of where the badges are created to correspond to the directory that contains my earlier nested checkout of the `badges` branch. Additionally, the action's default only generates the instructions coverage badge, so I additionally use `generate-branches-badge: true` to also generate a branches coverage badge. This action can also optionally generate a simple JSON file containing the coverage percentages, so I activate that with `generate-summary: true`. One of my later steps uses that JSON summary when commenting on pull requests. After the [jacoco-badge-generator](https://github.com/cicirello/jacoco-badge-generator) step below, I'm also showing a step that I use to log the coverage percentages to the workflow run log.

```YAML
    - name: Generate JaCoCo badge
      id: jacoco
      uses: cicirello/jacoco-badge-generator@v2
      with:
        badges-directory: badges
        generate-branches-badge: true
        generate-summary: true

    - name: Log coverage percentages to workflow output
      run: |
        echo "coverage = ${{ steps.jacoco.outputs.coverage }}"
        echo "branches = ${{ steps.jacoco.outputs.branches }}"
```

### Upload the full coverage report as a workflow artifact

I then use the `actions/upload-artifact` action to upload the full JaCoCo coverage report as a workflow artifact. This way I can download it from the workflow run via the `Actions` tab if I need to see the details. I'm using Maven, along with the default location of the JaCoCo reports, so they are found in the directory `target/site/jacoco/`. The `actions/upload-artifact` action creates a zip file with the entire contents of that directory, which in this case will include all of JaCoCo's various reports, such as the detailed HTML report, as well as the XML, and CSV versions.

```YAML
    - name: Upload JaCoCo coverage report as a workflow artifact
      uses: actions/upload-artifact@v3
      with:
        name: jacoco-report
        path: target/site/jacoco/
```

### Commit and push the coverage badges

The [jacoco-badge-generator](https://github.com/cicirello/jacoco-badge-generator) generates badges, but does not commit them. In this step, I just use a simple shell script to commit and push the badges. This step is conditional and runs only if the event that started the workflow is not a pull request (see the `if: ${{ github.event_name != 'pull_request' }}`). In other words, it runs on `push` and `workflow_dispatch` events. The coverage badges should be consistent with the state of the default branch, so committing badges that correspond to the coverage of a pull request that may or may not be merged doesn't make sense. If it is merged, the push event will then cause the workflow to run again, at which point the coverage badges will be committed. This step begins by changing the current directory to the directory where the `badges` branch was checked out. And it commits and pushes only if an `svg` or `json` file changed. The badges are SVGs, and recall the earlier step where I configured the [jacoco-badge-generator](https://github.com/cicirello/jacoco-badge-generator) to additionally generate a simple JSON file containing the coverage percentages.

```YAML
    - name: Commit and push the coverage badges and summary file
      if: ${{ github.event_name != 'pull_request' }}
      run: |
        cd badges
        if [[ `git status --porcelain *.svg *.json` ]]; then
          git config --global user.name 'github-actions'
          git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
          git add *.svg *.json
          git commit -m "Autogenerated JaCoCo coverage badges" *.svg *.json
          git push
        fi
```

### Comment the coverage percentages on pull requests

In this last step, I use the GitHub CLI to comment the coverage percentages on pull requests. This step is conditional, like the previous, but this time it only runs on pull requests (see the `if: ${{ github.event_name == 'pull_request' }}`). It is a simple shell script. The first few lines uses `jq` to parse the `coverage-summary.json` produced by [jacoco-badge-generator](https://github.com/cicirello/jacoco-badge-generator), and then generates a string with the content for a pull request comment. The last line then uses the GitHub CLI to comment on the pull request. The GitHub CLI requires authentication via the `GITHUB_TOKEN` passed as an environment variable.

```YAML
    - name: Comment on PR with coverage percentages
      if: ${{ github.event_name == 'pull_request' }}
      run: |
        REPORT=$(<badges/coverage-summary.json)
        COVERAGE=$(jq -r '.coverage' <<< "$REPORT")%
        BRANCHES=$(jq -r '.branches' <<< "$REPORT")%
        NEWLINE=$'\n'
        BODY="## JaCoCo Test Coverage Summary Statistics${NEWLINE}* __Coverage:__ ${COVERAGE}${NEWLINE}* __Branches:__ ${BRANCHES}"
        gh pr comment ${{github.event.pull_request.number}} -b "${BODY}"
      continue-on-error: true
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```

At the present time, I've set up this last step with `continue-on-error: true`, which will prevent this step from causing the workflow run to fail. The default permissions of the `GITHUB_TOKEN` are not sufficient for commenting on pull requests coming from forks. I need to elevate the permissions granted the `GITHUB_TOKEN` in order to do so, which is on my to-do list. Even without the commenting, the full coverage reports are uploaded as a workflow artifact during one of the earlier steps.


## Complete Workflow

Here's my full workflow.

```YAML
name: build

on:
  push:
    branches: [ master ]
    paths: [ '**.java', '.github/workflows/build.yml', 'pom.xml' ]
  pull_request:
    branches: [ master ]
  workflow_dispatch:

jobs:
  build:

    runs-on: ubuntu-latest
    
    steps:
    - name: Checkout
      uses: actions/checkout@v3

    - name: Checkout badges branch to a badges directory nested inside first checkout
      uses: actions/checkout@v3
      with:
        ref: badges
        path: badges

    - name: Set up JDK 17
      uses: actions/setup-java@v3
      with:
        distribution: 'adopt'
        java-version: '17'

    - name: Build with Maven
      run: mvn -B package -Pcoverage

    - name: Generate JaCoCo badge
      id: jacoco
      uses: cicirello/jacoco-badge-generator@v2
      with:
        badges-directory: badges
        generate-branches-badge: true
        generate-summary: true

    - name: Log coverage percentages to workflow output
      run: |
        echo "coverage = ${{ steps.jacoco.outputs.coverage }}"
        echo "branches = ${{ steps.jacoco.outputs.branches }}"

    - name: Upload JaCoCo coverage report as a workflow artifact
      uses: actions/upload-artifact@v3
      with:
        name: jacoco-report
        path: target/site/jacoco/

    - name: Commit and push the coverage badges and summary file
      if: ${{ github.event_name != 'pull_request' }}
      run: |
        cd badges
        if [[ `git status --porcelain *.svg *.json` ]]; then
          git config --global user.name 'github-actions'
          git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
          git add *.svg *.json
          git commit -m "Autogenerated JaCoCo coverage badges" *.svg *.json
          git push
        fi

    - name: Comment on PR with coverage percentages
      if: ${{ github.event_name == 'pull_request' }}
      run: |
        REPORT=$(<badges/coverage-summary.json)
        COVERAGE=$(jq -r '.coverage' <<< "$REPORT")%
        BRANCHES=$(jq -r '.branches' <<< "$REPORT")%
        NEWLINE=$'\n'
        BODY="## JaCoCo Test Coverage Summary Statistics${NEWLINE}* __Coverage:__ ${COVERAGE}${NEWLINE}* __Branches:__ ${BRANCHES}"
        gh pr comment ${{github.event.pull_request.number}} -b "${BODY}"
      continue-on-error: true
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
```


## Repositories Used in this Post

### Live Example

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

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

### The jacoco-badge-generator GitHub Action

One of the steps of my workflow uses the [jacoco-badge-generator](https://github.com/cicirello/jacoco-badge-generator) to parse and summarize the JaCoCo coverage report, producing coverage badges and a JSON summary file. Here is the repository for that action itself:

{% github cicirello/jacoco-badge-generator %}


## 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/ %}