POST

Actions are a welcome addition to the GitHub service. They add a free, built-in CI/CD capability similar to GitLab-CI, Travis and others.

What we’ll see in this blog post is a simple pattern to build a container image and push it to Docker Hub. What happens is based on whether we’re on a branch, responding to a pull-request, or responding to changes on main.

Basic anatomy of an Action

In its simplest form an Action: -

  • Is defined in a workflow file and committed along with your source code
  • Workflow file (or files, as you can have many) is a .yml (or .yaml) file in the directory .githib/workflows.
  • Has a name
  • Consist of steps in jobs
  • Jobs run on virtual machines
  • Jobs run based on one or more trigger conditions
  • Can use other actions

There’s a lot more to it. You can refer to the official syntax documentation, a comprehensive reference. Importantly, as we’ll see here you can build some useful actions with just a few lines of YAML.

What we’ll be doing here is writing a pair of actions that: -

  1. Build an image (but don’t push it) if we’re on a branch or responding to a pull request
  2. Build and push any change to main as a container image tagged :latest. This will require Docker credentials

To create the actions we: -

  • Create organisation secrets to provide a registry username and password
  • Create a build.yaml action in .githib/workflows
  • Create a build-latest.yaml action in .githib/workflows
  • Commit our changes

Special steps in pull requests

We often need to handle actions during a Pull Request - usually this is just to ensure that any testing, linting or building works when submitted from a fork of a repository.

We can’t usually push (publish) any results at this time as this requires authentication with the container registry. As these secrets are injected into the action through repository or organisation secrets, which are not available during a Pull Request, we therefore cannot login to a registry during a pull request. Hence, in pull requests we tend to limit ourselves to linting and testing.

Organisation secrets

We need to provide a username and password (or token) for our chosen container registry (Docker Hub). We can’t put these in the Action directly but secrets can be injected in the form of environment variables (secrets).

As our secrets are used across a number of similar repositories we can place the username and password values into secrets that are part of our GitHub’s organisation. that way we only need to define them once but they can be used on all our repositories.

To add an organisation secret…

  • Navigate to your repository Organisation
  • Navigate to Settings
  • Select Secrets and click New organisation secret
  • Create DOCKERHUB_USERNAME and DOCKERHUB_TOKEN secrets

Action workflow (build)

Now let’s create our action.

In the repository, create an action file called .github/workflows/build.yaml. The build action will simply build the image on any branch or pull request.

All actions have a name, it’s free-form text. We tend to stick to space-separated lower-case as a style. So start your build.yaml with a name: -

---
name: build

Now define the triggers that will start the action. Here we want to trigger on pushes (except those to the main branch) and pull requests: -

on:
  push:
    branches-ignore:
    - 'main'
  pull_request:
    branches:
    - 'main'

You might need to use master rather than main for old-style repositories. All newly created repositories now use main as the main branch.

Now we define the steps for our action in a job, that runs on ubuntu.

Here we simply rely on a library (built-in) action to build the image. An action written by the community.

We could write the individual docker build steps that are required to build a container image, but it’s already been done (in a much more powerful way), so we just use another action.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Build
      uses: docker/build-push-action@v2

The docker/build-push-action we use doesn’t need anything else. It assumes the Dockerfile is in the root of the repository and simply builds our container based on that fact.

We can use a different Dockerfile, name the image, provide tags, push to a registry and build for multiple architectures (ARM for example) using this one action. For our example, we don’t need any of that, so the build command is just one short and simple line in the action.

That’s it - our container will be built on any non-main branch or pull-request.

Our final workflow file looks like this: -

---
name: build

on:
  push:
    branches-ignore:
    - 'main'
  pull_request:
    branches:
    - 'main'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - name: Build
      uses: docker/build-push-action@v2

Action workflow (build and push)

Now let’s write a separate action to build and push the container image, one that will run on the main branch.

In the repository, create an action file .github/workflows/build-latest.yaml. The build and push action will look like this: -

---
name: build latest

on:
  push:
    branches:
    - 'main'

jobs:
  build-and-push-latest:
    runs-on: ubuntu-latest
    steps:
    - name: Login to DockerHub
      uses: docker/login-action@v1
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
    - name: Build and push
      uses: docker/build-push-action@v2
      with:
        push: true
        tags: my-org/my-image:latest

You’ll see that we use another action (docker/login-action) to simplify logging into a container registry. We provide this action with the secrets we created earlier to allow us to safely login to Docker Hub.

We also provide the docker/build-push-action with an image name and tag for use when pushing to Docker Hub.

This action will run on any change that’s made to the main branch.

In future posts we’ll advance our experience with Actions and look at: -

  • Creating tagged and stable images
  • Writing our own re-usable actions
latest posts
by year
by category