Skip to content
compiler.dev

Ruby on Rails GitHub Actions Workflow Template

Last updated

This is a working .github/workflows/ci.yml for a Rails app on Postgres. It caches gems with ruby/setup-ruby, starts a Postgres service container, prepares the test database and runs RuboCop and your tests.

The workflow

name: CI

on:
  push:
    branches: [main]
  pull_request:

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U postgres"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 10
    env:
      RAILS_ENV: test
      DATABASE_URL: postgres://postgres:postgres@localhost:5432/app_test
    steps:
      - uses: actions/checkout@v4

      - uses: ruby/setup-ruby@v1
        with:
          ruby-version: .ruby-version
          bundler-cache: true

      - run: bundle exec rubocop --parallel
      - run: bin/rails db:create db:schema:load
      - run: bin/rails test

What the non-obvious parts do

  • ruby-version: .ruby-version reads the version from your repository file, so CI and local development cannot drift. If you do not have the file, write a version such as "3.3" instead.
  • bundler-cache: true runs bundle install and caches the gems, keyed on Gemfile.lock and the Ruby version. You do not need a separate actions/cache step.
  • services.postgres runs Postgres next to the job at localhost:5432. The --health-* options make GitHub wait until it accepts connections.
  • DATABASE_URL overrides config/database.yml in Rails, so the same file can serve local and CI use. The database name app_test is created by db:create.
  • db:schema:load builds the database from db/schema.rb in one step, which is faster than replaying every migration. Use db:structure:load if you use structure.sql.
  • rubocop --parallel lints on all cores. Remove the step if you do not use RuboCop.
  • bin/rails test runs Minitest. Replace it with bundle exec rspec for RSpec.

Variants

System tests and JavaScript

Rails apps with JavaScript need Node. Add actions/setup-node@v4 with cache: yarn or npm, and run bin/rails assets:precompile or yarn build before the tests. System tests need Chrome, already present on ubuntu-latest.

Parallel tests

Rails can parallelise tests with parallelize(workers: :number_of_processors) in test_helper.rb. For several machines, use test sharding, and give each shard its own database.

Redis or Sidekiq

Add a redis:7 service with --health-cmd "redis-cli ping".

Deploy job

Add a second job with needs: test on main only. Use OIDC to reach your cloud.

macOS

Not needed for a server app. Stay on Linux; service containers do not run on macOS runners.

Run it on faster machines

Rails suites spend most of their time in database setup and test execution. Change one line:

    runs-on: [self-hosted, Linux, compiler-ci]

The job then runs on a fresh 8 vCPU machine from compiler.dev, with automatic fallback. For a full tuning list, see speed up Rails on GitHub Actions. Prices are on the pricing page.

FAQ

Why not use actions/cache for gems?

bundler-cache: true does it for you with a correct key. Manual caching often misses the Ruby version in the key.

My tests fail only in CI with a connection error. Why?

Check that DATABASE_URL host is localhost and that the health check options are present.

Can I use MySQL instead?

Yes. Swap the service for mysql:8, use --health-cmd "mysqladmin ping", and change the DATABASE_URL scheme to mysql2://.

How do I debug a failure?

Upload log/test.log and tmp/screenshots as an artifact with if: failure().