Deploy a Documentation Website for a Java Library Using GitHub Actions
Tags: java, devops, github, webdev
Published: 2022-11-30 19:15:43

This post continues my series on GitHub Actions workflows for Java projects. I maintain a few open source Java libraries that I've developed from my research projects. I use GitHub Actions to automate a variety of tasks. I regularly release updates to these libraries to the Maven Central Repository, GitHub Packages, and JitPack. I have a pair of GitHub workflows that automate the deployment process. One of these deploys the library's artifacts itself, and was covered in an earlier DEV post: [Deploying Java Artifacts to Multiple Maven Repositories with GitHub Actions](https://dev.to/cicirello/deploying-java-artifacts-to-multiple-maven-repositories-with-github-actions-4idf). Additionally, I have a workflow that deploys the API documentation of the library to a website hosted on GitHub Pages. My documentation workflow is the subject of this post, and includes steps for generating javadoc documentation, post-processing the output of javadoc to inject canonical links and a few other things into the head of the pages, updating an XML sitemap, and deploying to GitHub Pages.

__Table of Contents:__
* [Workflow Step by Step](#workflow-step-by-step) walks you through my documentation deployment workflow one step at a time.
* [Complete Workflow](#complete-workflow).
* [Live Example](#live-example) from one of my projects on GitHub.
* [Tools Used in the Workflow](#tools-used-in-the-workflow).
* [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.

### Workflow Events

I've designed the workflow to run on several different events, although the specific behavior of the workflow depends on the type of event that triggered it to run:
* It runs on `push` and `pull_request` events to the default branch, but for those events the workflow only verifies that the documentation builds without errors or warnings, and does not deploy. I want to keep the documentation website up to date with the latest release, so the deployment related steps are skipped during `push` and `pull_request` events, which you'll see later in the workflow. For pull requests, this workflow is also used as a required check. If the documentation doesn't build without errors or warnings, then this workflow will block the merge.
* It also runs on `release` events, specifically when a release is created, in which case the full workflow runs, including the steps related to deployment.
* I've also configured it to run on `workflow_dispatch` events so that I have the option to run it manually if necessary, such as to force out an important documentation update that applies to the current release.

```YAML
name: docs

on:
  push:
    branches: [ master ]
    paths: [ '**.java', '.github/workflows/docs.yml' ]
  pull_request:
    branches: [ master ]
  release:
    types: [created]
  workflow_dispatch:
```

### Checking Out the Repository

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

```YAML
jobs:
  docs:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout the repo
      uses: actions/checkout@v3
```

### Checking Out the `gh-pages` Branch

My documentation deployment workflow includes a second `actions/checkout` step. The documentation website is hosted on GitHub Pages, and is served from the `gh-pages` branch of the repository. This step checks out the `gh-pages` branch to a directory that I've also named `gh-pages` (see the `path` input to `actions/checkout`) nested within the first checkout. One of the later steps of the workflow uses a GitHub Action that I've implemented to generate an XML sitemap for the documentation site. That action uses the commit dates of the files to generate `<lastmod>` tags in the sitemap. So the step to checkout the `gh-pages` branch uses the input `fetch-depth: 0` of `actions/checkout` to include the complete git history of the `gh-pages` branch.

```YAML
    - name: Checkout the gh-pages branch
      uses: actions/checkout@v3
      with:
        fetch-depth: 0
        ref: gh-pages
        path: gh-pages
```

### Build the Documentation with Javadoc

Below you see the next two workflow steps. First, I set up the Adoptium distribution of JDK 17 with the `actions/setup-java` action. This project uses Maven for builds, so after setting up Java, I run javadoc with `mvn compile javadoc:javadoc`.

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

    - name: Build docs with Maven
      run: mvn compile javadoc:javadoc
```

### Copy the Documentation to Website Location

This next step is conditional, and only runs on `release` and `workflow_dispatch` events. Recall that the workflow also runs on `push` and `pull_request` events to verify that the documentation builds successfully. By using a conditional step here, I ensure that the documentation is only deployed to the website when a release is created to keep the documentation site up to date with the current release. The next several steps are actually conditional as well for the same reason.

Also recall that earlier the `gh-pages` branch was checked out to a directory `gh-pages` by a second use of `actions/checkout`. At the root of that branch I have a webpage (in AMP HTML) about the library, as well as a few other things, such as a sitemap.xml, a robots.txt, etc. The javadoc documentation is in a subdirectory `api`. So below the first thing I do is delete the contents of the `api` directory with `rm -rf gh-pages/api`. The purpose is to avoid accidentally leaving any stray obsolete files (e.g., perhaps the release removed a class). The `cp -rf target/site/apidocs/. gh-pages/api` then copies the javadocs from Maven's default location to the `api` directory of the `gh-pages` branch.

```YAML
    - name: Copy to Documentation Website Location
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        rm -rf gh-pages/api
        cp -rf target/site/apidocs/. gh-pages/api
```

### Post-Process the Javadoc Output

The next step uses a GitHub Action that I've implemented [javadoc-cleanup](https://github.com/cicirello/javadoc-cleanup) to insert [canonical URLs](https://developers.google.com/search/docs/crawling-indexing/consolidate-duplicate-urls) into the head of each javadoc page. I also use it to insert a [referrer policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Referrer-Policy) of `strict-origin-when-cross-origin` into the head of each javadoc page, as well as links to my project's [favicon](https://en.wikipedia.org/wiki/Favicon). This step is conditional, like the previous step, and only runs on `release` and `workflow_dispatch` events.

I have a recent DEV post about [javadoc-cleanup](https://github.com/cicirello/javadoc-cleanup) if you want to learn more about what you can use it for, and how to use it:

{% link https://dev.to/cicirello/post-process-javadoc-generated-documentation-in-github-actions-before-deploying-to-the-web-11k8 %}

```YAML
    - name: Tidy up the javadocs
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      id: tidy
      uses: cicirello/javadoc-cleanup@v1
      with:
        base-url-path: https://chips-n-salsa.cicirello.org/
        path-to-root: gh-pages
        user-defined-block: |
          <meta name="referrer" content="strict-origin-when-cross-origin">
          <link rel="icon" href="/images/favicon48.png" sizes="16x16 32x32 48x48" type="image/png">
          <link rel="icon" href="/images/favicon96.png" sizes="96x96" type="image/png">
          <link rel="icon" href="/images/favicon144.png" sizes="144x144" type="image/png">
          <link rel="icon" href="/images/favicon192.png" sizes="192x192" type="image/png">
          <link rel="icon" href="/images/favicon384.png" sizes="384x384" type="image/png">

    - name: Log javadoc-cleanup output
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        echo "modified-count = ${{ steps.tidy.outputs.modified-count }}"
```

### Commit the Documentation

Just like the prior steps, this next only runs on `release` and `workflow_dispatch` events. Specifically, I use a simple shell script to commit the javadoc documentation to the `gh-pages` branch. The `cd gh-pages` ensures I'm in the directory `gh-pages` where I've checked out that branch. I do the commit as the GitHub Actions bot. Note that this step doesn't push yet.

```YAML
    - name: Commit documentation changes without pushing yet
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        cd gh-pages
        if [[ `git status --porcelain` ]]; then
          git config --global user.name 'github-actions'
          git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
          git add -A
          git commit -m "Automated API website updates."
        fi
        cd ..
```

The reason I didn't push yet is because in the next step you'll see how I generate a sitemap. This step committing the javadocs is necessary first because the sitemap generation relies on the commit dates to generate `<lastmod>` tags.

### Generate an XML Sitemap

On `release` and `workflow_dispatch` events, use the [generate-sitemap](https://github.com/cicirello/generate-sitemap) GitHub Action, which I maintain, to generate an XML sitemap for the documentation website. The first step below generates the sitemap, and the second one below just logs some information about that action's run to the workflow run's log (e.g., number of URLs in the sitemap, and number excluded by either robots.txt or `noindex` directives).

```YAML
    - name: Generate the sitemap
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      id: sitemap
      uses: cicirello/generate-sitemap@v1
      with:
        base-url-path: https://chips-n-salsa.cicirello.org/
        path-to-root: gh-pages

    - name: Output stats
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        echo "sitemap-path = ${{ steps.sitemap.outputs.sitemap-path }}"
        echo "url-count = ${{ steps.sitemap.outputs.url-count }}"
        echo "excluded-count = ${{ steps.sitemap.outputs.excluded-count }}"
```

I have a recent DEV post about [generate-sitemap](https://github.com/cicirello/generate-sitemap) if you want to learn more about it:

{% link https://dev.to/cicirello/generate-an-xml-sitemap-for-a-static-website-in-github-actions-20do %}


### Commit the Sitemap and Push to Website

Finally, we can commit the sitemap generated above to the `gh-pages`, by switching to the `gh-pages` directory where it is checked out, committing, and then pushing everything.

```YAML
    - name: Commit documentation website sitemap and push all commits
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        cd gh-pages
        if [[ `git status --porcelain` ]]; then
          git config --global user.name 'github-actions'
          git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
          git add -A
          git commit -m "Automated API website sitemap update."
        fi
        git push
        cd ..
```

The updated documentation has now been deployed to the project's website.


## Complete Workflow

Here's my complete Java library documentation deployment workflow.

```YAML
name: docs

on:
  push:
    branches: [ master ]
    paths: [ '**.java', '.github/workflows/docs.yml' ]
  pull_request:
    branches: [ master ]
  release:
    types: [created]
  workflow_dispatch:

jobs:
  docs:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout the repo
      uses: actions/checkout@v3

    - name: Checkout the gh-pages branch
      uses: actions/checkout@v3
      with:
        fetch-depth: 0
        ref: gh-pages
        path: gh-pages

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

    - name: Build docs with Maven
      run: mvn compile javadoc:javadoc

    - name: Copy to Documentation Website Location
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        rm -rf gh-pages/api
        cp -rf target/site/apidocs/. gh-pages/api

    - name: Tidy up the javadocs
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      id: tidy
      uses: cicirello/javadoc-cleanup@v1
      with:
        base-url-path: https://chips-n-salsa.cicirello.org/
        path-to-root: gh-pages
        user-defined-block: |
          <meta name="referrer" content="strict-origin-when-cross-origin">
          <link rel="icon" href="/images/favicon48.png" sizes="16x16 32x32 48x48" type="image/png">
          <link rel="icon" href="/images/favicon96.png" sizes="96x96" type="image/png">
          <link rel="icon" href="/images/favicon144.png" sizes="144x144" type="image/png">
          <link rel="icon" href="/images/favicon192.png" sizes="192x192" type="image/png">
          <link rel="icon" href="/images/favicon384.png" sizes="384x384" type="image/png">

    - name: Log javadoc-cleanup output
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        echo "modified-count = ${{ steps.tidy.outputs.modified-count }}"

    - name: Commit documentation changes without pushing yet
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        cd gh-pages
        if [[ `git status --porcelain` ]]; then
          git config --global user.name 'github-actions'
          git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
          git add -A
          git commit -m "Automated API website updates."
        fi
        cd ..

    - name: Generate the sitemap
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      id: sitemap
      uses: cicirello/generate-sitemap@v1
      with:
        base-url-path: https://chips-n-salsa.cicirello.org/
        path-to-root: gh-pages

    - name: Output stats
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        echo "sitemap-path = ${{ steps.sitemap.outputs.sitemap-path }}"
        echo "url-count = ${{ steps.sitemap.outputs.url-count }}"
        echo "excluded-count = ${{ steps.sitemap.outputs.excluded-count }}"

    - name: Commit documentation website sitemap and push all commits
      if: ${{ github.event_name == 'release' || github.event_name == 'workflow_dispatch' }}
      run: |
        cd gh-pages
        if [[ `git status --porcelain` ]]; then
          git config --global user.name 'github-actions'
          git config --global user.email '41898282+github-actions[bot]@users.noreply.github.com'
          git add -A
          git commit -m "Automated API website sitemap update."
        fi
        git push
        cd ..
```


## Live Example

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

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



## Tools Used in the Workflow

More information about the [javadoc-cleanup](https://github.com/cicirello/javadoc-cleanup) and [generate-sitemap](https://github.com/cicirello/generate-sitemap) GitHub Actions is found on a website about the various GitHub Actions that I maintain, as well as within the corresponding GitHub repositories:

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

{% github cicirello/javadoc-cleanup %}

{% github cicirello/generate-sitemap %}



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