Converting TravisCI Jobs to GitHub Actions

Submitted by david.stinemetze on Mon, 01/07/2019 @ 11:19am

I recently was granted early beta access to GitHub Actions. I decided to play around with it to see how it worked and wanted to take the opportunity to share my findings.

What is GitHub Actions?

GitHub Actions is workflow automation built directly inside GitHub. This means a lot of CI functionality, commonly handled by tools like TravisCI and Jenkins, can now be managed using GitHub directly. Actions use docker to execute relevant tasks inside of containers. You can create multiple workflows that do different things for different contexts:

  • push - Triggered when branches are pushed, or pull requests are opened, etc...

    This is mostly what I experimented with and what the rest of this post will be about.
     
  • issue - Triggered when issues are created, assigned, deleted, etc...
    Note: This is only available for private repos during the limited beta.
     
  • release - Triggered when releases are created, deleted, etc...
    Note: This is only available for private repos during the limited beta.

    For example, when a new software release is pushed, you can automatically trigger deployments. There are even predefined actions for integrating with cloud providers like AWS, Google Cloud and Azure, although I haven't actually tested any of those.

It's worth pointing that GitHub Actions is still in a limited beta, meaning things may continue to change and (hopefully) improve before it is officially launched to the public.

Getting Started

The GitHub Actions developer documentation is a great starting point. It provides you with some of the basic information you need to get started and a simple example.

I wanted a very simple use case for testing purposes, so I decided to make use of the drupal-patch-checker composer plugin I built previously for my Patching Production Drupal Sites With hook_update_N() is Risky post. It's a small PHP package that currently uses TravisCI to perform simple code-style analysis and unit testing. I wanted to see if I could perform those same actions using GitHub Actions instead.

Current TravisCI Configuration

TravisCI - drupal-patch-checker

This my current TravisCI configuration:

language: php
php:
  - 7.1
  - 7.2
env:
  - CMD="test"
  - CMD="phpcs"
install:
  - "composer install"
script:
  - "composer $CMD"

If you're not familiar with TravisCI, essentially this runs composer test and composer phpcs using both PHP 7.1 and 7.2.

In my composer.json file, those scripts are defined as:

    "scripts": {
        "test": [
            "vendor/bin/phpunit --testsuite=DrupalPatchChecker"
        ],
        "phpcs": [
            "vendor/bin/phpcs --standard=vendor/drupal/coder/coder_sniffer/Drupal/ruleset.xml src/ tests/"
        ]
    }

Creating Workflows & Actions

Workflows can be created either via code or by using a visual editor. All workflows live in a .github folder located inside the main folder of the repo. A workflow consists of a series of actions. You can use either external actions or define your own internally. Actions can happen either sequentially or in parallel. 

According to the Creating a new GitHub Action documentation, all actions should consist of a Dockerfile and README.md. Additionally, it may be beneficial to include an entrypoint.sh file which is a shell script that can be configured to define the behaviors for an action.

My workflow ended up looking like this:

GitHub Actions Workflow

workflow "Main Workflow" {
  on = "push"
  resolves = [
    "Code Style Analysis",
    "Unit Tests",
  ]
}

action "Build" {
  uses = "./actions/build"
}

action "Unit Tests" {
  uses = "./actions/test"
  needs = ["Build"]
}

action "Code Style Analysis" {
  uses = "./actions/phpcs"
  needs = ["Build"]
}

Comparing the screenshot and the code, we can paint a pretty clear picture of what's actually happening. Our workflow ultimately wants to run the Code Style Analysis and Unit Tests actions; however, both of those actions need the Build action to complete first.

Originally, I tried creating just two actions running in parallel similar to how things ran in TravisCI. This meant both actions first ran composer install followed by their respective commands. What I didn't realize is that despite the commands running in separate docker containers, they share a common codebase. As a result, having two competing containers trying to execute composer install on the same code at the same time, created a race condition that caused one of those containers to always fail. 

I realized that I only needed to run composer install once anyway, so I created a preliminary Build action, that would then trigger the other two actions. 

Build Action

Based on the standard composer:latest docker image, this action simply installs composer and then runs composer install

actions/build/Dockerfile:

FROM composer:latest

LABEL "com.github.actions.name"="build"
LABEL "com.github.actions.description"="Builds the code"
LABEL "com.github.actions.icon"="terminal"
LABEL "com.github.actions.color"="black"

LABEL "repository"="https://github.com/WidgetsBurritos/drupal-patch-checker"
LABEL "homepage"="https://www.widgetsandburritos.com/posts/2018-12-07/patching-production-drupal-sites-hook-update-n-risky"
LABEL "maintainer"="David Stinemetze <[email protected]>"

ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

actions/build/entrypoint.sh:

#!/bin/sh -l

sh -c "echo 'Build the project'"
composer install

Code Style Analysis Action

Since this action uses a separate docker container from the Build step, I had a choice here. I could stick with what we did in the previous step and make this dependent on composer:latest as well, or I could take composer out of the picture altogether. Composer is really only needed to pull in the necessary packages and build the project.

With all of that being done during the previous Build action, I opted to make this container dependent on php:7.2 instead as it is a slightly lighter image. Since composer is not accessible here, I can't run composer phpcs, but that's just a wrapper around some other command anyway, so I just used that command directly.

actions/phpcs/Dockerfile:

FROM php:7.2

LABEL "com.github.actions.name"="phpcs"
LABEL "com.github.actions.description"="Performs code-style analysis"
LABEL "com.github.actions.icon"="code"
LABEL "com.github.actions.color"="blue"

LABEL "repository"="https://github.com/WidgetsBurritos/drupal-patch-checker"
LABEL "homepage"="https://www.widgetsandburritos.com/posts/2018-12-07/patching-production-drupal-sites-hook-update-n-risky"
LABEL "maintainer"="David Stinemetze <[email protected]>"

ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

actions/phpcs/entrypoint.sh:

#!/bin/sh -l

sh -c "echo 'Running code style analysis'"
vendor/bin/phpcs --standard=vendor/drupal/coder/coder_sniffer/Drupal/ruleset.xml src/ tests/

Unit Tests Action

Much like the code style analysis action, this action only needs PHP, not composer. 

actions/test/Dockerfile:

FROM php:7.2

LABEL "com.github.actions.name"="test"
LABEL "com.github.actions.description"="Performs phpunit tests"
LABEL "com.github.actions.icon"="edit"
LABEL "com.github.actions.color"="red"

LABEL "repository"="https://github.com/WidgetsBurritos/drupal-patch-checker"
LABEL "homepage"="https://www.widgetsandburritos.com/posts/2018-12-07/patching-production-drupal-sites-hook-update-n-risky"
LABEL "maintainer"="David Stinemetze <[email protected]>"

ADD entrypoint.sh /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]

actions/test/entrypoint.sh:

#!/bin/sh -l

sh -c "echo 'Running unit tests'"
vendor/bin/phpunit --testsuite=DrupalPatchChecker

PHP Version Discaimer

In TravisCI, I was running both PHP 7.1 and PHP 7.2 compatibility tests where this isn't quite doing that.

Using the composer:latest image on the build step technically uses PHP 7.3 for running composer install, but both of the followup actions use PHP 7.2. So this isn't 100% feature-parity here.

If I wanted to, I could spend some time clearly defining those docker files and just creating separate actions for PHP 7.1 and PHP 7.2 to get parity. I might do something like that at a later time. For now I just wanted to focus on the actual workflow functionality and not get too hung up on the implementation details.

Running the Actions

After a bit of trial-and-error I got everything up and running:

GitHub Actions Tab

As you can see in the screenshot, the build step executed first, and then the code style analysis and unit tests ran at the same time. Everything succeeded.  That is great, but to really see this in action we need to make some things fail. 

Making Code-Style Analysis and Unit Tests Fail

I pushed up a branch that should cause both code-style analysis and unit tests to fail, but that's not exactly what happened:

GitHub Actions Tab - Test/Style failures

Instead, the Code Style Analysis and Unit Tests actions started running at the same time, but as soon as one of them failed the other one was cancelled. This is something I wonder if is configurable. Since code style analysis and unit tests are completely unrelated to one another, if both fail, I'd like to be able to see why both failed and potentially fix them at the same time, rather than fix one, push up changes and then fix the other check.

I could consolidate both commands into a single shell script, but that's not really the behavior that I'm looking for here as I want these to be separate checks. I suppose I could create two separate workflows instead of a single workflow with multiple actions, but hopefully there's a way to solve this at the action-level. I've reached out to GitHub support for more assistance here and will update this post if I get a response. 

Taking things a step further, I opened a pull request for this branch

Unit Test and Code Style Failures

As you can see, there are five checks that ran on this PR. Two of them were from my existing TravisCI integration (one for my pull request and the other for the branch since it's on the same remote), but we also see three new checks that all have the GitHub logo next to them, each one relating to a respective action.

Making the Build Step Fail

I decided to open one more branch and corresponding pull request. This time around I made a change that causes the build step to fail. This behaves similarly to the previous scenario with one exception. Since the build action was a prerequisite for the other two actions, they don't run at all. 

GitHub Actions Tab - Build failure

Unit Test and Code Style Failures

It is worth noting that just because the jobs do not run does not mean the containers aren't created. It appears to creates all the necessary containers when the initial step begins. It just goes unused until it's ready. We can determine this by looking at the log for one of the cancelled actions:

### CANCELLED Unit Tests 05:50:37Z

Already have image (with digest): gcr.io/cloud-builders/docker
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855: Pulling from gct-12--2526filds2absw7fqldw5n/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/6a4873f201e251a68f899b19841308d3ab0bd8ba31ea367f3580c02378794b2c
177e7ef0df69: Already exists
9bf89f2eda24: Already exists
350207dcf1b7: Already exists
a8a33d96b4e7: Already exists
6d6cf8b28f60: Pulling fs layer
7a2d1180d62e: Pulling fs layer
5867e72fdab6: Pulling fs layer
bf5377ba85a2: Pulling fs layer
5c86df2dcf38: Pulling fs layer
576b459e3a3f: Pulling fs layer
5c86df2dcf38: Waiting
576b459e3a3f: Waiting
bf5377ba85a2: Waiting
7a2d1180d62e: Verifying Checksum
7a2d1180d62e: Download complete
bf5377ba85a2: Verifying Checksum
bf5377ba85a2: Download complete
5c86df2dcf38: Verifying Checksum
5c86df2dcf38: Download complete
576b459e3a3f: Verifying Checksum
576b459e3a3f: Download complete
6d6cf8b28f60: Verifying Checksum
6d6cf8b28f60: Download complete
5867e72fdab6: Verifying Checksum
5867e72fdab6: Download complete
6d6cf8b28f60: Pull complete
7a2d1180d62e: Pull complete
5867e72fdab6: Pull complete
bf5377ba85a2: Pull complete
5c86df2dcf38: Pull complete
576b459e3a3f: Pull complete
Digest: sha256:fe5181cfb33305a24090522ac7dc7d958d3c4d300c29f33ebe69200dff653ad7
Status: Downloaded newer image for gcr.io/gct-12--2526filds2absw7fqldw5n/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/6a4873f201e251a68f899b19841308d3ab0bd8ba31ea367f3580c02378794b2c:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Already have image (with digest): gcr.io/cloud-builders/docker
Sending build context to Docker daemon  4.608kB
Step 1/10 : FROM php:7.2
7.2: Pulling from library/php
177e7ef0df69: Already exists
9bf89f2eda24: Already exists
350207dcf1b7: Already exists
a8a33d96b4e7: Already exists
6d6cf8b28f60: Already exists
7a2d1180d62e: Already exists
5867e72fdab6: Already exists
bf5377ba85a2: Already exists
5c86df2dcf38: Already exists
Digest: sha256:fff3205a549103739bf709e787cb0582c9f52eb26328cf48d8c140ae3f71f902
Status: Downloaded newer image for php:7.2
 ---> a1c0790840ba
Step 2/10 : LABEL "com.github.actions.name"="test"
 ---> Using cache
 ---> e5c5793a40ab
Step 3/10 : LABEL "com.github.actions.description"="Performs phpunit tests"
 ---> Using cache
 ---> aebdc1843c3e
Step 4/10 : LABEL "com.github.actions.icon"="edit"
 ---> Using cache
 ---> ded1c75c2e2c
Step 5/10 : LABEL "com.github.actions.color"="red"
 ---> Using cache
 ---> 31602a051740
Step 6/10 : LABEL "repository"="https://github.com/WidgetsBurritos/drupal-patch-checker"
 ---> Using cache
 ---> 1fa0d22992b9
Step 7/10 : LABEL "homepage"="https://www.widgetsandburritos.com/posts/2018-12-07/patching-production-drupal-sites-hook-update-n-risky"
 ---> Using cache
 ---> dae12ac1a9db
Step 8/10 : LABEL "maintainer"="David Stinemetze <[email protected]>"
 ---> Using cache
 ---> 3b358f2917be
Step 9/10 : ADD entrypoint.sh /entrypoint.sh
 ---> Using cache
 ---> 3915b9d87ddc
Step 10/10 : ENTRYPOINT ["/entrypoint.sh"]
 ---> Using cache
 ---> f880b1204194
Successfully built f880b1204194
Successfully tagged gcr.io/gct-12--2526filds2absw7fqldw5n/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/6a4873f201e251a68f899b19841308d3ab0bd8ba31ea367f3580c02378794b2c:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
Action `Unit Tests' cancelled because action `Build' failed

From this we can see that the container was in fact built and tagged, but the action was cancelled since the build action failed. 

I imagine this was done for performance reasons. Docker images can sometimes take a while to download and build, so you don't want to have to sit and wait for them to get built between steps, especially if your workflow is deeper that 2 levels. 

Final Thoughts

I've only scratched the surface of what you can do with GitHub Actions. I'm very interested to see how it would work with deployment pipelines. As it stands now, I'm not rushing to replace any of my more critical CI systems with it quite yet. As it's still in beta, I wouldn't necessarily recommend relying on it for any critical production environments, but I definitely thing it's day will come soon.

Have you played with GitHub Actions yet? Any initial thoughts about it? Leave your feedback below.