Setting up a CI/CD workflow on GitHub Actions for a React App (with GitHub Pages and Codecov)
Tags: github, react, devops, javascript
Published: 2020-06-19 01:36:42

In this tutorial, I'm going to show you how to create a simple workflow that I use on my personal projects with React.

This workflow created on [GitHub Actions](https://github.com/features/actions) will be responsible for automatically test the source code, generate a test coverage report and upload it on [Codecov](https://codecov.io), build and deploy the project on [GitHub Pages](https://pages.github.com). All these jobs are activated by a push or pull request event on master branch.

> All the source code can be found in this repository:
{% github dyarleniber/react-workflow-gh-actions no-readme %}

# Getting started

GitHub Actions allows you automate, customize, and execute software development workflows right in your GitHub repository.

A workflow is a configurable automated process made up of one or more jobs. You can configure your workflows to run when specific activity on GitHub happens, at a scheduled time, or when an event outside of GitHub occurs.

The workflow configuration is define by a YAML file. GitHub provides preconfigured workflow templates.

First, on your React App GitHub repository, navigate to the main page of the repository, click Actions.

Then, you'll see suggestions of CI templates that are the best fit for your project. You can use workflow templates as a starting place to build your custom workflow.
In this case, click Set up this workflow, under the name of the template Node.js.

![Actions on GitHub page](https://dev-to-uploads.s3.amazonaws.com/i/tcybkp4u4841gc551vur.png)

Finally, you'll see a default YAML file like this:

```yaml
# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions

name: Node.js CI

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [10.x, 12.x, 14.x]

    steps:
    - uses: actions/checkout@v2
    - name: Use Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}
    - run: npm ci
    - run: npm run build --if-present
    - run: npm test
```

There are 2 events that trigger the workflow on push or pull request. The virtual environment used to run the workflow is an Ubuntu machine in the latest version.

In the steps section, 2 actions and 3 NPM scripts are being used. The first action `actions/checkout@v2` is a standard that you must include in your workflow before other actions when your workflow requires a copy of your repository's code. The second one, as the name suggests, it is a set up for Node.js, as you can see, the set up is made in different versions of Node.js, through an array with all the versions used.

Besides these actions, you can browse and search for actions in [GitHub Marketplace](https://github.com/marketplace?type=actions) to use in your workflows.

GitHub Marketplace is a central location for you to find actions created by the GitHub community.

# Test coverage report generation

Let's start changing the name, the Node.js version and the NPM scripts used in the workflow:

```yaml
name: CI/CD

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x]

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Set up Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: npm install

    - name: Run the tests
      run: npm test

    - name: Build
      run: npm run build
```

After set up Node.js, now, we are installing the dependencies, running the tests and building the application.

> You can define a name for each step.

Now, we are going to use Codecov to upload and compare test coverage reports. Codecov is one of the most popular code coverage solution, and it's totally free for open source projects.

First, go to the [Codecov website](https://codecov.io/) and create an account, you can use your GitHub account to sign up.

Next, access your account on the website, click Repositories, after, click Add new repository and choose the repository you want to use.

![Codecov interface](https://dev-to-uploads.s3.amazonaws.com/i/hzop5owzcpbaz3rtlgyb.png)

You will see a token, that token will not be useful if your repository is public, otherwise, if your repository is private, you must copy it, as you will need to use it later.

There is an [action available in GitHub Marketplace](https://github.com/marketplace/actions/codecov), that easily upload reports to Codecov, we will use it.

The first thing we need to do is change the `npm test` command to generate the test reports:

```yaml
- name: Run the tests and generate coverage report
  run: npm test -- --coverage
```

After that, include the following action in the workflow:

```yaml
- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v1
```

If you repository is private, you must include the codecov token, through a [secret](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets) that makes your token accessible in the workflow. Like this:

```yaml
- name: Upload coverage to Codecov
  uses: codecov/codecov-action@v1
  with:
    token: ${{ secrets.CODECOV_TOKEN }}
```

> We'll create a secret on GitHub for deployment in a moment.

# Deployment on GitHub Pages

We'll use GitHub Pages to deploy our application. GitHub Pages are public pages hosted and published through GitHub.

Before adding new steps to the workflow, we need to install and configure a new package in the project.

1. Add homepage to `package.json`.

Open your `package.json` and add a homepage field for your project:

```json
"homepage": "https://myusername.github.io/my-app",
```

2. Install `gh-pages` and add `deploy` to `scripts` in `package.json`.

Run:

```bash
$ npm install --save gh-pages
```

Add the following scripts in your `package.json`:

```json
"predeploy": "npm run build",
"deploy": "gh-pages -d build",
```

> The `predeploy` script will run automatically before `deploy` is run.

Now we are going to create a new access token, in order to deploy our application through the workflow.

Go to the [Personal access tokens area](https://github.com/settings/tokens) in the Developer settings of your GitHub profile and click Generate new token.

![Creating personal access token](https://dev-to-uploads.s3.amazonaws.com/i/zruq5vw6btno2899m4bu.png)

Fill up a note (What's this token for?) and in the repo scope, select only the first 4 options.

![personal access token scope](https://dev-to-uploads.s3.amazonaws.com/i/lhimosexokwen7vm7qlq.png)

Copy the generated token.

Now, in order to use this token in the workflow on GitHub Actions, we have to create a secret for our repository.

Secrets are encrypted environment variables that allows you to store sensitive information in your repository. The secrets you create are available to use in GitHub Actions workflows.

On GitHub, navigate to the main page of the repository, under your repository name, click Settings. In the left sidebar, click Secrets. And finally, Click Add a new secret.

![Alt Text](https://dev-to-uploads.s3.amazonaws.com/i/9tnsdxq4u5wovvf8fm32.png)

Type a name for your secret in the Name input box, like `ACTIONS_DEPLOY_ACCESS_TOKEN`.

Enter the value for your secret (which should be the personal access token we just created).

Click Add secret.

Finally, we can include the script `npm run deploy` as the last step in our workflow.

But, before that, we should make some configurations in the Git. To ensure that it will have all the necessary access to be able to deploy the application.

Your last step on your workflow should be some like this:

```yaml
- name: Deploy
  run: |
    git config --global user.name $user_name
    git config --global user.email $user_email
    git remote set-url origin https://${github_token}@github.com/${repository}
    npm run deploy
  env:
    user_name: 'github-actions[bot]'
    user_email: 'github-actions[bot]@users.noreply.github.com'
    github_token: ${{ secrets.ACTIONS_DEPLOY_ACCESS_TOKEN }}
    repository: ${{ github.repository }}
```

The name and email information need not necessarily be your real information. And you must replace `ACTIONS_DEPLOY_ACCESS_TOKEN` with the name of the secret you just created.

The `github.repository` variable will automatically get the name of your repository.

> To learn more about environment variables on GitHub Actions, please, [read this article](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets).

After adding the deploy command on your workflow, click Start commit, and click Commit new file.

![Workflow commit](https://dev-to-uploads.s3.amazonaws.com/i/1kw82q8g6s09wyf305yd.png)

Your final workflow file should be like this:

```yaml
name: CI/CD

on:
  push:
    branches: [ master ]
  pull_request:
    branches: [ master ]

jobs:
  build:

    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [12.x]

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Set up Node.js ${{ matrix.node-version }}
      uses: actions/setup-node@v1
      with:
        node-version: ${{ matrix.node-version }}

    - name: Install dependencies
      run: npm install

    - name: Run the tests and generate coverage report
      run: npm test -- --coverage

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v1

    - name: Build
      run: npm run build

    - name: Deploy
      run: |
        git config --global user.name $user_name
        git config --global user.email $user_email
        git remote set-url origin https://${github_token}@github.com/${repository}
        npm run deploy
      env:
        user_name: 'github-actions[bot]'
        user_email: 'github-actions[bot]@users.noreply.github.com'
        github_token: ${{ secrets.ACTIONS_DEPLOY_ACCESS_TOKEN }}
        repository: ${{ github.repository }}
```

> Make sure GitHub Pages option in your GitHub project settings is set to use the gh-pages branch:
![gh-pages branch](https://dev-to-uploads.s3.amazonaws.com/i/ga940kangwb3ljmwox3c.png)

Now, in every push or pull request event on master branch, the CI/CD workflow will be activated. And you will be able to see if all steps have passed or not.

![Workflow execution](https://dev-to-uploads.s3.amazonaws.com/i/s8tg50gitavbj973delw.png)

# Adding status badges

You can also add a workflow status badge to your repository. Status badges show whether a workflow is currently failing or passing. A common place to add a status badge is in the `README.md` file of your repository, but you can add it to any web page you'd like.

This is the default URL for adding a workflow status badge provided by GitHub Actions:

```
https://github.com/<OWNER>/<REPOSITORY>/workflows/<WORKFLOW_NAME>/badge.svg
```

If your workflow uses the name keyword, you must reference the workflow by name. If the name of your workflow contains white space, you'll need to replace the space with the URL encoded string %20.

For more information about GitHub Actions workflow status badge, [read this article](https://help.github.com/en/actions/configuring-and-managing-workflows/configuring-a-workflow#adding-a-workflow-status-badge-to-your-repository).

For instance, this will be the badge that I will put in my `README.md` file:

```
![CI/CD](https://github.com/dyarleniber/react-workflow-gh-actions/workflows/CI/CD/badge.svg)
```

Codecov also provided a badge you can use in your projects. This is an example of an URL:

```
[![codecov](https://codecov.io/gh/<OWNER>/<REPOSITORY>/branch/master/graph/badge.svg)](https://codecov.io/gh/<OWNER>/<REPOSITORY>)
```

In my case, my URL will be:

```
[![codecov](https://codecov.io/gh/dyarleniber/react-workflow-gh-actions/branch/master/graph/badge.svg)](https://codecov.io/gh/dyarleniber/react-workflow-gh-actions)
```

You can also use this amazing website: [Shields.io](https://shields.io/) to customise your badges. Accessing the website you will see many options of badges categories you can use, such as Build, Code Coverage, Size, Downloads, License and so on. In each category you can select the service you are using, fill up the name of the repository, customise as you prefer, and copy the link of the badge.

For example, this will be my custom coverage badge:

```
[![Codecov](https://img.shields.io/codecov/c/github/dyarleniber/react-workflow-gh-actions)](https://codecov.io/gh/dyarleniber/react-workflow-gh-actions)
```

I also included a license badge:

```
![License](https://img.shields.io/github/license/dyarleniber/react-workflow-gh-actions)
```

These badges in your `README.md` file, should look like this:

![Badges](https://dev-to-uploads.s3.amazonaws.com/i/3uf0iwu1bj29yg0i22bw.png)

You can explore the [Shields.io](https://shields.io/) website and see the different options of badges available.

# Bonus: Caching dependencies

To make your workflows faster and more efficient, you can create and use caches for dependencies and other commonly reused files.

GitHub can cache dependencies you frequently use in workflows. Jobs on GitHub-hosted runners start in a clean virtual environment and must download dependencies each time, causing increased network utilization, longer runtime, and increased cost.

To cache dependencies for a job, you'll need to use GitHub's `cache` action. The action retrieves a cache identified by a unique key. For more information, see [actions/cache](https://github.com/actions/cache).

In our case, we can create a new cache when the packages in `package-lock.json` file change, or when the runner's operating system changes. The cache key uses contexts and expressions to generate a key that includes the runner's operating system and a SHA-256 hash of the `package-lock.json` file.

You should include the following snippet of code before installing the dependencies:

```yaml
    - name: Cache dependencies
      uses: actions/cache@v2
      with:
        path: |
          **/node_modules
        key: ${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
```

The `key` is required. It is the key created when saving a cache and the key used to search for a cache. Keys have a maximum length of 512 characters.

The `path` is required. It is the file path on the runner to cache or restore. This can be an absolute path or relative to the working directory. The path input must be a directory. You cannot cache a single file.

The `restore-keys` is an optional parameter. It is an ordered list of alternative keys to use for finding the cache if no cache hit occurred for key.

The `cache` action will attempt to restore a cache based on the `key` you provide. When the action finds a cache, the action restores the cached files to the `path` you configure. If there is no exact match, the action creates a new cache entry if the job completes successfully. The new cache will use the `key` you provided and contains the files in the `path` directory.

# References

- https://github.com/features/actions
- https://www.codecov.io/
- https://github.com/codecov/codecov-action
- https://pages.github.com/
- https://create-react-app.dev/docs/deployment#github-pages
- https://shields.io/
- https://help.github.com/pt/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows
- https://github.com/actions/cache