Using Cypress in GitLab CI

Cypress is a nice way to do automated tests for applications that don't come with a testing framework. Since I stumbled upon some issues getting it to work with our CI (Gitlab CI) I though to contribute with some documentation on how to get it working.

Using Cypress in GitLab CI

What you will learn from this how-to

  • Creating Gitlab CI configuration
  • Define variables in Gitlab CI
  • Run cypress tests in Gitlab CI
  • Fix cypress 'sad face' crash
  • In the second part of this post: Automatic deploys with GitLab CI

A bit of history

I have written something similar a while back on my personal blog - Testing websites with cypress and GitLab CI - Part2

The new version described here is updated and works even better with Gitlab CI and it should be easy for you to adapt it to some other CI.

This post also does not cover the installation of cypress, I have written about this in my Testing websites with cypress and GitLab CI - Part1 post and there are a lot of posts about that out there.

Move cypress to a separate docker image

I needed cypress with ruby 2.3 to get wagon from LocomotiveCMS working, to keep my project's Dockerfile and .gitlab-ci.yml clean I decided to create a separate docker image for cypress and ruby based on the official cypress Dockerfile you can find the resulting Dockerfile in our Gitlab repo.

I will spare you the content of the Dockerfile and you can find the resulting image on Dockerhub under lcxat/cypress-ruby

The main takeaway is that it bases on ruby:2.3.8 and node:12.1.0  after which it installs cypress on top of it. Update it to the ruby version you need if 2.3.8 does not suit you.

Creating Gitlab CI configuration

The Building stage

Let's start with the building stage in gitlab CI and work from there to a complete .gitlab-ci.yml

image: lcxat/cypress-ruby

stages:
  - build

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - vendor/ruby

build:
  stage: build
  script:
    - gem install bundler
    - bundle install -j $(nproc) --path vendor
    - sed -e s/APIKEY/$LOCO_API/g config/deploy-testing.yml > config/deploy.yml
    - bundle exec wagon deploy testing -v -d

Let's analyze what's going on here, I'm starting off the previously mentioned Docker image lcxat/cypress-ruby. To speed things up I have added a cache setting in the .gitlab-ci.yml and also told bundler to install the gems in the vendor path which is cached.

Then there is this sed part, a little bit of wagon background: The wagon tool needs an API key to be able to deploy to our testing environment, you could start wagon locally on the gitlab CI and test it like that, but the problem is that wagon does not behave 100% the same as after deploying the page, that's why I went with a real deploy to a testing page on a live server. Now since you don't want any API keys in the git repo, I use sed and Gitlab's variable settings to fix this and create a valid deploy.yml file.

Go in your gitlab under settings -> CI/CD and add a new variable called LOCO_API with your API Key as the value.

Gitlab Variable settings

My deploy.yml looks something like this

testing:
  host: cms.example.at
  handle: site-autotest
  email: livadaru+cypress@example.com
  api_key: APIKEY

When sed runs it replaces APIKEY with the value provided in Gitlab's variables setting mentioned above.

If you now commit and push to gitlab you should have a successful pipeline run for the building stage. Great, now let's go to the testing part.

Testing with cypress

Add a test stage to your .gitlab-ci.yml file like this:

cypress:
  stage: test
  script:
    - sed -i -e s#http://localhost:3333/#$CMS_URL#g cypress.json
    - npm test
  artifacts:
    paths:
      - /builds/foo/bar/cypress/videos/

and extend your stages definition

stages:
  - build
  - test

ok, let's dig into this. There is again a sed command the edits the cypress.json in place -i

I'm doing this since I also want to be able to test locally, that's why the cypress.json looks like this

{
  "baseUrl": "http://localhost:3333/",
  "viewportWidth": 1000,
  "viewportHeight": 1000
}

This allows me to test locally but of course, will fail when testing on CI since wagon did not start the server locally. Once again you must define a gitlab variable pointing to where the test page is deployed and name the variable CMS_URL

Gitlab variable for cypress baseurl

For the npm test to work, you need to have a proper setup package.json something like this, please note "private": true, to prevent npm from publishing your repo.

{
  "name": "i2c",
  "private": true,
  "version": "0.0.1",
  "description": "Demo App",
  "main": "index.js",
  "scripts": {
    "test": "/usr/local/bin/cypress run"
  },
  "repository": {
    "type": "git",
    "url": "git@git.lcx.at:foo/bar.git"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
    "cypress": "^3.3.2"
  }
}
package.json

Ok, now commit and push to gitlab and wait for the test to start ... and most likely fail.

We detected that the Chromium Renderer process just crashed.

This is the equivalent to seeing the 'sad face' when Chrome dies.

This can happen for a number of different reasons:

- You wrote an endless loop and you must fix your own code
- There is a memory leak in Cypress (unlikely but possible)
- You are running Docker (there is an easy fix for this: see link below)
- You are running lots of tests on a memory intense application
- You are running in a memory starved VM environment
- There are problems with your GPU / GPU drivers
- There are browser bugs in Chromium

You can learn more including how to fix Docker here:

https://on.cypress.io/renderer-process-crashed

or as screenshot

Chromium Renderer process just crashed

Well, this is a bummer :(

What's happening here is that the Electron app runs out of shared memory and crashes, the cypress link tells you if you are running in docker, that you should start it with --ipc=host but that won't work in my case. Gitlab runner is running as a docker container but I would not like to start all containers with this setting. Assuming you are running gitlab-runner in docker, edit your config.toml and add this to your volume setting "/dev/shm:/dev/shm" which will then look something like this:

volumes = ["/opt/docker/gitlab-runner/cache:/cache","/dev/shm:/dev/shm"] you don't really need the cache part, but I wanted to see what gitlab-ci is doing with the cache so I added that part as well.

Stop & Restart your gitlab-runner to pick up the changes and rerun the failed job, it should now pass with flying colors and no more Chromium crashes.

Some notes about the cache

I have been playing with several cache settings and here are some things that might help when looking at what gitlab-ci does. If you look at the end of the build, you must see that gitlab-ci has created the cache

Creating cache new-docker...
vendor/ruby: found 9813 matching files             
No URL provided, cache will be not uploaded to shared cache server. Cache will be stored only locally. 
Created cache

Don't freak when looking at the next stage where it says Removing vendor/

Fetching changes...
Reinitialized existing Git repository in /builds/foo/bar/.git/
Checking out ee8d93ed as new_docker...
Removing config/deploy.yml
Removing vendor/

This is not gitlab removing the cache but git clean! Don't worry, the cache will be back when needed, gitlab-ci takes care of that. Just make sure you run bundle on each stage where you need the gems and don't forget to add the --path vendor otherwise it will install all gems from scratch an not use the cache

In the second part - Automatic deploys with GitLab CI, I will take this a step further and configure gitlab to automatically deploy to staging and add a manual option to allow deploying to production with the click of a button.