Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

I often have to build a pipeline to build projects in Java. Sometimes it's open source, sometimes it's not. I recently decided to try migrating some of my repositories from Travis-CI and TeamCity to GitHub Actions, and this is what came of it.

What will we automate

First we need a project that we will automate, let's make a small application on Spring boot / Java 11 / Maven. Within the framework of this article, the application logic will not interest us at all, the infrastructure around the application is important to us, so a simple controller REST API will suffice for us.

You can view the sources here: github.com/antkorwin/github-actions all stages of building a pipeline pipeline are reflected in the pull requests of this project.

Jira and planning

It is worth saying that we usually use JIRA as an issue tracker, so let's create a separate board for this project and put the first issues there:

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

A little later, we will return to what interesting things JIRA and GitHub can give in conjunction.

We automate the assembly of the project

Our test project is built via maven, so building it is pretty easy, all we need is mvn clean package.

To do this using Github Actions, we will need to create a file in the repository with a description of our workflow, this can be done with a regular yml file, I can’t say that I like “yml programming”, but what can I do - we do it in the .github / directory workflow/ build.yml file in which we will describe the actions when building the master branch:

name: Build

on:
  pull_request:
    branches:
      - '*'
  push:
    branches:
      - 'master'

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Maven Package
        run: mvn -B clean package -DskipTests

on is a description of the event on which our script will be launched.

on: pull_request/push - says that this workflow needs to be run every time you push to the master and create pull requests.

The following is a description of the tasksjobs) and execution steps (steps) for each task.

runs-on - here we can choose the target OS, surprisingly you can even choose Mac OS, but on private repositories this is quite an expensive pleasure (compared to linux).

uses allows you to reuse other actions, so for example with the actions/setup-java action we set the environment for Java 11.

By means of With we can specify the parameters with which we launch the action, in fact, these are the arguments that will be passed to the action.

It remains only to start the assembly of the project with maven: run: mvn -B clean package flag -B says that we need a non-interactive mode so that maven suddenly does not want to ask us something

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

Great! Now, with each commit to the master, the project build is started.

Automating the launch of tests

Building is good, but in reality, the project can build safely, but not work. Therefore, the next step is to automate the test run. In addition, it is quite convenient to see the result of passing tests when doing a PR review - you know for sure that the tests are passing and no one forgot to run their branch before doing the merge.

We run tests when creating a pull request and merge into the master, and at the same time add a code-coverage report.

name: Build

on:
  pull_request:
    branches:
      - '*'
  push:
    branches:
      - 'master'

jobs:
  build:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Maven Verify
        run: mvn -B clean verify
      - name: Test Coverage
        uses: codecov/codecov-action@v1
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

For test coverage, I use codecov in conjunction with the jacoco plugin. codecov has its own action, but it needs a token to work with our pull request:

${{ secrets.CODECOV_TOKEN }} - we will meet this construction more than once, secrets is a mechanism for storing secrets in the github, we can write passwords / tokens / hosts / urls and other data there that should not be shone in the code base of the repository.

You can add a variable to secrets in the repository settings on GitHub:

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

You can get a token at codecov.io after authorization through GitHub, to add a public project, you just need to follow the link of the form: GitHub user name/[reponame]. You can also add a private repository, for this you need to give codecov rights to the application in the github.

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

Add the jacoco plugin to the POM file:

<plugin>
	<groupId>org.jacoco</groupId>
	<artifactId>jacoco-maven-plugin</artifactId>
	<version>0.8.4</version>
	<executions>
		<execution>
			<goals>
				<goal>prepare-agent</goal>
			</goals>
		</execution>
		<!-- attached to Maven test phase -->
		<execution>
			<id>report</id>
			<phase>test</phase>
			<goals>
				<goal>report</goal>
			</goals>
		</execution>
	</executions>
</plugin>
<plugin>
	<groupId>org.apache.maven.plugins</groupId>
	<artifactId>maven-surefire-plugin</artifactId>
	<version>2.22.2</version>
	<configuration>
		<reportFormat>plain</reportFormat>
		<includes>
			<include>**/*Test*.java</include>
			<include>**/*IT*.java</include>
		</includes>
	</configuration>
</plugin>

Now the codecov bot will enter each of our pull requests and add a coverage change schedule:

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

Add a static analyzer

In most of my open source projects, I use sonar cloud for static code analysis, it is quite easy to connect it to travis-ci. So it's a logical step when migrating to GitHub Actions to do the same. The action market is a cool thing, but this time it let me down a bit, because out of habit I found the right action and added it to the workflow. But it turned out that sonar does not support working through an action for analyzing projects on maven or gradle. Of course, this is written in the documentation, but who reads it ?!

It is impossible through an action, so we will do it through the mvn plugin:

name: SonarCloud

on:
  push:
    branches:
      - master
  pull_request:
    types: [opened, synchronize, reopened]

jobs:
  sonarcloud:
    runs-on: ubuntu-16.04
    steps:
      - uses: actions/checkout@v1
      - name: Set up JDK
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Analyze with SonarCloud
#       set environment variables:
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
#       run sonar maven plugin:
        run: mvn -B verify sonar:sonar -Dsonar.projectKey=antkorwin_github-actions -Dsonar.organization=antkorwin-github -Dsonar.host.url=https://sonarcloud.io -Dsonar.login=$SONAR_TOKEN -Dsonar.coverage.jacoco.xmlReportPaths=./target/site/jacoco/jacoco.xml

SONAR_TOKEN - available at sonarcloud.io and you need to register it in secrets. GITHUB_TOKEN - this is a built-in token that github generates, with the help of it, sonarcloud[bot] will be able to log in to the git to leave us messages in pull requests.

Dsonar.projectKey — name of the project in sonar, you can see it in the project settings.

Dsonar.organization — organization name from GitHub.

We make a pull request and wait for sonarcloud[bot] to come in the comments:

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

Release management

The build is set up, the tests are run, and you can make a release. Let's take a look at how GitHub Actions can greatly simplify release management.

At work, I have projects whose codebase is in bitbucket (just like in that story “I write to bitbucket during the day, I commit to GitHub at night”). Unfortunately, bitbucket does not have built-in release management tools. This is a problem, because for each release, you have to manually start a page in confluence and throw off all the features included in the release there, woolen the palaces of the mind, tasks in jira, commits in the repository. There are many chances to make a mistake, you can forget something or write in what was already released last time, sometimes it’s just not clear what a pull request should be attributed to - is it a feature or a bug fix, or editing tests, or something infrastructural .

How can GitHub actions help us? There is a great action - release drafter, it allows you to set a release notes file template to set up categories of pull requests and automatically group them in a release notes file:

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

An example of a template for customizing a report (.github/release-drafter.yml):

name-template: 'v$NEXT_PATCH_VERSION'
tag-template: 'v$NEXT_PATCH_VERSION'
categories:
  - title: ' New Features'
    labels:
      - 'type:features'
# в эту категорию собираем все PR с меткой type:features

  - title: ' Bugs Fixes'
    labels:
      - 'type:fix'
# аналогично для метки type:fix и т.д.

  - title: ' Documentation'
    labels:
      - 'type:documentation'

  - title: ' Configuration'
    labels:
      - 'type:config'

change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
template: |
  ## Changes
  $CHANGES

Add a script to generate a draft release (.github/workflows/release-draft.yml):

name: "Create draft release"

on:
  push:
    branches:
      - master

jobs:
  update_draft_release:
    runs-on: ubuntu-18.04
    steps:
      - uses: release-drafter/release-drafter@v5
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

All pull requests from now on will be collected in release notes automatically - magic!

Here the question may arise: what if the developers forget to tag the PR? Then it is not clear what category it belongs to, and again you will have to deal with it manually, with each PR separately. To fix this problem, we can use another action - label verifier - it checks for the presence of tags on the pull request. If there is not a single required tag, then the check will fail and we will see a message about this in our pull request.

name: "Verify type labels"

on:
  pull_request:
    types: [opened, labeled, unlabeled, synchronize]

jobs:
  triage:
    runs-on: ubuntu-18.04
    steps:
      - uses: zwaldowski/match-label-action@v2
        with:
          allowed: 'type:fix, type:features, type:documentation, type:tests, type:config'

Now any pull-request needs to be marked with one of the tags: type:fix, type:features, type:documentation, type:tests, type:config.

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

Auto-annotating pull requests

Since we have touched on such a topic as effective work with pull requests, it is worth saying more about such an action as labeler, it puts tags in PR based on which files have been changed. For example, we can mark as [build] any pull request that has changes in the directory .github/workflow.

Connecting it is quite simple:

name: "Auto-assign themes to PR"

on:
  - pull_request

jobs:
  triage:
    runs-on: ubuntu-18.04
    steps:
      - uses: actions/labeler@v2
        with:
          repo-token: ${{ secrets.GITHUB_TOKEN }}

We also need a file describing the correspondence of project directories with pull request topics:

theme:build:
  - ".github/**"
  - "pom.xml"
  - ".travis.yml"
  - ".gitignore"
  - "Dockerfile"

theme:code:
  - "src/main/*"

theme:tests:
  - "src/test/*"

theme:documentation:
  - "docs/**"

theme:TRASH:
  - ".idea/**"
  - "target/**"

I didn’t manage to make friends between the action that automatically puts labels in pull requests and the action that checks for the presence of mandatory labels, the match-label doesn’t want to see the labels put by the bot. It seems easier to write your own action that combines both steps. But even in this form, it is quite convenient to use, you need to select a label from the list when creating a pull request.

Time to deploy

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

I've tried several deployment options via GitHub Actions (via ssh, via scp, and using docker-hub), and I can say that most likely you will find a way to upload the binary to the server, no matter how perverted your pipeline is.

I liked the option to keep all the infrastructure in one place, so let's look at how to deploy to GitHub Packages (this is a repository for binary content, npm, jar, docker).

Script for building a docker image and publishing it to GitHub Packages:

name: Deploy docker image

on:
  push:
    branches:
      - 'master'

jobs:

  build_docker_image:
    runs-on: ubuntu-18.04
    steps:

#     Build JAR:
      - uses: actions/checkout@v1
      - name: set up JDK 11
        uses: actions/setup-java@v1
        with:
          java-version: 1.11
      - name: Maven Package
        run: mvn -B clean compile package -DskipTests

#     Set global environment variables:
      - name: set global env
        id: global_env
        run: |
          echo "::set-output name=IMAGE_NAME::${GITHUB_REPOSITORY#*/}"
          echo "::set-output name=DOCKERHUB_IMAGE_NAME::docker.pkg.github.com/${GITHUB_REPOSITORY}/${GITHUB_REPOSITORY#*/}"

#     Build Docker image:
      - name: Build and tag image
        run: |
          docker build -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:latest" -t "${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}:${GITHUB_SHA::8}" .

      - name: Docker login
        run: docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}

#     Publish image to github package repository:
      - name: Publish image
        env:
          IMAGE_NAME: $GITHUB_REPOSITORY
        run: docker push "docker.pkg.github.com/$GITHUB_REPOSITORY/${{ steps.global_env.outputs.IMAGE_NAME }}"

First we need to build the JAR file of our application, after which we calculate the path to the GitHub docker registry and the name of our image. There are a few tricks here that we haven't come across yet:

  • construction like: echo "::set-output name=NAME::VALUE" allows you to set the value of a variable in the current step, so that it can then be read in all other steps.
  • you can get the value of the variable set in the previous step through the identifier of this step: ${{ steps.global_env.outputs.DOCKERHUB_IMAGE_NAME }}
  • The standard variable GITHUB_REPOSITORY stores the name of the repository and its owner (“owner/repo-name”). To strip everything except the repository name from this line, use the bash syntax: ${GITHUB_REPOSITORY#*/}

Next, we need to build the docker image:

docker build -t "docker.pkg.github.com/antkorwin/github-actions/github-actions:latest"

Log in to the registry:

docker login docker.pkg.github.com -u $GITHUB_ACTOR -p ${{secrets.GITHUB_TOKEN}}

And publish the image to the GitHub Packages Repository:

docker push "docker.pkg.github.com/antkorwin/github-actions/github-actions"

In order to specify the version of the image, we use the first digits from the SHA hash of the commit - GITHUB_SHA also has nuances here, if you do such builds not only during merge in master, but also on the pull request creation event, then SHA may not match the hash we see in the git history because the actions/checkout action makes its own unique hash to avoid action deadlocks in PR.

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

If everything turned out well, then opening the packages section (https://github.com/antkorwin/github-actions/packages) in the repository, you will see a new docker image:

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

You can also see the list of versions of the docker image there.

It remains only to configure our server to work with this registry and restart the service. About how to do this through systemd, I, perhaps, will tell another time.

Monitoring

Let's see a simple way to health check our application using GitHub Actions. Our boot application has an actuator, so you don’t even need to write an API to check its state, everything has already been done for the lazy. You just need to pull the host: SERVER-URL:PORT/actuator/health

$ curl -v 127.0.0.1:8080/actuator/health

> GET /actuator/health HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.61.1
> Accept: */*

< HTTP/1.1 200
< Content-Type: application/vnd.spring-boot.actuator.v3+json
< Transfer-Encoding: chunked
< Date: Thu, 04 Jun 2020 12:33:37 GMT

{"status":"UP"}

All we need is to write a task for checking the server by cron, but if suddenly he does not answer us, then we will send a notification in telegrams.

First, let's figure out how to run workflow by cron:

on:
  schedule:
    - cron:  '*/5 * * * *'

Everything is simple, I can’t even believe that in github you can make such events that do not fit into webhooks at all. Details are in the documentation: help.github.com/en/actions/reference/events-that-trigger-workflows#scheduled-events-schedule

Let's check the server status manually using curl:

jobs:
  ping:
    runs-on: ubuntu-18.04
    steps:

      - name: curl actuator
        id: ping
        run: |
          echo "::set-output name=status::$(curl ${{secrets.SERVER_HOST}}/api/actuator/health)"

      - name: health check
        run: |
          if [[ ${{ steps.ping.outputs.status }} != *"UP"* ]]; then
            echo "health check is failed"
            exit 1
          fi
          echo "It's OK"

First, we save into a variable what the server responded to the request, at the next step we check that the status is UP and, if it is not, then we exit with an error. If you need to "fill up" the action with your hands, then exit 1 - the right weapon.

  - name: send alert in telegram
    if: ${{ failure() }}
    uses: appleboy/telegram-action@master
    with:
      to: ${{ secrets.TELEGRAM_TO }}
      token: ${{ secrets.TELEGRAM_TOKEN }}
      message: |
        Health check of the:
        ${{secrets.SERVER_HOST}}/api/actuator/health
        failed with the result:
        ${{ steps.ping.outputs.status }}

Sending to telegrams is done only if the action failed in the previous step. To send a message, we use appleboy/telegram-action, you can read about how to get the bot token and chat id in the documentation: github.com/appleboy/telegram-action

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

Do not forget to write in the secrets on the github: the URL for the server and the tokens for the telegram bot.

Bonus track - JIRA for the lazy

I promised that we would return to JIRA, and we returned. Hundreds of times I watched a situation at stand-ups when developers made a feature, merged a branch, but forgot to drag the task to JIRA. Of course, if all this was done in one place, it would be easier, but in fact we write code in the IDE, merge branches into bitbucket or GitHub, and then drag the tasks to Jira, for this we need to open new windows, sometimes log in again and etc. When you perfectly remember what you need to do next, then there is no point in opening the board once again. As a result, in the morning at stand-up, you have to spend time updating the task board.

GitHub will help us in this routine as well, for starters, we can drag tasks automatically into the code_review column when we uploaded a pull request. All you need to do is stick to the branch naming convention:

[имя проекта]-[номер таска]-название

for example, if the "GitHub Actions" project key is GA, then GA-8-jira-bot may be a branch for the implementation of the GA-8 task.

JIRA integration works through actions from Atlassian, they are not perfect, I must say that some of them did not work for me at all. But we will discuss only those that definitely work and are actively used.

First you need to go through authorization in JIRA using the action: atlassian/gajira-login

jobs:
  build:
    runs-on: ubuntu-latest
    name: Jira Workflow
    steps:
      - name: Login
        uses: atlassian/gajira-login@master
        env:
          JIRA_BASE_URL: ${{ secrets.JIRA_BASE_URL }}
          JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }}
          JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}

To do this, you need to get a token in JIRA, how to do this is described here: confluence.atlassian.com/cloud/api-tokens-938839638.html

Extract the task ID from the branch name:

  - name: Find Issue
    id: find_issue
    shell: bash
    run: |
      echo "::set-output name=ISSUE_ID::$(echo ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}')"
      echo brach name: $GITHUB_HEAD_REF
      echo extracted issue: ${GITHUB_HEAD_REF} | egrep -o 'GA-[0-9]{1,4}'

  - name: Check Issue
    shell: bash
    run: |
      if [[ "${{steps.find_issue.outputs.ISSUE_ID}}" == "" ]]; then
        echo "Please name your branch according to the JIRA issue: [project_key]-[task_number]-branch_name"
        exit 1
      fi
      echo succcessfully found JIRA issue: ${{steps.find_issue.outputs.ISSUE_ID}}

If you search the GitHub marketplace, you can find an action for this task, but I had to write the same thing through grep by the name of the branch, because this action from Atlassian did not want to work on my project in any way, to figure out what was wrong there - longer than doing the same by hand.

It remains only to move the task to the “Code review” column when creating a pull request:

  - name: Transition issue
    if: ${{ success() }}
    uses: atlassian/gajira-transition@master
    with:
      issue: ${{ steps.find_issue.outputs.ISSUE_ID }}
      transition: "Code review"

There is a special action on GitHub for this, all it needs is the issue ID obtained in the previous step and the authorization in JIRA that we did above.

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

In the same way, you can drag tasks when merge to the master, and other events from the GitHub workflow. In general, it all depends on your imagination and desire to automate routine processes.

Conclusions

If you look at the classic DEVOPS diagram, we have covered all the stages, except for operating, I think if you try, you can find some action in the market for integration with the help-desk system, so we will assume that the pipeline turned out to be solid and on conclusions can be drawn based on its use.

Circles of hell with GitHub Actions (building a CI/CD pipeline for a Java project)

Pros:

  • Marketplace with ready-made actions for all occasions, it's very cool. In most of them, you can also look at the source codes in order to understand how to solve a similar problem or post a feature request to the author directly in the github repository.
  • The choice of the target platform for the build: Linux, mac os, windows is quite an interesting feature.
  • Github Packages is a great thing, it’s convenient to keep the entire infrastructure in one place, you don’t have to surf through different windows, everything is within a radius of one or two mouse clicks and is perfectly integrated with GitHub Actions. Docker registry support in the free version is also a good advantage.
  • GitHub hides secrets in the build logs, so using it to store passwords and tokens is not so scary. For all the time of experiments, I have never been able to see the secret in its pure form in the console.
  • Free for Open Source projects

Cons:

  • YML, well, I don't like him. When working with such a flow, my most frequent commit message is “fix yml format”, then you forget to put a tab somewhere, or you write on the wrong line. In general, sitting in front of a screen with a protractor and a ruler is not the most pleasant experience.
  • DEBUG, debugging flow commits, launching rebuilds and outputting to the console is not always convenient, but it’s more like “you are snickering”, you are used to working with convenient IDEAs when you can debug anything you want.
  • You can write your action on anything if you wrap it in docker, but only javascript is natively supported, of course this is a matter of taste, but I would prefer something else instead of js.

Let me remind you that the repository with all the scripts is here: github.com/antkorwin/github-actions

Next week I will be performing with report at the Heisenbug 2020 Piter conference. I will tell you not only how to avoid mistakes when preparing test data, but also share my secrets of working with data sets in Java applications!

Source: habr.com