---
title: "Immutable deploys and Cypress"
url: "https://www.cypress.io/blog/cypress-and-immutable-deploys"
description: "Continuous development pipeline can deploy hundreds of changes a day and give us peace of mind."
---

# Immutable deploys and Cypress

May 30, 2017

By Gleb Bahmutov

It is simple to run Cypress both locally and on CI. And it is simple to point your tests to a different server. If you are using immutable deploys, Cypress can be the ultimate “health check”. If the tests pass locally, then on CI, and then at a newly deployed environment - the chances are very high that you can point production DNS record at the new deployment and it will work.

Here is an example. Let us take a “stock” React [TodoMVC app](https://gitlab.com/bahmutov/todomvc?ref=cypress-io.ghost.io) and see all the moments we can run Cypress end to end tests against it.

```shell
git clone git@gitlab.com:bahmutov/todomvc.git
cd todomvc
npm install
npm start
```

Open `[http://localhost:8888](http://localhost:8888/?ref=cypress-io.ghost.io)` in the browser; the application should be running.

## Run end to end tests locally

I grabbed the existing end to end tests from the [Cypress example repo](https://github.com/cypress-io/cypress-example-todomvc?ref=cypress-io.ghost.io). The tests run very quickly inside the Cypress desktop application.

![todomvc-gif](https://cloud.githubusercontent.com/assets/1268976/12985445/ad168098-d0c0-11e5-94e7-2f2e619bae93.gif)

We can start practicing _test-driven development_ by keeping the Cypress application open and updating our test code - the tests will be rerun automatically. If we update the _application_ code, we need to reload the Cypress window - just press `Command-R`. I like concentrating on a feature by allowing only a block of tests to run with `describe.only` or even just a single test with `it.only`.

Seeing the UI update during the test is a powerful drug 😉, but we can run Cypress without the UI too. Assuming the TodoMVC application server is running we can just call `cypress run` to execute end to end tests.

Notice that the headless run _records a video_, so you will always know what has happened during the test execution. You can control video recording and compression using [Cypress options](https://on.cypress.io/configuration?ref=cypress-io.ghost.io#Videos).

Let us write a single command that starts the server, runs end to end tests and then closes the server. I am going to use the [npm-run-all](https://github.com/mysticatea/npm-run-all?ref=cypress-io.ghost.io#readme) module for this.

```shell
$ npm i -D npm-run-all
```

Then let us define a script command in `package.json`

```json
{
  "scripts": {
    "start": "http-server -p 8888 -c-1",
    "e2e": "cypress run",
    "test": "run-p --race start e2e"
  }
}
```

From now on, if you run `npm test` it will start the server, execute all end to end tests and then will close the server.

```shell
$ npm t

> todomvc@ test /Users/irinakous/git/todomvc
> run-p --race start e2e

Starting up http-server, serving ./
Available on:
  http:127.0.0.1:8888
  http:10.47.29.203:8888
Hit CTRL-C to stop the server

Started video recording: /todomvc/cypress/videos/im8kg.mp4
... test output ...
http-server stopped.
```

Perfect.

## Run tests before pushing code

We can execute E2E tests on [each commit](https://glebbahmutov.com/blog/never-break-master-by-accident/?ref=cypress-io.ghost.io) and before pushing the code to the master repo, just to make sure [the master is never broken](https://glebbahmutov.com/blog/never-break-remote-master-again/?ref=cypress-io.ghost.io). In this case, I will use [pre-git](https://github.com/bahmutov/pre-git?ref=cypress-io.ghost.io#readme) to run the tests before pushing my changes to the origin.

```shell
$ npm install -D pre-git
```

```json
{
  "devDependencies": {
    "npm-run-all": "^4.0.2",
    "pre-git": "^3.14.0"
  },
  "config": {
    "pre-git": {
      "commit-msg": "simple",
      "pre-commit": [],
      "pre-push": ["npm test"],
      "post-commit": [],
      "post-checkout": [],
      "post-merge": []
    }
  }
}
```

I set `pre-push` hook commands to be just `npm test`. Let me commit and push this code to the origin.

```shell
$ git commit -m "feat(ci): run tests on pre-push"
running bin/pre-commit.js script
pre-commit Nothing the hook needs to do. Bailing out.
post-commit Nothing the hook needs to do. Bailing out.
[master acc83af] feat(ci): run tests on pre-push
 1 file changed, 14 insertions(+), 2 deletions(-)
```

Nothing extraordinary happens during commit, I am just following [simple commit message format](https://github.com/bahmutov/simple-commit-message?ref=cypress-io.ghost.io#valid-commit-messages). Let us push the code to the remote server.

```shell
$ git push
pre-push Detected files in diff: 1
executing task "npm test"

> todomvc@ test /Users/irinakous/git/todomvc
> run-p --race start e2e
> todomvc@ e2e /Users/irinakous/git/todomvc
> cypress run
> todomvc@ start /Users/irinakous/git/todomvc
> http-server -p 8888 -c-1

Starting up http-server, serving ./
Available on:
  http:127.0.0.1:8888
  http:10.2.60.121:8888
Hit CTRL-C to stop the server

Started video recording: /Users/irinakous/git/todomvc/cypress/videos/478b6.mp4

  (Tests Starting)
... test output ...
http-server stopped.
Counting objects: 3, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (3/3), done.
Writing objects: 100% (3/3), 439 bytes | 0 bytes/s, done.
Total 3 (delta 2), reused 0 (delta 0)
To gitlab.com:bahmutov/todomvc.git
   c378c1d..acc83af  master -> master
```

Great, the E2E tests were executed before our code was pushed to the remote repository. This sanity test pass is one less thing we have to worry about now. If I am really in a rush, and willing to live dangerously I can skip Git `pre-push` hook using `--no-verify` CLI option.

## Run tests on CI

Executing Cypress E2E tests on CI is as simple as running unit tests. As long as your CI has the [right dependencies](https://on.cypress.io/continuous-integration?ref=cypress-io.ghost.io), you are good to go. Our customers are successfully using Cypress on all major CI systems: [Jenkins](https://jenkins.io/?ref=cypress-io.ghost.io), [Travis](https://travis-ci.com/?ref=cypress-io.ghost.io), [Circle](https://circleci.com/?ref=cypress-io.ghost.io) and others. Recently, we also built [Cypress Docker image](https://hub.docker.com/r/cypress/base/?ref=cypress-io.ghost.io) that you can use in a containerized CI environment.

But I am a Cypress insider, so there are some perks 😋 like building a Docker image with dependencies _and Cypress pre-installed_. While we are still trying to figure out the best long-term solution, I am using [cypress/internal:cy-0.19.2](https://hub.docker.com/r/cypress/internal/tags/?ref=cypress-io.ghost.io) image. This image makes it so simple to run E2E tests on [GitLabCI for example](https://gitlab.com/cypress-io/cypress-example-docker-gitlab?ref=cypress-io.ghost.io), that it is almost unnoticeable. Here is my `.gitlab-ci.yml` file, which mirrors my local development flow 100%.

```yaml
image: cypress/internal:cy-0.19.2
cypress-e2e:
  stage: test
  script:
    - npm install
    - npm test
```

You can see the CI running the tests on GitLab [todomvc/builds](https://gitlab.com/bahmutov/todomvc/builds?ref=cypress-io.ghost.io). For each test run, it

1.  Starts the local server
2.  Runs the E2E tests against the local server

The system is self-contained; the CI runs both the server and the tests.

## Immutable deploy

Running TodoMVC server locally is nice, but we want to share it with the world. The simplest way to do this that I know of is to serve it via [Zeit.co now](https://zeit.co/now?ref=cypress-io.ghost.io) tool. For example, from the local terminal, I can just invoke `now` and get my server up and running!

How cool is that? Notice the unique URL created during deploy - each `now` invocation is creating a brand new site; the old deployments are unaffected. This is called _immutable deployment_. But is the application working as expected after deployment? Let us test it using Cypress of course.

We are telling Cypress test runner what to load via the `baseUrl` option in the `cypress.json` file. By default it is matching our local server address: `[http://localhost:8888](http://localhost:8888/?ref=cypress-io.ghost.io)`. We can [override this setting](https://on.cypress.io/configuration?ref=cypress-io.ghost.io#Overriding-Options) when starting the desktop application or running the tests headlessly. Either `cypress run --config baseUrl=...` or `CYPRESS_baseUrl=... cypress run` would work in this case. Let us try it out.

```shell
$ cypress run --config baseUrl=https://todomvc-jodtvxbtuh.now.sh

Started video recording: /Users/irinakous/git/todomvc/cypress/videos/xjl7n.mp4
... test output ...
```

The tests finish and if we open the generated video file `cypress/videos/xjl7n.mp4` we can confirm that Cypress tested the deployed site. Perfect, our immutable deploy is working according to spec.

If we can deploy and test our application from the local terminal, we should be able to deploy and test the same way on CI. And it almost works, except `now` tool fails inside the very limited Docker environment.

```shell
$ npm install -g now
/usr/local/bin/now -> /usr/local/lib/node_modules/now/download/dist/now

> now@6.1.1 postinstall /usr/local/lib/node_modules/now
> node download/install.js

> Retrieving the latest CLI version...
> For the sources, check out: https://github.com/zeit/now-cli

/usr/local/lib
`-- now@6.1.1
$ now

> Error! Unexpected error. Please try later. (Authentication error – stdin lacks setRawMode support)
```

Luckily, I have a work around that uses [Zeit API](https://zeit.co/api?ref=cypress-io.ghost.io) to drive the deployment. The tool is called [now-pipeline](https://github.com/bahmutov/now-pipeline?ref=cypress-io.ghost.io) and just needs `NOW_TOKEN` environment variable to work. You can make a new token at [https://zeit.co/account/tokens](https://zeit.co/account/tokens?ref=cypress-io.ghost.io); it is a good idea to give it a very descriptive name, like `<repo url> - gitlab ci deploy` string.

`now-pipeline` will run the deploy for your project, then will invoke a test command against the newly created url. To avoid variable interpolation problems, we can tell `now-pipeline` to pass the newly created url using environment variable named `CYPRESS_baseUrl`. Our CI job definition (I am using `deploy` stage for this) looks like this:

```yaml
deploy-and-test:
  stage: deploy
  script:
    # we now longer need to install package.json dependencies
    - npm install -g now-pipeline@1.8.0
    # we do not need to start local server
    # since it is already running in the cloud
    - now-pipeline --as CYPRESS_baseUrl --test 'cypress run'
  artifacts:
    expire_in: 1 week
    paths:
    - cypress/screenshots
    - cypress/videos
```

The job runs and successfully tests a new unique deploy every time it runs.

```text
$ now-pipeline --as CYPRESS_baseUrl --test 'cypress run'
READY todomvc-swtllwnzbh.now.sh limit 600 seconds
deployed to url https://todomvc-swtllwnzbh.now.sh
testing url https://todomvc-swtllwnzbh.now.sh
passing it as env variable CYPRESS_baseUrl
test command "cypress run"
running "cypress" with extra env keys [ 'CYPRESS_baseUrl' ]
Added this project: /builds/bahmutov/todomvc
... test output ...
deployed url https://todomvc-swtllwnzbh.now.sh is working
found 0 deploy(s) with aliases
there is no existing alias
will skip updating alias to https://todomvc-swtllwnzbh.now.sh
Uploading artifacts...
WARNING: cypress/screenshots: no matching files
cypress/videos: found 2 matching files
Uploading artifacts to coordinator... ok
Job succeeded
```

We just ran our E2E tests against freshly deployed instance, verified that it works, yet have not affected the existing production deployments in any way.

## Switch production DNS

Pointing a production URL / DNS record at a fresh and tested deployment is the last step we are going to do. Just add `--alias todomvc` parameter to the `now-pipeline` command to switch the `todomvc.now.sh` (or any custom domain controlled by [Zeit World](https://zeit.co/world?ref=cypress-io.ghost.io)) to point at the new deployment **if and only if tests passed**. We can also prune old deploys after switching the alias.

```yaml
deploy-and-test:
  stage: deploy
  script:
    - npm install -g now-pipeline@1.8.0
    - now-pipeline --as CYPRESS_baseUrl --alias todomvc --test 'cypress run'
    - now-pipeline-prune
```

Finally, we can verify that the application is still working after changing the DNS record. Who knows, maybe we had some hard-coded URLs or domain white lists? Just run Cypress tests against the production URL! By now you know how to do this; we have been using the one and only `cypress run` command every time, and just gave it different base urls. I will run the `test-production` job during `verify` stage which executes _after_ `deploy` stage, because that is the order I listed them in the [“stages” list](https://docs.gitlab.com/ce/ci/yaml/README.html?ref=cypress-io.ghost.io#stages), you can see the complete [.gitlab-ci.yml file](https://gitlab.com/bahmutov/todomvc/blob/master/.gitlab-ci.yml?ref=cypress-io.ghost.io) for reference.

```yaml
image: cypress/internal:cy-0.19.2
stages:
  - test
  - deploy
  - verify
# cypress-e2e job as before
# deploy-and-test as before
test-production:
  stage: verify
  script:
    - cypress run --config baseUrl=https://todomvc.now.sh
  artifacts:
    expire_in: 1 week
    paths:
    - cypress/screenshots
    - cypress/videos
```

Perfect, let us try it one more time to confirm. The entire pipeline runs … and passes.

## Final thoughts

Immutable deploys bring peace of mind to continuous deployment, at least for simple cases without shared dependencies. Trying to deploy new code does not affect the existing production system, and we can fully test and verify the fresh version before switching users to it. Zeit.co makes immutable deploys _incredibly_ simple and fast. Our tool, Cypress, makes it similarly easy to verify that the entire web application is working correctly. Taken together, Zeit and Cypress verify your code at multiple check points. To review, we run E2E tests:

-   Locally as we are working on new features, or refactoring existing code.
-   Locally before pushing new code to the remote repository to avoid accidentally sending broken or incomplete code.
-   On CI using local isolated server.
-   After deploying new code to its own unique cloud instance.
-   After switching production DNS to point at the latest deployed instance.

Of course, I just showed different times when we could run E2E; it might seem like an overkill. I would suggest at least running tests when the code crosses a boundary: from local machine to CI, and from CI to production. With this approach, our continuous deployment pipeline can deploy hundreds of changes per day, yet giving us peace of mind. Easy win.

### Links

-   [TodoMVC example project](https://gitlab.com/bahmutov/todomvc?ref=cypress-io.ghost.io)
-   [Zeit.co Now tool](https://zeit.co/now?ref=cypress-io.ghost.io)
-   [Cypress CI docs](https://on.cypress.io/continuous-integration?ref=cypress-io.ghost.io)
-   [Cypress Changelog](https://github.com/cypress-io/cypress/wiki/changelog?ref=cypress-io.ghost.io)
-   [now-pipeline](https://github.com/bahmutov/now-pipeline?ref=cypress-io.ghost.io) deployment tool

**Happy testing!**