Zeit Now GitHub app + Renovate app + Cypress tests = 💝

How Cypress, Zeit Now GitHub app and Renovate app play well together to give you well tested PRs and keep your dependencies up to date with zero effort.

Tested PR deploys

Recent news: Zeit.co has released a GitHub app for automatically deploying GitHub pull requests using Zeit Now tool. Previously, I have been a huge fan of testing my code on Now cloud using immutable deploys - read Immutable deploys and Cypress and try my helper tool now-pipeline. Now (excuse my pun), I wanted to see if I could run the same tests on each pull request without any custom tooling, just by using Zeit GitHub app.

So I made a tiny static repo bahmutov/tiny-blog and connected it to the "Now" GitHub application.

Now GitHub application

My example is a static site, which I configure using tiny-blog/now.json file

now.json
1
2
3
{
"type": "static"
}

I needed to add a Dockerfile to test and serve my site. Zeit Now can be used for Node deploys, but its full power lies in running any Docker-based project. So I have added a Dockerfile that runs end-to-end tests against a local site using Cypress.io test runner. The Dockerfile looks intimidating, but as I explain in Fast Tests, Tiny Docker Image blog post, it only looks complicated because I build two images - one to run end-to-end tests, and another to actually serve the static site in production. By using Docker multi-stage build feature we can keep the production image really really small - because it will NOT include tests or test dependencies. In summary, the Dockerfile looks like this

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
#
# Testing image
#
FROM cypress/base:10 as TEST
# dependencies will be installed only if the package files change
COPY package.json .
COPY package-lock.json .
# by setting CI environment variable we switch the Cypress install messages
# to small "started / finished" and avoid 1000s of lines of progress messages
# https://github.com/cypress-io/cypress/issues/1243
ENV CI=1
RUN npm ci
# tests will rerun if the "cypress" folder, "cypress.json" file or "public" folder
# has any changes
# copy tests
COPY cypress cypress
COPY cypress.json .
# copy what to test
COPY public public
#
# run e2e Cypress tests
#
RUN npm test

#
# Production image - without Cypress and node modules!
#
FROM busybox as PROD
COPY --from=TEST /app/public /public
# nothing to do - Zeit should take care of serving static content
# we would only need a command if we want to use this image locally

That is it - and the best part is that you can build this Docker image locally to make sure it is working! You should see end-to-end tests executing headlessly. Here is a typical test that Cypress will run

cypress/integration/spec.js
1
2
3
4
5
6
7
/// <reference types="cypress" />
describe('tiny blog', () => {
it('loads', () => {
cy.visit('localhost:5000')
cy.contains('h1', 'Tiny Blog')
})
})

And here is the output from Docker building the image and running tests on my Mac. I am passing an argument to force Docker to NOT cache intermediate step "RUN npm test", thus forcing the tests to run. Notice how most steps say "Using cache" - because Docker is really good at caching intermediate steps as layers!

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
$ docker build . --build-arg HOSTNAME=$(date +%s)
Sending build context to Docker daemon 1.966MB
Step 1/18 : FROM cypress/base:10 as TEST
---> 1613db8573fa
Step 2/18 : WORKDIR /app
---> Using cache
---> 3038f77e74b3
Step 3/18 : COPY package.json .
---> Using cache
---> e9ba57858ac0
Step 4/18 : COPY package-lock.json .
---> Using cache
---> 1d5d0f63cf76
Step 5/18 : ENV CI=1
---> Using cache
---> c677a011cf68
Step 6/18 : RUN npm ci
---> Using cache
---> f6a0137ee209
Step 7/18 : RUN npx cypress verify
---> Using cache
---> 37ebebd34694
Step 8/18 : COPY cypress cypress
---> Using cache
---> 613f68dfd956
Step 9/18 : COPY cypress.json .
---> Using cache
---> cd084d7f40c7
Step 10/18 : COPY public public
---> Using cache
---> 2a26ab11cb83
Step 11/18 : RUN ls -la
---> Using cache
---> 43078b798bcc
Step 12/18 : RUN ls -la public
---> Using cache
---> 640b6abba3ea
Step 13/18 : ARG HOSTNAME=1
---> Using cache
---> 5fa9ec65b8d3
Step 14/18 : RUN npm test
---> Running in 766257f65511

> [email protected] test /app
> start-test 5000 cy:run

starting server using command "npm run start"
and when url "http://localhost:5000" is responding
running tests using command "cy:run"

> [email protected] start /app
> serve public

INFO: Accepting connections at http://localhost:5000

> [email protected] cy:run /app
> cypress run


====================================================================================================

(Run Starting)

┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 3.0.3 │
│ Browser: Electron 59 (headless) │
│ Specs: 1 found (spec.js) │
└────────────────────────────────────────────────────────────────────────────────────────────────┘


────────────────────────────────────────────────────────────────────────────────────────────────────

Running: spec.js... (1 of 1)


tiny blog
✓ loads (88ms)
✓ loads 2nd time (105ms)


2 passing (1s)


(Results)

┌────────────────────────┐
│ Tests: 2 │
│ Passing: 2 │
│ Failing: 0 │
│ Pending: 0 │
│ Skipped: 0 │
│ Screenshots: 0 │
│ Video: true │
│ Duration: 1 second │
│ Spec Ran: spec.js │
└────────────────────────┘


(Video)

- Started processing: Compressing to 32 CRF
- Finished processing: /app/cypress/videos/spec.js.mp4 (0 seconds)


====================================================================================================

(Run Finished)


Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ✔ spec.js 00:01 2 2 - - - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
All specs passed! 00:01 2 2 - - -


INFO: Gracefully shutting down. Please wait...
Removing intermediate container 766257f65511
---> 669fd49b065a
Step 15/18 : FROM busybox as PROD
---> 22c2dd5ee85d
Step 16/18 : COPY --from=TEST /app/public /public
---> Using cache
---> 080af7d4cccd
Step 17/18 : RUN ls -la
---> Using cache
---> 8996a8bc6893
Step 18/18 : RUN du -sh
---> Using cache
---> 84d15f475947
Successfully built 84d15f475947

Ok, works locally, now push to GitHub! From now on (again, another pun), every pull request to the bahmutov/tiny-blog repo is built on the Zeit cloud, the tests are executed and if they pass, the site is deployed under a unique url. You can see an example pull request 3

Pull request 3

Under "details", Zeit adds a message with the link going to the deployment.

Deployed url in the details

During deploy, if you are fast enough (or at any time by going to the https://zeit.co/<username>/dashboard) you should see the same output, but from the Docker container being built by the Zeit cloud machines.

Zeit cloud build

The deployed site does not look like much, does it 😄

Deployed tiny-blog

But the really good thing about this setup is this - I have a well tested unique deploy for each pull request with minimum effort!

Dependencies

Well, if we can have one GitHub app, why not have two? I am a huuuge fan of RenovateApp - a fast and painless way to keep all my NPM dependencies up to date with minimum effort. What happens if we set both Zeit Now and Renovate apps?

Zeit Now and Renovate apps in tiny-blog

I really trust good tests and usually allow automerging minor and patch version changes (if tests pass), while manually reviewing major version upgrades. So my renovate.json looks like this

renovate.json
1
2
3
4
5
6
7
{
"extends": ["config:base"],
"automerge": true,
"major": {
"automerge": false
}
}

And here is the result - when Renovate notices a new version of an NPM dependency, it opens a pull request. Zeit Now runs the end-to-end tests using Cypress, and tells GitHub.

Pull requests opened by Renovate app

Each pull request like this one tells me what is about to happen, and has a unique deploy URL

Typical pull request opened by Renovate app

Renovate uses the test status (pass / fail) and upgrade type (major / minor / patch) to keep the PR open or automerge it after an hour or so. The commit log shows these auto-merged dependency upgrades.

Commits automatically merged by Renovate after tests passed

Boom! I got myself pain-free, almost-zero config way of testing code changes, seeing the deployed result and keeping dependencies up to date.