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!