CI with GitHub Actions for Ember Apps
Tags: ember, javascript, ci, showdev
Published: 2020-03-17 17:35:08

*Originally published on [crunchingnumbers.live](https://crunchingnumbers.live/2020/03/17/ci-with-github-actions-for-ember-apps/)*

Lately I've been working on [Ember Music](https://ember-music.herokuapp.com/), an app that I can use as a playground to test addons and ideas in Ember. When I need to write a blog post, I can reach for this app instead of designing a new one each time. Since the app will grow over time, I wanted to introduce **continuous integration** (CI) and **continuous deployment** early.

[Heroku Dashboard](https://dashboard.heroku.com/) makes deploying code on GitHub simple. From the Deploy tab, you select GitHub, find your repo, then check "Wait for CI to pass before deploy."

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/introduction_heroku.png" alt="" width="600" height="320" />

<figcaption>Heroku makes deploying code on GitHub simple.</figcaption>
</figure>

For continuous integration, I tried out [GitHub Actions](https://github.com/features/actions) since it is free (there are limits to minutes and storage for private repos) and my code is on GitHub. I also wanted to find an alternative to Codeship Pro that I use for work. One app has about 150 tests, but CI time wildly varies between 3 and 15 minutes. Because ten minutes is how long CI took for a larger app that I had worked on, I haven't been content.

With GitHub Actions, I was able to make a **workflow** that did everything I want:

- Set operating system and Node version
- Cache ​dependencies (avoid `yarn install`)
- Lint files and dependencies
- Run tests separately from linting
- Split tests and run in parallel
- Take [Percy](https://docs.percy.io/docs/ember) snapshots in parallel
- Be cost effective

In this blog post, I will share my workflow because there is a high chance that you, too, want to solve the problems listed above. Rather than dumping the entire workflow on you, I will start with a simple one and let it organically grow. Throughout, I will assume that you use `yarn` to manage packages. If you use `npm`, please check the GitHub Gist at the end to see the differences.


# 1. I Want to Run Tests

Testing is available to every Ember app and is integral to CI, so let's look at how to write a workflow that runs `ember test`. Along the way, you will see how to set the operating system and Node version.


## a. Create Workflow

In the root of your project, create folders called `.github` and `.github/workflows`. All workflows must be stored in `.github/workflows`. Workflows are written in YAML, so let's create a file called `ci.yml`.

```bash
# Folder structure

ember-music
│
├── .github
│   │
│   └── workflows
│       │
│       └── ci.yml
│
├── app
│
│   ...
│
├── tests
│
│   ...
│
├── package.json
│
└── yarn.lock
```

In the file, we can use the `on` and `jobs` keys to specify when CI runs and what it does. We can also give the workflow a `name`.

```yaml
# File: .github/workflows/ci.yml

name: CI

on: [push, pull_request]

jobs:
```

If you commit and push this file, the workflow will fail in an instant. (GitHub does notify you by email.) On GitHub, let's click on the Actions tab, then find the workflow to see what went wrong. The error message shows that we haven't defined jobs.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_1a_github_actions.png" alt="" width="600" height="500" />

<figcaption>GitHub Actions alerts us we haven't defined jobs.</figcaption>
</figure>


## b. Define Jobs

A workflow must have one or more jobs to do. A **job** is completed by following a set of `steps`. At each **step**, we can `run` a command or `use` an action (custom or imported) to do something meaningful—something that gets us closer to finishing the job.

When someone makes a push or pull request, a CI's job is to run tests. Think about what steps you take to test someone else's Ember app. Likely, you would:

1. Clone the repo.
2. Set the Node version, maybe with `nvm`.
3. Run `yarn` to install dependencies.
4. Run `ember test`.

Guess what? We can tell a workflow to do the same!

```yaml
# File: .github/workflows/ci.yml

name: CI

on: [push, pull_request]

jobs:
    test:
        name: Run tests
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo
              uses: actions/checkout@v2

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

            - name: Install dependencies
              run: yarn install --frozen-lockfile

            - name: Test Ember app
              run: yarn test
```

Because checking out a repo and setting up Node are common tasks, GitHub Actions provides actions that you can just call. The `matrix` key lets you run the workflow on various operating systems and Node versions. Since I'm writing the app for myself, I specified one OS and Node version. If you are developing an addon for other people, you will likely specify more (take [Ember versions](https://github.com/ember-cli/ember-try) into account, too).

You may have noticed that I ran `yarn test`. I did so because `package.json` provides a script called `test`. In Ember 3.16, these are the default scripts:

```javascript
// File: package.json

{
    ...

    "scripts": {
        "build": "ember build --environment=production",
        "lint:hbs": "ember-template-lint .",
        "lint:js": "eslint .",
        "start": "ember serve",
        "test": "ember test"
    }

    ...
}
```

In short, running `yarn test` means running `ember test`. By relying on the scripts in `package.json`, CI can check our code in the same manner as we might locally. We'll update these scripts as we expand the workflow.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_1b_github_actions.png" alt="" width="600" height="320" />

<figcaption>GitHub Actions can run Ember tests.</figcaption>
</figure>


## c. When Should CI Run?

In the sections above and below, I used `on: [push, pull_request]` for simplicity.

For a production app where you would create branches, make pull requests (PRs), then merge to `master` branch, consider instead:

```yaml
# File: .github/workflows/ci.yml

name: CI

on: 
    push:
        branches:
            - master
    pull_request:

...
```

Then, your CI will run according to these rules:

- If you create a branch and make a push, CI will not run.
- If you create a PR for that branch (draft or open), CI will run. GitHub Actions shows the run type to be `pull_request`.
- Marking a draft PR as ready (open) will not trigger CI again. 👍
- Any additional pushes that you make to the PR will trigger CI. (type: `pull_request`)
- If you merge the PR into `master`, CI will run once more. (type: `push`)


# 2. I Want to Lint

A CI can also lint files and dependencies. Before the app becomes large and unwieldy, we want to ensure that our code follows a standard and relies on a single version for each package.

Rather than adding a step to our existing job, we can create 2 jobs—one for linting and another for running tests—so that they can run in parallel. In GitHub Actions, we specify an extra job like this:

```yaml
# File: .github/workflows/ci.yml

name: CI

on: [push, pull_request]

jobs:
    lint:
        name: Lint files and dependencies
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo
              uses: actions/checkout@v2

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

            - name: Install dependencies
              run: yarn install --frozen-lockfile

            - name: lint:dependency
              run: yarn lint:dependency

            - name: lint:hbs
              run: yarn lint:hbs

            - name: lint:js
              run: yarn lint:js

    test: ...
```

Although duplicate code (lines 14-23) are an eyesore, we'll repeat steps for simplicity—take baby steps to understanding GitHub Actions. At this point, we are more concerned by if the workflow will still pass than if GitHub Actions allows a "beforeEach hook." (The feature that'd let us DRY steps is called **YAML anchor**. At the time of writing, [anchors are not supported](https://github.community/t5/GitHub-Actions/Support-for-YAML-anchors/m-p/30336).)

From line 26, you might guess that `package.json` has an additional script. Indeed, it runs the addon [ember-cli-dependency-lint](https://github.com/salsify/ember-cli-dependency-lint).

```javascript
// File: package.json

{
    ...

    "scripts": {
        "build": "ember build --environment=production",
        "lint:dependency": "ember dependency-lint",
        "lint:hbs": "ember-template-lint .",
        "lint:js": "eslint .",
        "start": "ember serve",
        "test": "ember test --query=nolint"
    }

    ...
}
```

By default, Ember QUnit lints if you have `ember-cli-eslint`, `ember-cli-template-lint`, or `ember-cli-dependency-lint`. Now that we have a job dedicated to linting, I passed `--query=nolint` so that the job for testing does not lint again.

As an aside, starting with [Ember 3.17](https://blog.emberjs.com/2020/03/16/ember-3-17-released.html), you are advised to remove `ember-cli-eslint` and `ember-cli-template-lint` in favor of using `eslint` and `ember-template-lint`. The one exception is if you need _live_ linting. But chances are, you don't thanks to CI. You can now enjoy faster build and rebuild!

Let's commit changes and push. When you see 2 green checks, let out that sigh.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_2_github_actions-1.png" alt="" width="600" height="320" />

<figcaption>GitHub Actions can lint and run tests at the same time.</figcaption>
</figure>


# 3. I Want to Run Tests in Parallel

We can [promote writing more tests](https://crunchingnumbers.live/2019/10/11/write-tests-like-a-mathematician-part-3/) if the time to run them can remain small. One way to achieve this is to split tests and run them in parallel using [Ember Exam](https://ember-cli.com/ember-exam/).


## a. Setup

After you install `ember-exam`, please open the file `tests/test-helper.js`. You must replace the `start` method from Ember QUnit (or Mocha) with the one from Ember Exam. Otherwise, running the command `ember exam` has no effect.

```javascript
// File: tests/test-helper.js

import Application from '../app';
import config from '../config/environment';
import { setApplication } from '@ember/test-helpers';
import start from 'ember-exam/test-support/start';

setApplication(Application.create(config.APP));

start({
    setupTestIsolationValidation: true
});
```


## b. Divide and Conquer

By trial and error, I came up with a script that I hope works for you too:

```javascript
// File: package.json

{
    ...

    "scripts": {
        "build": "ember build --environment=production",
        "lint:dependency": "ember dependency-lint",
        "lint:hbs": "ember-template-lint .",
        "lint:js": "eslint .",
        "start": "ember serve",
        "test": "ember exam --query=nolint --split=4 --parallel=1"
    }

    ...
}
```

I wrote the script so that we can append flags to do useful things. With `yarn test --server`, for example, you should see 4 browsers running. Good to have a sanity check. Each browser—a **partition**—handles roughly a quarter of the tests. If you use QUnit, you can run `yarn test --server --random` to check if your tests are order-dependent.

Most importantly, the script allows us to append the `--partition` flag so that GitHub Actions knows how to run Ember tests in parallel. Let's rename the job called `test` to `test-partition-1` and update its last step to run partition 1. Then, create three more jobs to run partitions 2 to 4.

```yaml
# File: .github/workflows/ci.yml

name: CI

on: [push, pull_request]

jobs:
    lint: ...

    test-partition-1:
        name: Run tests - Partition 1
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo
              uses: actions/checkout@v2

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

            - name: Install dependencies
              run: yarn install --frozen-lockfile

            - name: Test Ember app
              run: yarn test --partition=1

    test-partition-2: ...

    test-partition-3: ...

    test-partition-4:
        name: Run tests - Partition 4
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo
              uses: actions/checkout@v2

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

            - name: Install dependencies
              run: yarn install --frozen-lockfile

            - name: Test Ember app
              run: yarn test --partition=4
```

Now, the workflow has 5 jobs. You can check that tests do run separately from linting and in parallel. You can also check that each partition has a different set of tests.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_3b_github_actions.png" alt="" width="600" height="500" />

<figcaption>GitHub Actions can run tests in parallel.</figcaption>
</figure>

Unfortunately, everything isn't awesome. Each job has to run `yarn install`, and this will happen every time we make a push or pull request. When you think about it, linting and running tests can rely on the same setup so why install 5 times? Furthermore, if packages didn't change since the last build, we could skip installation altogether.

Let's take a look at how to **cache** in GitHub Actions next.


# 4. I Want to Cache

Here is where things began to fall apart for me. The documentation didn't make it clear that the way to cache *differs* between `yarn` and `npm`. It also didn't show how to avoid `yarn install` when the cache *is* available and up-to-date. Hopefully, this section will save you from agony.

To illustrate caching, I'll direct your attention to one job, say `test-partition-1`:

```yaml
# File: .github/workflows/ci.yml

name: CI

on: [push, pull_request]

jobs:
    test-partition-1:
        name: Run tests - Partition 1
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo
              uses: actions/checkout@v2

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

            - name: Install dependencies
              run: yarn install --frozen-lockfile

            - name: Test Ember app
              run: yarn test --partition=1
```

We want to know how to update lines 22-23 so that the job does `yarn install` only when necessary. The changes that we will make also apply to the other jobs.

The idea is simple. First, `yarn` keeps a **global cache** that stores every package that you use. This way, it doesn't need to download the same package again. We want to cache that global cache. Second, from experience, we know that creating the `node_modules` folder takes time. Let's cache that too! When the global cache or `node_modules` folder is out of date, we will run `yarn install`.

The hard parts are digging documentation and scouring the web for examples. I'll save you the trouble. In the end, we get lines 22-48:

```yaml
# File: .github/workflows/ci.yml

name: CI

on: [push, pull_request]

jobs:
    test-partition-1:
        name: Run tests - Partition 1
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo
              uses: actions/checkout@v2

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

            - name: Get Yarn cache path
              id: yarn-cache-dir-path
              run: echo "::set-output name=dir::$(yarn cache dir)"

            - name: Cache Yarn cache
              id: cache-yarn-cache
              uses: actions/cache@v1
              with:
                path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
                key: ${{ runner.os }}-${{ matrix.node-version }}-yarn-${{ hashFiles('**/yarn.lock') }}
                restore-keys: |
                  ${{ runner.os }}-${{ matrix.node-version }}-yarn-

            - name: Cache node_modules
              id: cache-node-modules
              uses: actions/cache@v1
              with:
                path: node_modules
                key: ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-${{ hashFiles('**/yarn.lock') }}
                restore-keys: |
                  ${{ runner.os }}-${{ matrix.node-version }}-nodemodules-

            - name: Install dependencies
              run: yarn install --frozen-lockfile
              if: |
                steps.cache-yarn-cache.outputs.cache-hit != 'true' ||
                steps.cache-node-modules.outputs.cache-hit != 'true'

            - name: Test Ember app
              run: yarn test --partition=1
```

Amid the changes, I want you to grasp just 3 things.

First, the workflow needs to know where to find the global cache to cache it. We use `yarn cache dir` to find the path (line 24) and pass it to the next step via `id` (line 23) so that we don't hardcode a path that works for one OS but not others. (For `npm`, the documentation showed `path: ~/.npm`. It works in Linux and Mac, but not Windows.)

Second, the workflow needs to know when it is okay to use a cache. The criterion will depend on what we're caching. For the global cache and `node_modules` folder, we can be certain that it's okay to use the cache if `yarn.lock` hasn't changed. `hashFiles()` allows us to check for a file difference with efficiency and high confidence. We encode this criterion by including the hash in the cache's `key` (lines 31 and 40).

Finally, we can use `if` to take a conditional step (line 46). The action, `actions/cache`, returns a Boolean to indicate if it found a cache. As a result, we can tell the workflow to install dependencies if the `yarn.lock` file changed.

Thanks to caching, all jobs can now skip `yarn install`.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_4_github_actions.png" alt="" width="600" height="500" />

<figcaption>GitHub Actions can cache dependencies.</figcaption>
</figure>


# 5. I Want to Take Percy Snapshots

The last problem that we want to solve is taking Percy snapshots (visual regression tests) *in parallel*.


## a. Setup

If you haven't yet, make a new project in Percy. Link it to your GitHub repo by clicking on the Integrations tab. Finally, retrieve the project token, `PERCY_TOKEN`, by switching to Project settings tab.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5a_percy.png" alt="" width="600" height="320" />

<figcaption>Link the Percy project to your GitHub repo.</figcaption>
</figure>

You can provide `PERCY_TOKEN` to GitHub by visiting your repo and clicking on the Settings tab. Find the submenu called Secrets.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5a_github.png" alt="" width="600" height="320" />

<figcaption>Pass the Percy token to GitHub.</figcaption>
</figure>

GitHub Actions can now access `PERCY_TOKEN` and send Percy snapshots.


## b. First Attempt

Integrating Percy with GitHub Actions isn't too difficult. Percy documented the how-to well and even provides an action, `percy/exec-action`, to facilitate the workflow.

Let's see what happens when we update the test step like this:

```yaml
# File: .github/workflows/ci.yml

name: CI

on: [push, pull_request]

jobs:
    lint: ...

    test-partition-1:
        name: Run tests - Partition 1
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo

            ...

            - name: Test Ember app
              uses: percy/exec-action@v0.3.0
              with:
                custom-command: yarn test --partition=1
              env:
                PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

    test-partition-2: ...

    test-partition-3: ...

    test-partition-4:
        name: Run tests - Partition 4
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo

            ...

            - name: Test Ember app
              uses: percy/exec-action@v0.3.0
              with:
                custom-command: yarn test --partition=4
              env:
                PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
```

We need to change the `test` script one last time. Let's prepend `percy exec --`. It allows Percy to start and stop around the supplied command.

```javascript
# File: package.json

{
    ...

    "scripts": {
        "build": "ember build --environment=production",
        "lint:dependency": "ember dependency-lint",
        "lint:hbs": "ember-template-lint .",
        "lint:js": "eslint .",
        "start": "ember serve",
        "test": "percy exec -- ember exam --query=nolint --split=4 --parallel=1"
    }

    ...
}
```

When we commit the changes, the tests for Ember will continue to pass. However, Percy will think that we made 4 builds rather than 1. It's hard to tell which of the four holds the "truth." Maybe none do.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5b_percy_incorrect_setup.png" alt="" width="600" height="360" />

<figcaption>Multiple Percy builds occur if we naively implement the parallel workflow.</figcaption>
</figure>

This problem occurs when we run tests in parallel. We need to tell Percy somehow that there are 4 jobs for testing and the snapshots belong to the same build.


## c. Orchestrate


Luckily, we can use Percy's environment variables to coordinate snapshots. Setting `PERCY_PARALLEL_TOTAL`, the number of parallel build nodes, is easy in my case. It's always 4. But what about `PERCY_PARALLEL_NONCE`, a unique identifier for the build?

GitHub keeps track of two variables, `run_id` and `run_number`, for your repo. The former is a number for each run in the repo (e.g. 56424940, 57489786, 57500258), while the latter is a number for each run of a particular workflow in the repo (e.g. 44, 45, 46). Just to be safe, I combined the two to arrive at a nonce.

```yaml
# File: .github/workflows/ci.yml

name: CI

on: [push, pull_request]

env:
    PERCY_PARALLEL_NONCE: ${{ github.run_id }}-${{ github.run_number }}

jobs:
    lint: ...

    test-partition-1:
        name: Run tests - Partition 1
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo

            ...

            - name: Test Ember app
              uses: percy/exec-action@v0.3.0
              with:
                custom-command: yarn test --partition=1
              env:
                PERCY_PARALLEL_NONCE: ${{ env.PERCY_PARALLEL_NONCE }}
                PERCY_PARALLEL_TOTAL: 4
                PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}

    test-partition-2: ...

    test-partition-3: ...

    test-partition-4:
        name: Run tests - Partition 4
        runs-on: ${{ matrix.os }}
        strategy:
            matrix:
                os: [ubuntu-latest]
                node-version: [12.x]
        steps:
            - name: Check out a copy of the repo

            ...

            - name: Test Ember app
              uses: percy/exec-action@v0.3.0
              with:
                custom-command: yarn test --partition=4
              env:
                PERCY_PARALLEL_NONCE: ${{ env.PERCY_PARALLEL_NONCE }}
                PERCY_PARALLEL_TOTAL: 4
                PERCY_TOKEN: ${{ secrets.PERCY_TOKEN }}
```

Once you introduce these environment variables, Percy will group the snapshots to a single build.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_5b_percy_correct_setup.png" alt="" width="600" height="240" />

<figcaption>A single Percy build occurs when we implement the parallel workflow right.</figcaption>
</figure>


# 6. Conclusion

Overall, I had a great time figuring out how to write a CI workflow for Ember apps in GitHub Actions. Writing code helped me understand better the steps involved in a CI. Not all was great, though. The documentation for caching can definitely use help with showing clear, exhaustive examples.

At any rate, now, I can sit back and enjoy the benefits of linting and running tests with every commit. I'm looking forward to see what [Ember Music](https://ember-music.herokuapp.com/) will turn into.

<figure>
<img src="https://crunchingnumbersdotlive.files.wordpress.com/2020/03/section_6_badges.png" alt="" width="600" height="168" />

<figcaption>You made it! Give yourself some badges.</figcaption>
</figure>


# Notes

You can find my CI workflow for Ember apps on GitHub Gist ([yarn](https://gist.github.com/ijlee2/2e7be877ceb60b9fa7c80298121d1024), [npm](https://gist.github.com/ijlee2/030df226d02e32f6e4311591f58d87f8)). It works for all operating systems: Linux, Mac, and Windows.

In `testem.js`, you will see a reference to `process.env.CI`:

```javascript
// File: testem.js

module.exports = {
    test_page: 'tests/index.html?hidepassed',

    ...

    browser_args: {
        Chrome: {
            ci: [
                // --no-sandbox is needed when running Chrome inside a container
                process.env.CI ? '--no-sandbox' : null,
                '--headless',
                '--disable-dev-shm-usage',
                '--disable-software-rasterizer',
                '--mute-audio',
                '--remote-debugging-port=0',
                '--window-size=1440,900'
            ].filter(Boolean)
        }
    }
};
```

I'm not sure where `--no-sandbox` gets used (this [comic](https://www.google.com/googlebooks/chrome/med_26.html) explains **sandbox**) and haven't found a need for it yet. If you need it for CI, please check the `ember-animated` example below. It seems, at the job level, you can set the environment variable.

I would like to know more about the history of and need for `--no-sandbox`.


## Resources

If you want to learn more about GitHub Actions, Ember Exam, and Percy, I encourage you to visit these links:


### GitHub Actions

- [About Billing for GitHub Actions](https://help.github.com/en/github/setting-up-and-managing-billing-and-payments-on-github/about-billing-for-github-actions)
- [Configuring a Workflow](https://help.github.com/actions/configuring-and-managing-workflows/configuring-a-workflow)
- [Using Node.js with GitHub Actions](https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions)
- [Caching Dependencies to Speed Up Workflows](https://help.github.com/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows)
- [Cache Implementation for `npm`](https://github.com/actions/cache/blob/master/examples.md#node---npm)
- [Cache Implementation for `yarn`](https://github.com/actions/cache/blob/master/examples.md#node---yarn)


### Ember Exam

- [Quickstart](https://ember-cli.com/ember-exam/docs)


### Percy

- [GitHub Actions](https://docs.percy.io/docs/github-actions)
- [Parallel Test Suites](https://docs.percy.io/docs/parallel-test-suites)


### Workflow Examples

- [chart-parts](https://github.com/microsoft/chart-parts/blob/master/.github/workflows/ci.yml)
- [ember-animated](https://github.com/ember-animation/ember-animated/blob/master/.github/workflows/nodejs.yml)
- [ember-clear](https://github.com/NullVoxPopuli/emberclear/blob/master/.github/workflows/frontend-tests.yml)
- [ember-cli-htmlbars](https://github.com/ember-cli/ember-cli-htmlbars/blob/master/.github/workflows/ci.yml)
- [glimmer.js](https://github.com/glimmerjs/glimmer.js/blob/master/.github/workflows/ci.yml)