CI for Ruby on Rails: GitHub Actions vs. CircleCI
This is part of a three part series where I will walk you through setting up your CI suite with GitHub Actions, CircleCI, and then comparing which you may want to use if you are setting up continuous integration for your Rails app.
Part 1: GitHub Actions
1. Set the name for your action
name: Run Tests & Linters
2. Set what events should trigger the action to run
name: Run Tests & Linters
on:
pull_request:
branches:
- '*'
push:
branches:
- master
What this says is that this action will run anytime a pull_request is updated on any branch, and also on pushes to master.
3. Create your job, and choose what to run the action on
jobs:
build:
runs-on: ubuntu-latest
This tells our action we want to run the action on Ubuntu, and use the latest version GitHub has available, which is Ubuntu 18.04.
4. Define services
For a typical Rails app, you are probably using Redis for caching a tools like Sidekiq, and you also probably have a database. Defining services in your action allows us to use additional containers to run these types of tools.
services:
postgres: # The name of the service
image: postgres:11 # A docker image
env: # Environment variables you want to use inside the service
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports: ['5432:5432'] # The port that you can access the service on
options: >- # Options for the service, in this case we want to make sure that the Postgres container passes a health check
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis: # The name of the service
image: redis # A docker image
ports: ['6379:6379'] # The ports that you can access the service on
options: --entrypoint redis-server # Options for the service
5. Setup dependencies and checkout the branch
Here is where it got tricky for me. If you search for using GitHub actions with Rails, you will probably see something like this:
- uses: actions/checkout@v1
- name: Setup Ruby
uses: actions/setup-ruby@v1
with:
ruby-version: 2.6.x
- uses: borales/actions-yarn@v2.0.0
with:
cmd: install
This particular example is from my friend Chris Oliver, who runs Go Rails (check it out!!).
This solution would have been great except:
- The latest Ruby version available from GitHub is Ruby 2.6.3
- The latest Node version available from GitHub is Node 12.13.1
At CodeFund, we are using Ruby 2.6.5 (about to bump to 2.7) and Node 13.0.1. There are a few solutions that have been proposed for this problem, like installing the version of Ruby you want from source with ruby build or using a tool like nvm. These may work for you but they can be slow, and they wouldn't work for a problem I would later have. Instead, I wrote my own Docker image that had everything I needed already built in. Ruby 2.6.5, Node 13.0.1, additional packages you would need for Postgres, Chrome for system tests, Bundler 2.0.2, and my generic environment variables.
I am not going to explain all of the details here, and I know I could reduce the size a bit but here is the first iteration of that image:
FROM ruby:2.6.5
LABEL "name"="Locomotive"
LABEL "version"="0.0.1"
ENV APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=DontWarn
ENV BUNDLE_PATH='/bundle/vendor'
ENV LANG=en_US.UTF-8
ENV LANGUAGE=en_US.UTF-8
ENV LC_ALL=C.UTF-8
ENV PG_HOST='postgres'
ENV PG_PASSWORD='postgres'
ENV PG_USERNAME='postgres'
ENV RACK_ENV='test'
ENV RAILS_ENV='test'
ENV REDIS_CACHE_URL='redis://redis:6379/0'
ENV REDIS_QUEUE_URL='redis://redis:6379/0'
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
curl -sL https://deb.nodesource.com/setup_13.x | bash - && \
wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" | tee /etc/apt/sources.list.d/google-chrome.list && \
apt-get update && \
apt-get install -y google-chrome-stable && \
echo "CHROME_BIN=/usr/bin/google-chrome" | tee -a /etc/environment && \
wget -q -O - https://dl.google.com/linux/linux_signing_key.pub | apt-key add - && \
echo 'deb http://dl.google.com/linux/chrome/deb/ stable main' | tee /etc/apt/sources.list.d/google-chrome.list && \
apt-get -yqq install libpq-dev && \
apt-get install -qq -y google-chrome-stable yarn nodejs postgresql postgresql-contrib
RUN gem install bundler:2.0.2
6. Use Docker container
container:
image: andrewmcodes/locomotive:v0.0.1 # my image name
env: # additional environment variables I want to have access to
DEFAULT_HOST: app.codefund.io
Note: If you do not set a container, all steps will run directly on the host specified, which if you remember is Ubuntu 18.04.
As of now, our action looks like:
name: Run Tests & Linters
on:
pull_request:
branches:
- '*'
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:11
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports: ['6379:6379']
options: --entrypoint redis-server
container:
image: andrewmcodes/locomotive:v0.0.1
env:
DEFAULT_HOST: app.codefund.io
7. Add steps
Now it is time to run commands inside of our container. We will start by checking out the code.
steps:
- uses: actions/checkout@v2
8. Caching
Thankfully, GitHub provides some examples for getting started with your tools of choice for caching dependencies. I recommend checking those out and the documentation.
GitHub Actions Cache Examples
Cache Documentation
NOTE: Individual caches are limited to 400MB and a repository can have up to 2GB of caches. Once the 2GB limit is reached, older caches will be evicted based on when the cache was last accessed. Caches that are not accessed within the last week will also be evicted.
- name: Get Yarn Cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Node Modules Cache
id: node-modules-cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Gems Cache
id: gem-cache
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Assets Cache
id: assets-cache
uses: actions/cache@v1
with:
path: public/packs-test
key: ${{ runner.os }}-assets-${{ steps.extract_branch.outputs.branch }}
restore-keys: |
${{ runner.os }}-assets-
9. Bundle, Yarn, and Precompile Assets
Next, we will want to run Bundler and Yarn to install our dependencies if they were not restored from the cache, and precompile our assets.
- name: Bundle Install
run: bundle check || bundle install --path vendor/bundle --jobs 4 --retry 3
- name: Yarn Install
run: yarn check || bin/rails yarn:install
- name: Compile Assets
run: |
if [[ ! -d public/packs-test ]]; then
bin/rails webpacker:compile
else
echo "No need to compile assets."
fi
NOTE: You may be able to skip the asset compilation, that is up to you.
10. Update some files
In order to get this to work, I had to make a couple updates to some files in my project.
-
config/database.yml
Update host for test to be:host: <%= ENV.fetch("PG_HOST", "localhost") %>
- Update
test/application_system_test_case.rb
require "test_helper"
class ApplicationSystemTestCase < ActionDispatch::SystemTestCase
driven_by :selenium, using: :headless_chrome, screen_size: [1400,1400] do |driver_options|
driver_options.add_argument("--disable-dev-shm-usage")
driver_options.add_argument("--no-sandbox")
end
end
11. Setup Database
One last item we need to take care of prior to running the tests and linters is setting up our database.
- name: Setup DB
run: bin/rails db:drop db:create db:structure:load --trace
12. Run Tests and Linters
Now we can finally run our tests and linters.
- name: Run Rails Tests
run: |
bin/rails test
bin/rails test:system
- name: Zeitwerk Check
run: bundle exec rails zeitwerk:check
- name: StandardRB Check
run: bundle exec standardrb --format progress
- name: ERB Lint
run: bundle exec erblint app/views_redesigned/**/*.html.erb
- name: Prettier-Standard Check
run: yarn run --ignore-engines prettier-standard --check 'app/**/*.js'
At this point, your action should be complete!
Here is my completed action file:
name: Run Tests & Linters
on:
pull_request:
branches:
- '*'
push:
branches:
- master
jobs:
build:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:11
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
ports: ['5432:5432']
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis
ports: ['6379:6379']
options: --entrypoint redis-server
container:
image: andrewmcodes/locomotive:v0.0.1
env:
DEFAULT_HOST: app.codefund.io
steps:
- uses: actions/checkout@v1
- name: Get Yarn Cache
id: yarn-cache
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Cache Node Modules
id: node-modules-cache
uses: actions/cache@v1
with:
path: ${{ steps.yarn-cache.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Cache Gems
id: gem-cache
uses: actions/cache@v1
with:
path: vendor/bundle
key: ${{ runner.os }}-gem-${{ hashFiles('**/Gemfile.lock') }}
restore-keys: |
${{ runner.os }}-gem-
- name: Cache Assets
id: assets-cache
uses: actions/cache@v1
with:
path: public/packs-test
key: ${{ runner.os }}-assets-${{ steps.extract_branch.outputs.branch }}
restore-keys: |
${{ runner.os }}-assets-
- name: Bundle Install
run: bundle install --path vendor/bundle --jobs 4 --retry 3
- name: Yarn Install
run: bin/rails yarn:install
- name: Compile Assets
shell: bash
run: |
if [[ ! -d public/packs-test ]]; then
bundle exec rails webpacker:compile
else
echo "No need to compile assets."
fi
- name: Setup DB
run: bin/rails db:drop db:create db:structure:load --trace
- name: Run Rails Tests
run: |
bin/rails test
bin/rails test:system
- name: Zeitwerk Check
run: bundle exec rails zeitwerk:check
- name: StandardRB Check
run: bundle exec standardrb --format progress
- name: ERB Lint
run: bundle exec erblint app/views_redesigned/**/*.html.erb
- name: Prettier-Standard Check
run: yarn run --ignore-engines prettier-standard --check 'app/**/*.js'
As you can see, setting up GitHub Actions for your CI can be quite involved and requires a lot of initial setup. Hopefully this post will help you if you are thinking of experimenting with them on your Rails app. Check back later this week for Part 2, setting up CircleCI!
Top comments (2)
Thx for the detailed explanations! As for ruby, node and yarn versions I went with existing actions from the marketplace and it works flawlessly. You can also use Yarn policies to be extra sure. It’s also extremely fast. My full Rails 6 CI workflow is 1min, see: dev.to/vvo/a-rails-and-postgresql-...
Hey! Yes you can do that but as I stated above, I needed versions of Ruby and Node that weren't offered by the official actions. Thanks for readying!