Assume you have a large or mid-level organization where a repository is shared by multiple developers and they often push the code during a day. Let’s say, a developer was building a feature and was coding in a class/module that was being called by ten other places, coded by other developer(s). Let’s also assume that he has completed his feature, tested it and it’s ready for production and it is pushed to the repository and deployed. But he might not be aware of the fact that he has changed the code that was being called by several other modules, and now, let’s say if not all, 2 to 3 of the places are broken and no one knows about it until end-user hits the problem, report it, then the developers fix the code for those 2 to 3 places, this might end up causing problems in other places where it was being called, and you end up in total chaos and it also multiplies the cost of development.
This is the best scenario where Continuous Integration comes in. It is a practice in software development that is being followed in almost all modern applications because of its numerous advantages that include.
- Build a solid and working foundation.
- Catch the issues before it’s too late.
- Decrease the debugging time and increase productivity.
- Integration time is reduced and hence an increase in productive time.
- Avoids last-minute chaos at release dates, when everyone tries to check in their slightly incompatible versions.
- Reduces the development and integration cost.
We’re always developing large scale Ruby on Rails applications where developers push the code quite often and before we actually start development, we work on an integration/deployment script so that we have a solid foundation and a check on code even on the first push. This blog will focus on the integration script for a Ruby on Rails application and the Github as the platform. So I am taking the liberty of assuming that you have.
- A Github account.
- Knowledge of Git.
- A Ruby on Rails application(You might also clone it from here).
- Knowledge of Rspec(Unit testing gem used in almost all applications).
- Rubocop(Code refactoring gem).
That’s enough talking, now start working. Let’s go ahead and create main.yml(you can name it anything with yml extension) in project/.github/workflows directory.
In your .github/workflows/main.yml let’s first put this code.
1
2
3
name: CI
on: [push, pull_request]
Here we are not doing much work, just telling our action that we want to run the CI when we push the code and also when a pull request is created.
Let’s now tell or action that we want to run our job on latest version of Ubuntu that Github has.
1
2
3
4
5
6
7
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
Configure Postgres and Redis Server as our Services.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
# Similar to docker-compose.yml but not the same, 🤷♂️
services:
postgres:
image: postgres:11.6-alpine
ports: ['5432:5432']
# needed because the postgres container does not provide a healthcheck
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports: ['6379:6379']
options: --entrypoint redis-server
Now it’s time to do Ruby on Rails stuff in our action and for that, I had do consult GoRails and I always use their script for that. Let’s set it up. This script is for Rails 6 application and it was a bit tricky as for now, we don’t have much in Rails 6 because many applications are running on Rails 5. So here it is.
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
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
# Similar to docker-compose.yml but not the same, 🤷♂️
services:
postgres:
image: postgres:11.6-alpine
ports: ['5432:5432']
# needed because the postgres container does not provide a healthcheck
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports: ['6379:6379']
options: --entrypoint redis-server
steps:
- uses: actions/checkout@v2
- name: Setup Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: 2.7.x
- name: Install required apt packages
run: |
sudo apt-get -y install libpq-dev
- name: Setup cache key and directory for gems cache
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-use-ruby-${{ hashFiles('**/Gemfile.lock') }}
- name: Read Node.js version to install from `.nvmrc`
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
id: nvm
- name: Install required Node.js version
uses: actions/setup-node@v1
with:
node-version: "${{ steps.nvm.outputs.NVMRC }}"
- name: Get Yarn cache directory path
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Setup cache key and directory for node_modules cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Bundle install
run: |
gem install bundler:2.1.4
bundle install --jobs 4 --retry 3
- name: Yarn install
run: yarn --frozen-lockfile
At this point, if you don’t have Rspec and Rubocop included in your gems, let’s add them.
1
2
3
4
5
6
group :development, :test do
# Call 'byebug' anywhere in the code to stop execution and get a debugger console
gem 'byebug', platforms: %i[mri mingw x64_mingw]
gem 'rspec-rails'
gem 'rubocop'
end
Configure Rspec here and Consult Rubocop here, they have good documentation.
Add some models and controller and basic functionality to your app, well you can always clone it here. Write tests against them and then update your integration script like below.
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
name: CI
on: [push, pull_request]
jobs:
build:
runs-on: ubuntu-latest
# Similar to docker-compose.yml but not the same, 🤷♂️
services:
postgres:
image: postgres:11.6-alpine
ports: ['5432:5432']
# needed because the postgres container does not provide a healthcheck
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports: ['6379:6379']
options: --entrypoint redis-server
steps:
- uses: actions/checkout@v2
- name: Setup Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: 2.7.x
- name: Install required apt packages
run: |
sudo apt-get -y install libpq-dev
- name: Setup cache key and directory for gems cache
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-use-ruby-${{ hashFiles('**/Gemfile.lock') }}
- name: Read Node.js version to install from `.nvmrc`
run: echo "##[set-output name=NVMRC;]$(cat .nvmrc)"
id: nvm
- name: Install required Node.js version
uses: actions/setup-node@v1
with:
node-version: "${{ steps.nvm.outputs.NVMRC }}"
- name: Get Yarn cache directory path
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Setup cache key and directory for node_modules cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- name: Bundle install
run: |
gem install bundler:2.1.4
bundle install --jobs 4 --retry 3
- name: Yarn install
run: yarn --frozen-lockfile
- name: Rails test
env: # Or as an environment variable
DATABASE_URL: postgres://postgres:@localhost:5432/test
REDIS_URL: redis://localhost:6379/0
RAILS_ENV: test
RAILS_MASTER_KEY: ${{ secrets.RAILS_MASTER_KEY }}
run: |
bundle exec rake db:drop db:create db:schema:load
bundle exec rake db:migrate db:seed
bundle exec rspec
- name: Rubocop
run: |
bundle exec rubocop