Using GitLab to build, test and deploy modern front end applications

Stoyan Delev
Jul 7, 2019 · 5 min read
Image for post
Image for post

Disclaimer: I don’t claim that is the right way of doing CI/CD, it’s the way that works for me.

I’ve been using GitLab and Firebase separately as tools for around 4 years and after I struggling with integrating deployment into my development process, finally around 1 year ago, I decided that its time to combine the power of them: easily manageable gitlab pipelines and simplicity of firebase hosting.

The whole process ( pipeline ) looks like that: Install dependencies -> Build code -> Run tests and linters -> Deploy to Firebase -> Make audit with Lighthouse

0: The project


1: Configure Firebase

There are 2 ways to config different environments ( production/staging/test ) in firebase: using different project per env or one project with multiple sites. I personally prefer the second one. Here is the official documentation of how to do that. Once you are done, your firebase configs should look similar to .firebaserc, firebase.json


2: Setup GitLab

  • install — install all dependencies from NPM
  • build — build the code
  • quality — run eslint, unit tests with Jest and end-2-end test with Cypress
  • deploy — deploy the code to firebase
  • audit — Run lighthouse against deployed code

Jobs are fundamentals of gitlab CI, every job should have elements with an arbitrary name and must contain at least the script clause, and in our case includes also the stage.

linting:
stage: quality
script:
- npm run lint

2.1: Install step

install:
stage: install
script:
- npm install
artifacts:
name: "artifacts"
untracked: true
expire_in: 30 mins
paths:
- .npm/
- node_modules/

2.2: Build

build:
stage: build
script:
- CI=false npm run build
artifacts:
paths:
- build
expire_in: 30 mins
dependencies:
- install

2.3: Quality

2.3.1: Linting
That is the simplest job, it runs npm run lint, which is a script in package.json “npx eslint ‘src/**/*.{js,jsx}’

2.3.2: Unit tests
Running Jest with coverage mode ( Here I use regex to parse output so it can be shown in merge request )

test:unit:
stage: quality
script:
- npm run test:coverage
dependencies:
- install
coverage: /All files[^|]*\|[^|]*\s+([\d\.]+)/

2.3.3: End-2-end tests
As mentioned earlier for e2e we will use cypress ( I must say it again, that is wonderfull tool !!! ) Running it in CI requires more complex setup: you need cypress specific docker image, also to be able to run a web server in that docker image, for that purpose I use few packages: sirv, start-server-and-test.
Here are the steps how the process looks: Run a web server with already generated code from build step ( serv module ); wait till server is running on a specific port, and finally run cypress against that server ( all those are scripts in package.json )

"e2e": "CYPRESS_baseUrl=http://localhost:3333 npx cypress run",
"e2e:ci": "npx start-server-and-test serve:e2e http://localhost:3333 e2e",
"serve:e2e": "node_modules/.bin/sirv build --quiet --single --port 3333",

so our gitlab-ci config looks like:

test:e2e:
stage: quality
image: cypress/browsers:chrome69
dependencies:
- install
- build
script:
- npm run e2e:ci
artifacts:
paths:
- cypress/screenshots
- cypress/videos
expire_in: 1 day

Notice that we saved screenshots and videos from failed test into artifacts, so can be downloaded and reviewed later.


2.4: Deploy

Image for post
Image for post

Since my project has 3 environments ( production/alpha/beta ) I need to write a reusable deployment job.

.deploy:
stage: deploy
before_script:
- npm install -g firebase-tools
- (if [ -d "build" ]; then echo ok; else exit "no build folder, try to run pipeline again"; fi);
script:
- firebase deploy --token $FIREBASE_TOKEN --non-interactive --only hosting:$ENV
when: manual

Here we have before_script attribute in which install firebase tools and check if “build” folder exists. We keep build folder only for 30 min in artifacts so might be that it’s gone. In the script section: we do the real deployment using firebase token and $ENV variable ( we pass that from another job )
when” section specify how we want to run that job, in our case is manual, but can be automatic as well.

deploy_to_prod:
environment:
name: prod
url: $PROD_URL
extends: .deploy
variables:
ENV: prod
only:
refs:
- master

And here is the deploy to production job, which extends our common .deploy one also pass ENV variable and with only attribute can specify that want to be executed only on master branch.
Deploy to alpha and beta are the same.


2.5: Audits

.lighthouse:
image: markhobson/node-chrome
stage: audit
before_script:
- npm i -g lighthouse
script:
- lighthouse --chrome-flags="--headless --no-sandbox" $LIGHTHOUSE_TEST_URL --output html --output-path ./report.html
artifacts:
paths:
- ./report.html
expire_in: 1 month
when: manual

That’s pretty much all, here are few caveats that you need to know:

  • Artifacts can take disk space, so set an expiration limit
  • Short expiration limit means sometimes you need to re-run the pipeline again if you want to deploy later in time.

The full code, including firebase, gitlab and project source can be found here.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch

Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore

Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store