Skip Cypress Install On CI

How to add Cypress to an existing large project without slowing down the continuous integration step.

Let's say you want to introduce Cypress End-to-End tests in your company. You probably already have a repository with lots of tests, and the CI build script is ... complicated. Can you add Cypress without modifying lots of installation and caching steps in the CI config file? Can you avoid your coworkers' anger?

Sure. At first, you can get away with not installing Cypress on CI. You can still run your initial tests (that are probably going to be simple) using a cypress/included Docker image with Cypress pre-installed.

Let's take an example application in bahmutov/todomvc-vercel. It is deployed to Vercel platform, and runs just the linting step using the GitHub Actions lint.yml workflow.

.github/workflows/lint.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
name: lint
on: push
jobs:
lint:
runs-on: ubuntu-20.04
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Install dependencies
# https://github.com/bahmutov/npm-install
uses: bahmutov/npm-install@v1
- name: Lint
run: npm run lint

Let's add Cypress NPM dependency.

1
$ npm i -D cypress

We can write a few tests, like this sanity spec.js

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
/// <reference types="cypress" />

it('works', () => {
cy.visit('/')
// application starts with 3 todos
cy.get('.todo').should('have.length', 3)
cy.get('[data-cy=new-todo]').type('Add tests!{enter}')
cy.get('.todo')
.should('have.length', 4)
.eq(3)
.should('include.text', 'Add tests!')
...
})

Let's see what happens next:

  • the coworkers all get a Cypress binary on their machines the next time they run npm install or yarn install. This binary is stored in the central folder on their machine, thus it is downloaded and unzipped once per Cypress version. It is 1-2 minute process, but that is acceptable.
  • the continuous integration server will install the Cypress binary on every workflow execution, to avoid this you need to configure Cypress caching. If you have a complicated CI workflow, this will be complicated.

We can take a middle path: we can use Cypress to run our tests without installing it on the CI during the NPM installation. Let's keep our lint workflow and just stop it from downloading the Cypress binary - since it is not going to run any Cypress tests.

1
2
3
4
5
6
7
8
9
- name: Install dependencies
# https://github.com/bahmutov/npm-install
uses: bahmutov/npm-install@v1
env:
# avoid downloading and unzipping Cypress binary
# which saves time - we do not plan to run Cypress tests
# in this lint workflow, see
# https://on.cypress.io/installing
CYPRESS_INSTALL_BINARY: 0

By setting the environment variable CYPRESS_INSTALL_BINARY: 0 during the NPM installation step, we can skip the Cypress binary download. All our complicated workflows stay (almost) the same, and the caching during those workflows never sees any delays.

Let's move to the next task: we do want to run our Cypress tests after the deploy. Here is the simplest trick I know of: use cypress/included:x.y.z Docker image, check out the source code to bring the specs to the local container, and call cypress run to execute all tests. For example, after Vercel deploys the application, it triggers the GitHub deploy event. If the deploy is successful, we can grab the deployed URL and run Cypress tests against it.

.github/workflows/deploy.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
name: deploy
on: deployment_status
jobs:
tests-after-deploy:
# only runs this job on successful deploy
# https://glebbahmutov.com/blog/develop-preview-test/
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-20.04
# Use a Docker image with Cypress installed globally
# https://github.com/cypress-io/github-action#docker-image
# https://github.com/cypress-io/cypress-docker-images
container: cypress/included:8.3.1
steps:
# see if there is GITHUB_REF
- name: Check GITHUB_REF
run: |
echo "GITHUB_REF is ${GITHUB_REF}"
# to compute the branch name for the deployed commit
# need to grab the repo
- name: Checkout
uses: actions/checkout@v2
with:
fetch-depth: 0

# Notice that we do not install any dependencies in this job
# Our Cypress tests do not use any plugins, so we just
# need to invoke the global Cypress from the Docker image

- name: Run tests
run: |
echo "Vercel unique deployment URL is ${{ github.event.deployment_status.target_url }}"

echo "Deployed commit ${{ github.sha }}"
export BRANCH_NAME=$(git show -s --pretty=%D HEAD | tr -s ',' '\n' | sed 's/^ //' | grep -e 'origin/' | head -1 | sed 's/\origin\///g')
echo "Deployed branch ${BRANCH_NAME}"

export CYPRESS_baseUrl=${{ github.event.deployment_status.target_url }}
cypress run

You can see the tests running at bahmutov/todomvc-vercel/actions.

Running tests using cypress/included image

The cypress/included:x.y.z container has the Chrome browser installed. You can see what is inside using the cypress info command.

1
2
3
4
5
6
7
# show the info about the browsers
# already installed in the Docker image
cypress info
# one can use CYPRESS_ environment variables
# or --config ... arguments to configure Cypress values
# https://on.cypress.io/configuration
cypress run --config baseUrl=${{ github.event.deployment_status.target_url }}

cypress info command output when running using cypress/included Docker image

Nice - notice how fast the image executes, since nothing needs to be installed. On your CI, make sure to cache cypress/included:x.y.z images to avoid pulling them from Docker Hub on each run.

Bonus: installing just Cypress plugins

After a while, you might add some Cypress plugins to your testing pipeline. Thus you would need to install them before running tests in the cypress/included container. Just list the plugins to install explicitly.

1
2
3
4
5
6
# if we did not _any_ Cypress plugins, we could skip the
# NPM install step completely. As we are using the "cypress-grep"
# plugin, we need to install it. But we do not need to install
# the heavy modules like Cypress itself
- name: Install Cypress plugins
run: npm i -D cypress-grep

See the pull request #1. Of course, at this point you might set up NPM module caching just for the plugins.