In a new project that started in October, we were free to choose most of our tooling. I can never tell if fewer constraints make things easier or harder. With little choice, you have fewer mistakes to make. But if things are entirely up to you, you may have only yourself to blame.

Personally, I really wanted to give GitHub Actions a go. Having spent many years using Jenkins setups, I figured the idea of outsourcing CI/CD to GitHub made a lot of sense. Fast forward to a few weeks later, we have a fully functional Continuous Delivery structure. Let me give you a quick tour.

STEP 1: APPLICATION BUILD

At this point, we are developing two components. One is a frontend React application, the other a backend Spring application. In both cases, we have separated the checks to run for pull requests from the process of building a new artefact to deploy to a test environment.

To start using GitHub Actions, all you need is a YAML file in the .github/workflow/ directory of your repository. You can call it build.yaml. Its initial content can look like this:

name: Build

on:
  push:
    branches:
      - master
      - test
      - dev

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-java@v1
        with:
 java-version: 11
      - uses: gradle/gradle-build-action@v2
        with:
          arguments: build

The syntax is easy to read too. It’s going to execute when someone pushes a new version of the master, test, or dev branch. It’s going to execute on an Ubuntu Linux machine. After checking out the latest version of the code, it’s going to execute the Gradle build command and make sure to use Java version 11.

Execution results are visible in the Actions tab of your repository.

STEP 2: DOCKER IMAGES

Most of the time you will also need to dockerize your application. As you know, Dockerfiles are how you define the way to build Docker images for your applications. Once your Dockerfile is in place, building and storing a Docker image on GitHub is not a problem.

In the YAML file below, I’m omitting most of the lines from step 1. This time, we want to focus on steps needed to dockerize the application:

name: Build

(...)

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - (...)
      - uses: tj-actions/branch-names@v5
        id: branch-names
      - uses: docker/build-push-action@v1
        with:
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
          registry: ghcr.io
          tags: ${{ steps.branch-names.outputs.current_branch }}
          dockerfile: Dockerfile

Before the workflow reaches the docker/build-push-action, we add tj-actions/branch-names. It’s a very useful utility that reads the name of the branch the workflow executes for and stores the name in a variable. This way, the branch name is available for docker/build-push-action to use as a Docker image tag. You will now be able to identify the latest versions built for each branch. Regarding username and password, don’t worry about it. We are using two values (github.actor and secrets.GITHUB_TOKEN) that are automatically available to any workflow that executes.

Once the steps are complete, your Docker image will appear in the Packages tab of your GitHub organization.

STEP 3: DEPLOYMENTS

Now that we have built the JAR and put it inside a Docker image, we are ready to ship it to a test environment. In this example, we’re going to assume that you are using Kubernetes. Just like we chose to in our newest project.

Let’s have a look at two last additions to the workflow:

name: Build and deploy

(...)

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - (...)
      - uses: tj-actions/branch-names@v5
        id: branch-names
      - (...)
      - uses: steebchen/kubectl@v2.0.0
        with:
          config: ${{ secrets.KUBE_CONFIG_DEV }}
          version: v1.21.0
         command: rollout restart deployment/<your-deployment-name> -n ${{ steps.branch-names.outputs.current_branch }}
      (...)

First, we need the output of tj-actions/branch-names action again, so I am not omitting it. This time, we are using it for the Kubernetes namespace value. In our newest project, we have dev and test namespaces in the Kubernetes cluster. Each holds all components of corresponding environments.

There’s one extra thing to do before this final version finishes with success. Download the kubeconfig file from your Kubernetes provider and put it in the Secrets section of your GitHub repository settings. Otherwise, the workflow has no way of accessing your cluster to roll out the change.

Congratulations! You now have a complete CD flow for your GitHub repository, and all it took was a single YAML file with around 50 lines.

EXTRAS: INPUTS AND SLACK

Some time has passed since completing the initial configs. We had these new ideas for automation and wanted to see how difficult they could be in Actions.

First, we wanted to be able to provide inputs when triggering a workflow manually. After a failed deployment, we wanted to be able to build and deploy a specific previous version:

name: Build and deploy

on:
  workflow_dispatch:
    inputs:
      ref:
        required: false
        description: Git ref to deploy
  (...)

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
        with:
         ref: ${{ github.event.inputs.ref }}
      (...)

This is also easy to do in Actions. Notice how in the workflow_dispatch section we are declaring a “ref” input. It will be possible to fill in in a pop-up window when we choose to trigger the workflow manually.

Second, we wanted to see Slack notifications when a deployment fails. This can prove useful if you have a rule that everyone who breaks the build buys pizza for the whole team. Definitely worth it! And easy to do thanks to the ravsamhq/notify-slack-action action:

name: Build and deploy

(...)

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - (...)
      - uses: ravsamhq/notify-slack-action@v1
        if: always()
        with:
          status: ${{ job.status }}
          notify_when: 'failure'
        env:
        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
      (...)

There’s a little extra setup in your organization’s Slack to do for this to work. But instructions in the action’s documentation are very easy to follow. Do it once, and never miss a failed build again.

CONCLUSION

When you choose to introduce a new technology to your project, there are risks to face. Usually, there is extra time involved for learning and making all the beginner’s mistakes.

Believe it or not, there’s very little of that when you start with GitHub Actions. Of course, you may come up with tricky scenarios. But this has not been the case in our new project. ‘Convention over configuration’ is a favourable approach for CI/CD workflows. Especially if you want to concentrate on solving business problems instead of fine-tuning the boilerplate.