Set Up GitLab CI for Rails Applications

It is hard to go back, once you integrate CI/CD flow into your workflow. Luckily, tools like GitLab are great, as they provide not only a nice GIT hosting platform, but a robust continues integration and delivery platform.

Let’s setup our .gitlab-ci.yml file, so it meets our needs.

First steps

We will use the official Ruby docker image as a start and add PostgreSQL service. It is good idea to cache our dependencies as this will remove the need to download them if there are no changes.

image: ruby:2.4.3

cache:
paths:
- vendor/bundle
- node_modules

services:
- postgres:10.1

variables:
BUNDLE_PATH: vendor/bundle
DISABLE_SPRING: 1
DB_HOST: postgres

Before script

The before_script is the place, where you can put your setup steps that will run before every stage. Here we will put the installation of additional software that we need and the initial setup of the project, like running migration, compiling assets, etc.

before_script:
# Install node and some other deps
- curl -sL https://deb.nodesource.com/setup_8.x | bash -
- apt-get update -yq
- apt-get install -y apt-transport-https build-essential cmake nodejs python-software-properties software-properties-common unzip

# Install yarn
- wget -q -O - https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
- echo "deb https://dl.yarnpkg.com/debian/ stable main" > /etc/apt/sources.list.d/yarn.list
- apt-get update -yq
- apt-get install -y yarn
# Project setup
# Check if the dependencies are ok, if not install what is missing
- bundle check || bundle install --jobs $(nproc)
- yarn install
# database.yml.ci file contains the configurations for the CI
# server, so let's copy to the configuration file
- cp config/database.yml.ci config/database.yml
- bundle exec rails db:create RAILS_ENV=test
- bundle exec rails db:schema:load RAILS_ENV=test
- bundle exec webpack

Take a note that we copy our CI specific database configuration file, which contains the correct values for the runner. It should have the following content:

default: &default
adapter: postgresql
encoding: unicode
username: postgres
password: postgres
host: <%= ENV['DB_HOST'] %>
pool: 5
database: ci_db

Defining the stages

The stages block of the config file defines all the stages of the build process of the app. We, at Evermore, usually go with three: test, lint and deploy.

stages:
- test
- lint
- deploy

Test stage

GitLab allows to have as many task per stage as you like, which makes perfect sense for our unit and system tests. But to be able to run tests using headless Chrome, we need to install it and install chromedriver as well. As this is kinda expensive task, we only do it when we actually need it.

Tests:
stage: test
script:
- bundle exec rails test -d
System Tests:
stage: test
script:
- ./bin/setup_chrome
- bundle exec rails test:system

Here is the ./bin/setup_chrome file

#!/bin/bash
set -e
# Install Chrome
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add -
echo "deb http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google.list
apt-get update -yqqq
apt-get install -y google-chrome-stable > /dev/null 2>&1
sed -i 's/"$@"/--no-sandbox "$@"/g' /opt/google/chrome/google-chrome
# Install chromedriver
wget -O /tmp/chromedriver.zip http://chromedriver.storage.googleapis.com/2.35/chromedriver_linux64.zip
unzip /tmp/chromedriver.zip chromedriver -d /usr/bin/
rm /tmp/chromedriver.zip
chmod ugo+rx /usr/bin/chromedriver

Lint stage

At Evermore we have a coding style guide and one thing that we like to do is to automate the review process of lint violations. It is always a better idea if they come from a machine than a person :) To do this we use this awesome gem called pronto. It has various runners and rubocop is one of them.

Pronto:
stage: lint
allow_failure: true
except:
- master
script:
- bundle exec pronto run -f gitlab -c origin/master

As this is not curtail part of our build, we allow failures for this task. And, of course, there is no need to run this task on the master branch, because we want to lint only the change in a pull (merge) request.

Deploy stage

Deploy stage can be different depending on the needs. We usually use Heroku and have two environments — staging and production. We deploy to staging on successful build on branches (pull requests), so it is easy to review and deploy the master branch to production.

The easiest way to deploy your app to Heroku, is using the dpl package. You just need to install it and setup an API key. You will need to add it to your secret variables in GitLab.

Deploy Production:
stage: deploy
retry: 2
only:
- master
script:
- ./bin/setup_heroku
- dpl --provider=heroku --app=awesome-app --api-key=$HEROKU_API_KEY
- heroku run rake db:migrate --exit-code --app awesome-app
Deploy Staging:
stage: deploy
allow_failure: true
retry: 2
except:
- master
script:
- ./bin/setup_heroku
- dpl --provider=heroku --app=awesome-app-staging --api-key=$HEROKU_API_KEY
- heroku run rake db:migrate --exit-code --app awesome-app-staging
- heroku run rake db:seed --exit-code --app awesome-app-staging

Here is the ./bin/setup_heroku file

#!/bin/bash
set -e
apt-get update -yq
apt-get install apt-transport-https software-properties-common python-software-properties -y
add-apt-repository "deb https://cli-assets.heroku.com/branches/stable/apt ./"
curl -L https://cli-assets.heroku.com/apt/release.key | apt-key add -
apt-get update -yq
apt-get install heroku -y
gem install dpl

We install Heroku’s CLI tool, so we can run tasks like migrations, seeding the database and so on.

And we are done

Of course, we can go a step further and create a custom docker image. This way we will cache some of the setup steps and speed up the builds. Strangely enough, I was not able to find an image that fits my needs for this specific project, so I created one — https://hub.docker.com/r/mupkoo/rails-ci/. Give it a try if you like.

Here is the full script. Tweak it and make it yours. Good luck!