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

Stoyan Delev
Jul 7 · 5 min read

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

Most of my projects are React based, so for this example, I will use React and Create-React-App ( CRA for short ), however, it works with any modern framework. CRA comes with eslint, jest which I will use for linting and unit testing, on top of that we need to install Cypress for end-to-end tests. For installing CRA and Cypress check their documentation.

1: Configure Firebase

The reason I choose firebase is that its super simple to work and provides a lot of benefits out of the box: http2, CDN, gzip, SSL, h2 push and has an easy way to reverse deployment.
In order to setup firebase just install firebase-tools and run firebase init, you need to select “hosting” on the second step.

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

All gitlab configurations regarding CI/CD are placed in .gitlab-ci.yml file. In gitlab CI you have stages, and every stage has one or many jobs. Our stages are: install, build, quality, deploy and audit, they are run one after another.

  • 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

In install stage, we run npm install and put that into artifacts (artifacts is a way “save” content and transfer it between stages, in our case we keep npm_modules for next stages )

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

2.2: Build

In build stage, as the name says build our code and also put the output into artifacts ( build folder ).

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

2.3: Quality

In that step linters and tests are running, there are 3 different jobs for that.

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

That part is quite easy once you setup the firebase.
Before writing our deployment job we need to generate firebase token and configure gitlab to use it.
run firebase login:ci and we will have the token, afterwards and add the token: Gitlab → Your Project → Settings → CI/CD → Environment variables as “FIREBASE_TOKEN

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

After deployment is done, I love to run Lighthouse and collect the stats which can be stored as artifacts.
Here is the example job:

.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.

Stoyan Delev

Written by

web developer obsessed by web performance