DEV Community

Cover image for The Inversion of GitHub Actions, or Tom Sawyer in Your CI Pipeline
Roman Balashevich for Otomato

Posted on

The Inversion of GitHub Actions, or Tom Sawyer in Your CI Pipeline

The famous American writer Mark Twain, in his story about how Tom Sawyer made the boys paint the fence, showed the public how inversion of action works. Or the elaboration of negation, if you like to call it that.

But today we are interested in how to “make the machines under our control” think differently, because automation involves programming the behavior of machines, right? And scripts are all about the same thing. Let's see it in this article.
There's always a way to solve the problem

What grandpa bash dictates to us

In our beloved bash, set +e is basically the default: If you get an error in a bash script, it normally barfs out an error to the system (this is called an exit code) but the script will continue running. But if you want errors to cause the script immediately to exit, then you can put set -e on a line to force your script to exit on errors from that point on.

Talking about bash shell, which is widely used in Linux, there is a special built-in command, set. It may force your script to exit immediately if a pipeline, which may consist of a single simple command, a list, or a compound command returns a non-zero status (saying in more simple words, fails).

However, common-sense dictates that using set -e should be carefully planned… you don’t want to stop on the first error and miss the second problem, which could be something much worse. A real life example may be as following below:

set -e
The task: dodge an approaching car. The flow: steering wheel error — abandon program, including the routine to slam on the brakes — you harm yourself.

Putting Theory in Action. Paint the fence with fun.

(And with a full canister, labeled “GHA workflows”).

Okay, we figured out the theory. And what about in practice? As you may know, GitHub Actions allows you to automate any software workflow by including a simple YAML file in your git repository spelling out what you want to execute and when. And in practice, we can carry out what is described, using set command, too. Let’s look at this example:

on:
  workflow_dispatch:

jobs:
  catch_code:
    # The type of runner that the job will run on
    runs-on: ubuntu-latest

    steps:
    - name: Catch exit code
      id: catch
      run: |
          set +e
          echo Going to return exit code 1
          ls /home/nonexistent
          if [ $? != 0 ]; then
            echo We failed
          else
            echo We succeeded
          fi
          echo And now we continue
Enter fullscreen mode Exit fullscreen mode

This way we got the approach for controlling job behavior to prevent it from failing on error code 1 when indeed we need a detailed command execution parsing — and to continue.

Moreover, an if: failure() status check function may be used in GHA to make sure that the final step always runs after any earlier step fails.

Jobs are the higher level collections for steps in GitHub Actions workflows. Jobs may be run in parallel by default, and individual jobs do not share the same workspace or virtual machine, so you cannot automatically share state between jobs (compared to “steps” which all run in the same environment as their siblings). Each step can be an action or a shell script.

I. Where is the last stop?

As we managed to learn already, you can suppress the error so that things keep running, by using the bash set command. Specifically, set +e before the potentially failing-but-not-actually-a-problem line(s), and then optionally set -e to turn immediate-exit back on again prior to calling instructions that should cause your workflow to fail when they end in a non-zero exit code.

- name: List diff
  run: git diff --quiet archives/content/

- name: Commit changes
  run: |
    git config --local user.email "github-action@example.com"
    git config --local user.name "CI/CD"
    set +e
    git add -A
    git commit -m "..."
    set -e

- name: Push changes
  uses: ad-m/github-push-action@master
  with:
    github_token: ${{ secrets.GITHUB_TOKEN }}  
Enter fullscreen mode Exit fullscreen mode

II. Forcing workflow continuation

Another GHA feature which has been introduced in spring 2020 allows you even use expressions in the continue-on-error property for a job. This can be useful when you want to allow some jobs to fail based on runtime values, such as a matrix.

A matrix allows you to create multiple jobs by performing variable substitution in a single job definition. For example, you can use a matrix to create jobs for more than one supported version of a programming language, operating system, or tool.

jobs:
  build:
    strategy:
      matrix:
        os: ubuntu-latest, macos-latest, windows-latest
        node: [10, 12, 13]
      include:
        node: 13
        continue-on-error: true
    continue-on-error: ${{ matrix.continue-on-error }}
Enter fullscreen mode Exit fullscreen mode

III. Bread crumbs. Contexts

A very good thing to know about GitHub Actions is that upon running any script, you have some neat additional context to work with. Actions provide a robust runtime environment, giving you a lot of information about your repository, the triggered event, and the actors (systems or people) behind it.

GitHub Actions includes a collection of variables called contexts. You can use most contexts at any point in your workflow, including when default environment variables would be unavailable. For example, you can use contexts with expressions to perform initial processing before the job is routed to a runner for execution; this allows you to use a context with the conditional if keyword to determine whether a step should run. Once the job is running, you can also retrieve context variables from the runner that is executing the job, such as runner.os.

The following example demonstrates how these different types of environment variables can be used together in a job:

name: CI
on: push
jobs:
  prod-check:
    if: ${{ github.ref == 'refs/heads/main' }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Deploying to production server on branch $GITHUB_REF"
Enter fullscreen mode Exit fullscreen mode

In this example, the if statement checks the github.ref context to determine the current branch name; if the name is refs/heads/main, then the subsequent steps are executed. The if check is processed by GitHub Actions, and the job is only sent to the runner if the result is true. Once the job is sent to the runner, the step is executed and refers to the $GITHUB_REF environment variable from the runner.

IV. More flexibility. Conditionals

A conditional can make all the difference. GitHub Actions supports conditionals, which use the if keyword to determine if a step should run in a given workflow. You can use this to build upon dependencies so that if a dependent job fails, the workflow can continue running. These can use certain built-in functions for data operations.

One useful way to take advantage of this data is to use it to run workflow steps conditionally.

For example, you might want to check the name of the repository your workflow is running in before you execute a step. This is helpful if you're working on an open source project – since people who fork your repository have tokens with different permissions, you can skip the Publish step for forks.

This allows forked repositories to still perform continuous integration builds, and ensures that the workflow succeeds when builds run and tests pass, and don't fail because of permissions problems on a Publish step.

You can set up a conditional that ensures you're on the correct repository, and running in a CI build (from a push event).

name: CI

on:
  push:
    branches:
      - master
  pull_request:
    branches:
      - master

jobs:
  build:
    name: Publish
    runs-on: ubuntu-latest
    steps:
      - name: Build
        run: |
          make && make test
      - name: Publish Documentation
        run: |
          scripts/publish.sh
        env:
          PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }}
        if: github.repository == 'ethomson/project' && github.event_name == 'push'
Enter fullscreen mode Exit fullscreen mode

As a conclusion

GitHub Actions is designed to bring platform-native automation and CI/CD capabilities directly into the GitHub flow to simplify the developer experience. It can also be used to build out more advanced, custom workflows for anything from triggering an alarm to orchestrating complex security test automations.

If you’re looking to create advanced workflows, here are some sources of wisdom. You can also learn more about managing complex workflows with GitHub Actions in GitHub Docs, or watch Brian Douglas' on-demand session at GitHub Universe.

Returning to the lessons of literature. Mark Twain was not wrong about psychology. There are always possible ways to influence the situation that do not always lie on the surface. Influence in your favor! The toolkit is available today.

The author wishes to thank Brian Douglas, Georgy Glezer, Dan Lester and Edward Thomson for theirs blogs.

Discussion (0)