GitHub Actions: powering up your existing repos with build capabilities

Sergio Freire
8 min readSep 24, 2019

--

I was lucky enough to enter the beta program for GitHub Actions.

Throughout this article, I’ll give you an overview of my findings of “GitHub Actions”, core concepts and some examples.

What is it and what can it be used for?

GitHub Actions are used to “build an automated software development lifecycle workflow” (GitHub).

GitHub Actions are somehow a response from GitHub to competitors (e.g. BitBucket, Jenkins, CircleCI) that enable powerful build processes and CI/CD by using a descriptive syntax managed as a YAML file stored in the repository, alongside with code.

Thus, you can use it to compile, lint, test, package, deploy your software. You can do whatever you want and fine-tune your build to your needs. GitHub provides an infrastructure where jobs will run. But more than that: it provides a marketplace where “actions” can be published and freely be referenced and reused in your workflows.

Thus, you and the community can create these building blocks called “actions” and make them available for others, so you can stay focused on what matters. Examples of these actions would be: setup a specific Java version for the whole build process, publish a Ruby gem or a maven artifact, lint Javascript code, analyze the code coverage, do a security scan, etc.

But even if you don’t want to, you can keep your actions private and use them internally within your repositories.

In a nutshell

Workflows are defined as YAML files inside .github/workflows directory. Multiple workflows can coexist and run simultaneously.

Add description

To see the runs for your workflows (i.e. workflow runs), you may access Actions tab in your repository browser.

Add description

Add description

Characteristics

  • Multiple workflows for the same repository are supported
  • Run jobs from one workflow sequentially or in parallel
  • Steps can be implemented as native commands or “actions”, which in turn may be NodeJS scripts or docker containers
  • Docker-based actions don’t need to have the respective images published (e.g. on Docker Hub); you may simply define the Dockerfile, in the repository, which will be used to build the image on-the-fly, whenever the action is called

Similar technologies

GitHub Actions have some similarities with Bitbucket Pipelines, Jenkins Pipelines and CircleCI way of configuring builds. However, in this article, we’ll not dive into that.

Key concepts

Virtual environment

A virtual machine (Windows, Ubuntu, macOS) with a set of tools preinstalled where workflows will run. More info here.

Workflow

An automated process defined at the repository level, triggered upon a specific event or scheduled; an example would be a full CI/CD pipeline

  • A workflow is composed of one or more jobs, which in turn are composed of steps implemented as commands or actions
  • Workflows run in the virtual environment

More info here.

Workflow run

A running instance of the workflow.

Context

The context (i.e. temporary information/data) of the workflow run, including virtual environment, jobs, steps.

More info here.

Job

A task made of steps.

  • Jobs may run on parallel or sequentially, depending on the previous job result
  • All steps in job are executed in the same virtual environment instance, sharing the same filesystem

Step

A specific command run directly/explicitly or a call to a pre-built runnable code, called action (i.e. a command or an action)

Action

A reusable and pre-built runnable code (e.g. checkout code, code coverage analysis, security scan), either as a Docker container or a javascript script.

  • Docker container actions are limited to Linux based virtual environments
  • Can be stored locally, in the working code repository, or can be stored in a different repository
  • Can be versioned and distributed in Github Marketplace, as long as they’re in a public repository
  • Can receive parameters, with values hardcoded or obtained from GitHub secrets

More info here.

How does it work?

A user working in the GitHub repository defines a workflow by creating a file under .github/workflows. Multiple workflows may be defined.

A workflow defines this high-level process you want to run, like a traditional build composed of compile, test, package targets, upon a specific event such a code commit.

Upon the specific trigger defined in the workflow, or due to a pre-defined schedule also definable in the workflow, it will run. This will create a “workflow run”. Jobs defined in the workflow can run sequentially or in parallel, depending on the configuration.

Each job uses a clean virtual environment, where all steps will be executed on. Information can be shared between steps, either by using the file system or by accessing information produced by the step (e.g. the standard output). Environment variables can also be changed between steps, although I’ve not tried it out.

A step can run directly on the virtual environment or as a Docker container having access to the shared file system.

Logs from the workflow run can be accessed in the “Actions” tab in the repository.

Examples

The following examples are quite basic. The idea is to understand core concepts and how to evolve from a simple example to some a bit more evolved yet still very basic.

The examples may be found in this public repository.

Example 1a: Hello, world!

In this example, we’ll create a very basic workflow that prints “Hello, world!” to the console/stdout. It uses the Linux, Ubuntu-based virtual environment. It runs the “echo” command directly on that environment.

This example is triggered upon a push/code commit. The file is stored in the repository as .github/workflows/example1a.yml. The name of the workflow file is irrelevant but it would make sense to have the same name as the value for “name” field defined within the workflow file.

name: example1a
on: [push]
jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Run a one-line script

run: echo Hello, world!

Example 2a: using environment variables as parameters

In this evolved example, we will pass the message as an environment variable named “MESSAGE” to the command we want to run (“echo” in our example).

name: example2a
on: [push]
jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Run a one-line script
env:
MESSAGE: Hello, world2!

run: echo $MESSAGE

Using environment variables is common in this kind of approaches.

Example 3a: Docker-based action and a public image, overriding entrypoint, and arguments

In this example, we will use a public Docker image to be called, after it is instantiated as a contained, from a step. We can call just mention the container and GitHub will use the entrypoint defined in the Docker image. However, you may also override it. You may also enforce the arguments given to the entrypoint defined executable.

name: example3aon: [push]jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Run a one-line script
uses: docker://alpine:latest
with:
entrypoint: /bin/echo

args: Hello, world3!

Add description

Example 3b: local Docker-based action, overriding entrypoint, and arguments

In this example will define an action locally, in our code repository, and mention it in our workflow. For that, we just need to provide the path.

name: example3b
on: [push]
jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- name: Run a one-line script
uses: ./.github/actions/blabla
with:
entrypoint: /bin/echo
args: Hello, world3b!

The action is implemented using Docker; you need to define the respective Dockerfile (see below) and GitHub will build the image whenever the workflow runs.

.github/actions/blabla/Dockerfile

FROM debian:9.5-slim
ENTRYPOINT ["/bin/echo"]

Example 3c: local Docker-based action, using input variables

There are several ways of passing arguments/additional information to steps. Input parameters are available for Docker-based actions.

This example uses a locally defined Docker-based action. The action receives two input variables: “name” and “message”.

name: example3con: [push]jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- name: Run a one-line script
uses: ./.github/actions/say-hi
with:
name: Sergio

message: How are you?

The action specification is done in the actual Dockerfile, which in this case resides in the local repository within .github/actions folder (i.e. .github/actions/say-hi/Dockerfile).

During the workflow run, the image is automatically built and a container is created.

FROM debian:9.5-slimADD entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

The Docker image uses an auxiliary script that implements the logic itself; in this case, it’s just an executable that calls “echo” and prints two environment variables. Note that the input variables defined in the workflow are “translated” to environment variables using the syntax “INPUT_<variable_in_uppercase>”.

#!/bin/sh -l
/bin/echo "$INPUT_NAME: $INPUT_MESSAGE"

Example 3d: passing a secret as an environment variable

In this example, we start by defining a secret variable in the repository’ settings.

Add description

You may then reference it using contexts (e.g. ${{ secrets.SECRET_MESSAGE }}).

name: example3don: [push]jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Run a one-line script
env:
MESSAGE: ${{ secrets.SECRET_MESSAGE }}
run: echo $MESSAGE

Note that if you try to print a secret to the logs, GitHub automatically redacts it. More info on using secrets here.

Real-life like example: Build a java/maven based project (compile, test)

In this example, we’re building a Java, maven based project.

We’ll use the “actions/checkout” action to checkout the code from our repository to the virtual environment. This action is one of the “standard” actions provided by GitHub; full list here. The same for “actions/setup-java”.

name: example4aon: [push]jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1
- name: Set up Java
uses: actions/setup-java@v1
with:
java-version: '1.8+
- name: Build with Maven
run: mvn clean compile test --file java-junit-calc/pom.xml

Sometimes you may want to run your build against different target configurations (e.g. JDK versions). In that case, you may build a “matrix” strategy, where you define a variable with all the JDK versions. Then, you may reference it in the action where you set up Java.

The “build” job runs in parallel for all the different values of “java” variable; if one build fails, it does not affect other builds running.

name: example4bon: [push]jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '1.8', '9.0.x', '12.0.2' ]
fail-fast: false

steps:
- uses: actions/checkout@v1
- name: Set up Java
uses: actions/setup-java@v1
with:
java-version: ${{ matrix.java }}
- name: Build with Maven
run: mvn clean compile test --file java-junit-calc/pom.xml

Challenges and doubts

One challenge is validating actions locally, reproducing the virtual environment. I came across with Nektos’ act tool but unfortunately, it does not support the new YAML file syntax for workflows (as of September 2019).

A doubt that I had was: “When to use input variables or environment based variables?”. It seems to be irrelevant as input variables are also translated to environment variables (in uppercase, with their name preceded by “INPUT_”).

Beyond this article

There’s so much more to cover related to GitHub Actions (e.g. how to share data between steps). I’ll revisit this topic as I further explore it and share additional insights about it. Stay tuned :)

Further reading

--

--

Sergio Freire

An idiot, a *creative* thinker. Technology consultant and advisor. Addict of *innovation*. Beyond evangelist, a hands-on, new technologies pusher.