Getting started with GitHub Actions and workflows
Tags: node, javascript, devops, github
Published: 2022-04-03 12:55:37

---
title: Getting started with GitHub Actions and workflows
published: true
tags: node, javascript, devops, github
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fvxchp1cal66hxg0w7xf.png
---
[GitHub Actions](https://github.com/features/actions) are a great way to automate your own software development cycle. GitHub Actions are free of charge for public repositories and provide you with a whole CI/CD platform. It allows you to automate all parts of your software supply chain and run it in virtual environments or even your own environment using [self-hosted runners](https://docs.github.com/en/actions/hosting-your-own-runners/about-self-hosted-runners).

Much of what used to be done with a Jenkins job can now be done with GitHub Actions. In this article, I will give you a quick start in GitHub Actions and explain what **actions**, **workflows**, **events**, **jobs** and **steps** are. As an example we take a JavaScript application for which we set up a test automation.

<!--more-->

## What are GitHub Actions?

GitHub Actions are reusable scripts that can be used on GitHub's platform for **continuous integration** and **continuous delivery** (CI/CD). You can write your own actions using JavaScript (and other languages) or use published actions from the [GitHub Marketplace](https://github.com/marketplace?type=actions). 

There are already actions for various tasks like sending a message to a Slack channel ([slack-send](https://github.com/marketplace/actions/slack-send)), uploading code coverage reports ([codecov](https://github.com/marketplace/actions/codecov)) or deploying code to the Google Cloud ([setup-gcloud](https://github.com/marketplace/actions/set-up-gcloud-cloud-sdk-environment)). In this tutorial, we will use existing GitHub Actions and wire them together in a so-called "workflow".

## What are workflows?

A workflow is a description for your CI/CD pipeline on GitHub Actions. A workflow always runs one or more **jobs** and each job consists of **steps** which can be calls to GitHub Actions or regular shell commands. A workflow is triggered by an **event** (e.g. a commit in your branch) and runs on a virtual environment on GitHub (called "hosted runner") or your own environment (called "self-hosted runner").

## Test Automation with GitHub Actions

To ensure that pull requests are compatible with your code, you can setup a GitHub workflow to run a test automation pipeline. I will show you how to do this by using a JavaScript demo project for which we will run `npm test` when new code comes in.

### Setting up a workflow

Setting up a workflow is done by creating a YAML file inside of the `.github/workflows` directory of your repository on GitHub. We will save our test automation in `test.yml`:

**.github/workflows/test.yml**

```yml
# Name of our workflow
name: 'Test'

# Events that will trigger our workflow
on: [ 'pull_request', 'push' ]

# List of custom jobs
jobs:
  # Job is called "test"
  test:
    # Using a "label" to assign job to a specific hosted runner
    runs-on: ubuntu-latest
    steps:
      # Checks-out our repository under "$GITHUB_WORKSPACE", so our job can access it
      - name: 'Checkout repository'
        uses: actions/checkout@v3

      # Runs commands using the runners shell
      - name: 'Run tests'
        run: npm ci && npm test
```

### Specify Node.js version

GitHub provides [hosted runners](https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners) which can run your workflow in different [virtual environments](https://github.com/actions/virtual-environments). The "ubuntu-latest" environment already contains a recent version of Node.js which is ideal for testing JavaScript applications.

You can also use the [setup-node action](https://github.com/actions/setup-node) to configure any Node.js version you like to use:

```yml
name: 'Test'

on: [ 'pull_request', 'push' ]

jobs:
  test:
    # Using a build matrix to route workflow to hosted runner(s)
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ 'ubuntu-latest' ]
        node-version: [ '16.x' ]
    steps:
      - name: 'Checkout repository'
        uses: actions/checkout@v3
      
      # Uses specific version of Node.js
      - name: 'Use Node.js v${{ matrix.node-version }}'
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}
      
      - name: 'Run tests'
        run: npm ci && npm test
```

### Define workflow triggers

Currently our workflow is executed on every `git push` event and every event in a Pull Request. Pushing commits in a PR triggers our action twice because it is a push event and an event in our PR. To prevent this, we can restrict the events that trigger our workflow. We will limit the push events to the "main" branch, which is useful when we squash and merge a PR into our "main" branch:

```yml
name: 'Test'

on:
  pull_request:
  # Limit push events to "main" branch
  push:
    branches: [ 'main' ]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ 'ubuntu-latest' ]
        node-version: [ '16.x' ]
    steps:
      - name: 'Checkout repository'
        uses: actions/checkout@v3

      - name: 'Use Node.js v${{ matrix.node-version }}'
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: 'Run tests'
        run: npm ci && npm test
```

**Note:** Simply leave the value for `pull_request` empty to match any branch name.

### Run workflow manually with `workflow_dispatch`

We can also define a `workflow_dispatch` trigger which will allow us to run a workflow manually from the "Actions" tab of our repository:


```yml
name: 'Test'

on:
  pull_request:
  push:
    branches: [ 'main' ]
  # The "workflow_dispatch" event gives us a button in GitHub's "Action" UI
  workflow_dispatch:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ 'ubuntu-latest' ]
        node-version: [ '16.x' ]
    steps:
      - name: 'Checkout repository'
        uses: actions/checkout@v3

      - name: 'Use Node.js v${{ matrix.node-version }}'
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: 'Run tests'
        run: npm ci && npm test
```

**Screenshot:**

![Workflow dispatch in action](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tas45tcdz8v3ogomr2zk.png) 

### Run multiline shell commands

When working with TypeScript, it is advisable to check the validity of your types before running tests. This way errors can be catched even before setting up the test runner. We will accomplish this by running `tsc --noEmit` just before executing our test script. In order to have a better overview of our commands, we will replace the `&&` link with a multiline command using the pipe (`|`):

```yml
name: 'Test'

on:
  pull_request:
  push:
    branches: [ 'main' ]
  workflow_dispatch:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        os: [ 'ubuntu-latest' ]
        node-version: [ '16.x' ]
    steps:      
      - name: 'Checkout repository'
        uses: actions/checkout@v3

      - name: 'Use Node.js v${{ matrix.node-version }}'
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      # Runs multiple commands using the "|" operator
      - name: 'Run tests'
        run: |
          npm ci
          npx tsc --noEmit
          npm test
```

### Skip workflow execution

We can prevent our full test setup from running when adding a specific text (like `[skip ci]` or `[ci skip]`) in our commit message: 

```yml
name: 'Test'

on:
  pull_request:
  push:
    branches: [ 'main' ]
  workflow_dispatch:

jobs:
  test:
    runs-on: ${{ matrix.os }}
	# Condition to run the job using GitHub's event API
    if: |
      contains(github.event.commits[0].message, '[skip ci]') == false &&
      contains(github.event.commits[0].message, '[ci skip]') == false
    strategy:
      matrix:
        os: [ 'ubuntu-latest' ]
        node-version: [ '16.x' ]
    steps:      
      - name: 'Checkout repository'
        uses: actions/checkout@v3

      - name: 'Use Node.js v${{ matrix.node-version }}'
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: 'Run tests'
        run: |
          npm ci
          npx tsc --noEmit
          npm test
```

**Note:** By default [GitHub skips checks](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/about-status-checks#skipping-and-requesting-checks-for-individual-commits) for commits that have two empty lines followed by `skip-checks: true` within the commit message before the closing quotation:

```
git commit -m "Some commit message
>
>
skip-checks: true"
```

### Using expressions in workflows

The workflow syntax for GitHub Actions allows us to use [expressions](https://docs.github.com/en/actions/learn-github-actions/expressions#about-expressions). There is a set of built-in functions, like `success()` and `failure()`, that can be used in expressions and are very handy to check the status of your workflow. We will use `failure()` to send a message to our Slack channel whenever our tests fail:

```yml
name: 'Test'

on:
  pull_request:
  push:
    branches: [ 'main' ]
  workflow_dispatch:

jobs:
  test:
    runs-on: ${{ matrix.os }}
    if: |
      contains(github.event.commits[0].message, '[skip ci]') == false &&
      contains(github.event.commits[0].message, '[ci skip]') == false
    strategy:
      matrix:
        os: [ 'ubuntu-latest' ]
        node-version: [ '16.x' ]
    steps:
      - name: 'Checkout repository'
        uses: actions/checkout@v3

      - name: 'Use Node.js v${{ matrix.node-version }}'
        uses: actions/setup-node@v3
        with:
          node-version: ${{ matrix.node-version }}

      - name: 'Run tests'
        run: |
          npm ci
          npx tsc --noEmit
          npm test

      - name: 'Post error notification to Slack channel'
        uses: slackapi/slack-github-action@v1.18.0
        # Use built-in function in expression
        if: ${{ failure() }}
        with:
          channel-id: my-channel
          slack-message: 'Test run <${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}|${{ github.run_id }}> failed.'
        env:
          SLACK_BOT_TOKEN: ${{ secrets.MY_SLACK_BOT_TOKEN }}
```

**Note:** To make use of the Slack Action, you have to [create a Slack App](https://api.slack.com/apps) for your Slack workspace with an OAuth scope of `chat.write`. Afterwards, you have to make your "Bot User OAuth Token" available as environment variable (e.g. `MY_SLACK_BOT_TOKEN`) in your GitHub repository. This can be done in Settings → Secrets → Actions. It will then be accessible in your workflow file using the `${{ secrets.MY_SLACK_BOT_TOKEN }}` expression.

## Branch protection rules

Once you have a testing workflow and sufficient tests covering your code, you can setup a branch protection rule. This can be done by navigating to Settings → Branches → Branch protection rules → Add rule in your GitHub repository.

The "Branch name pattern" supports [fnmatch syntax](https://ruby-doc.org/core-2.5.1/File.html#method-c-fnmatch) but also allows to set a single branch name (like "main"). In order to protect your branch from incompatible dependency updates you have to activate "Require status checks to pass before merging". You can use GitHub Actions as status checks by searching for their job names (e.g. "test").

**Screenshot:**

![Branch Protection Rule Setup](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uew1wu0ujgncwndo7yvj.png) 

The branch protection rule will warn you when new code fails your testing pipeline. It will also prevent merging broken code into your "main" branch when you are not an administrator who can override such rules.

## Running your GitHub Actions locally

If you want to have faster feedback loops, you can also run GitHub Actions locally using the [act cli](https://github.com/nektos/act). It requires Docker and a [local installation](https://github.com/nektos/act/tree/v0.2.26#installation-through-package-managers) through your favorite package manager.

After installing "act", you can run it locally from your terminal by passing it the job name of your workflow, e.g. `act -j test`. It will then download the necessary Docker image. Depending on the complexity of your workflow, this image can be 20+ GB in size. For our small test setup a micro image (below 200 MB) that contains only Node.js is good enough when we remove our "skip ci" condition.

**Screenshot:**

![act-cli in action](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bz6v41au6aebfkrb0h6v.png)
 

## Where to go from here?  

**Congratulations!** You have just learned the fundamentals of GitHub Actions and you are now able to create your own workflows. With your newly acquired skills, you can build great CI/CD pipelines. 🎊

If you want to learn more about GitHub Actions, then I recommend the following topics:

- [Creating dependant jobs](https://docs.github.com/en/actions/using-workflows/advanced-workflow-features#creating-dependent-jobs)
- [Reuse workflows to avoid duplication](https://docs.github.com/en/actions/using-workflows/reusing-workflows)
- [Use Dependabot with GitHub Actions](https://docs.github.com/en/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions)
- [Review deployment jobs](https://youtu.be/w_37LDOy4sI?t=570)
- [Create and publish your own actions using JavaScript](https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action#writing-the-action-code)
- [Use self-hosted runners with your workflows](https://www.youtube.com/watch?v=SASoUr9X0QA)