---
title: A practical GitHub Actions pipeline example with Rails
published: "2024-10-28"
publisher: Honeybadger
author: Jeffery Morhous
category: Ruby articles
tags:
  - Ruby
  - Rails
  - GitHub
  - GitHub Actions
description: "Continuous Integration is a vital tool for developing software, and GitHub Actions makes it easy to add to almost any project. Dig into this practical GitHub Actions pipeline example and learn how to setup Continuous Integration for a Ruby on Rails project."
url: "https://www.honeybadger.io/blog/rails-ci/"
---

Continuous integration (CI) pipelines are an important part of building and deploying reliable software. Whether it's building a Docker image, running tests, or even just doing code linting, CI pipelines help you automate repetitive tasks and ship better code even faster. [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions/understanding-github-actions#overview) lets you create pipelines to build containers, test source code, and publish software.

In this blog post, we'll walk through a GitHub Actions pipeline example, building a Docker image with a Rails application, running tests, and publishing the Docker image. You'll walk away feeling confident that you can build a Rails CI pipeline that fits your needs.

## Building Rails continuous integration pipelines

Continuous integration (CI) refers to the best practice of automating the development workflow of multiple contributors introducing code into a project. An automated CI process—or pipeline—removes much of the burden of testing and releasing software. CI pipelines often run on a pull request, which either blocks or allows the merge of a branch into the primary git branch.

In many projects, the biggest benefit of a CI pipeline is that it builds containers that can be used to ship the software. It's also a common practice to run automated tests as part of a CI pipeline to ensure that code that fails tests cannot be merged into the project and cause a regression.

For some projects, a linter will run against the code changes to validate that it matches the project style guidelines automatically. Much of a CI process is intended to build confidence that a change is fit for production, which makes the code review process even easier.

## Shipping with GitHub Actions CI pipelines

GitHub Actions are an easy way to introduce a CI pipeline into your process as part of your existing workflow on GitHub. You can start a GitHub Actions workflow as part of several different events on GitHub, including pull requests, code pushes, or even issue creation.

GitHub Actions has starter workflows that make setting up a process for the first time easier, and there's one workflow for Ruby. You'll set up your GitHub continuous integration pipeline as several workflows, written as Infrastructure as Code (IaC), meaning all your configuration is human readable files that live in the repository.

You can follow along with a new project or any Rails application hosted on GitHub.

## Building and publishing a Docker image from a Rails CI pipeline

Deployment of code changes to a production environment, specifically continuous deployment, is often the final step in a CI/CD pipeline. If you deploy your application with Docker, you must build a new Docker image before each deployment. Docker uses a `Dockerfile`—like a blueprint—to create a Docker image, which is then used to create containers that run your application.

Building this image as part of a CI process is a great beginning for automating deploys. With a GitHub CI pipeline, you can build a Docker Image and push it to a registry, which a deploy tool can then pull from to deploy containers.

As of Rails 7.1, a Dockerfile is automatically created with `rails new`. If your application was created before Rails 7.1, or you don't already have a working Dockerfile to run your application in a container, you'll first want to [set up your app to run in Docker](https://www.honeybadger.io/blog/containerizing-an-existing-rails-application/).

Building and publishing a Docker image is a repeatable process, so GitHub Actions has presets that we can lean on. Under your repository name, go to the "Actions" tab. We'll add the first workflow in the next section.

![Adding GitHub Actions to a Repository](https://www-files.honeybadger.io/posts/rails-ci/adding-github-actions.png)_Adding GitHub Actions_

Select the configure on the "Publish Docker Container" suggested workflow. If it's not suggested for you, you can search for the workflow. Don't fret if you can't find it, as we'll be walking through the configuration file that is generated here. When you click "Configure," GitHub generates a new `docker-publish.yml` file for your repository:

```yml
name: Docker # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. on: schedule: - cron: '41 2 * * *' push: branches: [ "main" ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: branches: [ "main" ] env: # Use docker.io for Docker Hub if empty REGISTRY: ghcr.io # github.repository as <account>/<repo> IMAGE_NAME: ${{ github.repository }} jobs: build: runs-on: ubuntu-latest permissions: contents: read packages: write # This is used to complete the identity challenge # with sigstore/fulcio when running outside of PRs. id-token: write steps: - name: Checkout repository uses: actions/checkout@v3 # Install the cosign tool except on PR # https://github.com/sigstore/cosign-installer - name: Install cosign if: github.event_name != 'pull_request' uses: sigstore/cosign-installer@6e04d228eb30da1757ee4e1dd75a0ec73a653e06 #v3.1.1 with: cosign-release: 'v2.1.1' # Set up BuildKit Docker container builder to be able to build # multi-platform images and export cache # https://github.com/docker/setup-buildx-action - name: Set up Docker Buildx uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0 # Login against a Docker registry except on PR # https://github.com/docker/login-action - name: Log into registry ${{ env.REGISTRY }} if: github.event_name != 'pull_request' uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Extract metadata (tags, labels) for Docker # https://github.com/docker/metadata-action - name: Extract Docker metadata id: meta uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} # Build and push Docker image with Buildx (don't push on PR) # https://github.com/docker/build-push-action - name: Build and push Docker image id: build-and-push uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0 with: context: . push: ${{ github.event_name != 'pull_request' }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max # Sign the resulting Docker image digest except on PRs. # This will only write to the public Rekor transparency log when the Docker # repository is public to avoid leaking data.  If you would like to publish # transparency data even for private images, pass --force to cosign below. # https://github.com/sigstore/cosign - name: Sign the published Docker image if: ${{ github.event_name != 'pull_request' }} env: # https://docs.github.com/en/actions/security-guides/security-hardening-for-github-actions#using-an-intermediate-environment-variable TAGS: ${{ steps.meta.outputs.tags }} DIGEST: ${{ steps.build-and-push.outputs.digest }} # This step uses the identity token to provision an ephemeral certificate # against the sigstore community Fulcio instance. run: echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
```

Because this configuration file is well-commented, we can quickly go through it to see if it fits our needs. The first section (under the `on` key) tells GitHub actions _when to run this workflow_. Right now, it's set to run when a pull request is opened (or reopened) against `main`, when a push to a specific branch (the `main` branch) happens, and also every day at 2:41.

We don't need to run this workflow on a schedule, so we'll remove that part from the `on` section. It should look like this:

```yml
on: push: branches: [ "main" ] # Publish semver tags as releases. tags: [ 'v*.*.*' ] pull_request: branches: [ "main" ]
```

The next section sets environment variables, which we'll leave alone. The section after that gives details on the `jobs` to run, which contains most of the configuration.

This comes pre-configured with `steps` that run to install the necessary dependencies, build a Docker image from the repository, and push it to the configured registry. In this project, we're pushing Docker images to GitHub Packages.

One step is to install a tool called cosign-intaller, which you'll want to change to pull the latest version of cosign-installer. The step should now look like this:

```yml
# Install the cosign tool except on PR # https://github.com/sigstore/cosign-installer - name: Install cosign if: github.event_name != 'pull_request' uses: sigstore/cosign-installer@v3.5.0 with: cosign-release: 'v2.2.4' # optional
```

After you've made the change to the `on` section and the `Install cosign` step, click the "Commit Changes" button, which will create this file in `.github/workflows`.

## Testing this GitHub Actions pipeline example

You should see the running workflow when you click the Actions tab for your repository:

![GitHub Actions Tab Showing the First Running Workflow](https://www-files.honeybadger.io/posts/rails-ci/first-workflow-run.png)_The first running workflow_

When the workflow completes successfully, you'll see a new entry for "Packages" in the right sidebar of your GitHub repository.

![The Project's Main Page with a Published Package](https://www-files.honeybadger.io/posts/rails-ci/packages.png)_A published package_

## Running RuboCop with GitHub Actions

A robust continuous integration pipeline does more than build and publish a Docker image. Oftentimes a Rails CI pipeline [uses RuboCop](https://www.honeybadger.io/blog/linting-formatting-ruby/), running the linter on pull requests before allowing a merge.

To add a workflow to run RuboCop as part of our continuous integration in GitHub, we'll create a new file in our repository. While we used the GitHub UI to set up the workflow for Docker, we'll write this one ourselves. Create a new YAML file called `.github/workflows/run-linting.yml` that looks like this:

```yml
name: Linting on: push: pull_request: branches: [ "main" ] jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.2.0' - run: bundle install - name: RuboCop run: rubocop
```

This workflow is just a slightly modified version of the example in GitHub's documentation for Ruby. It sets up the workflow to run when commits are pushed or when a pull request is open. When the workflow runs, it installs (and caches) Ruby and its dependencies, runs `bundle install` for Gem dependencies, then runs `rubocop`.

Commit and push this file to your repository to see it run as an action. Then, go to the Actions tab of your repository, and you'll see your latest run triggered a workflow for both RuboCop and building a Docker image!

![GitHub Actions Pipeline Example](https://www-files.honeybadger.io/posts/rails-ci/running-rubocop.png)_The Project's Actions History with RuboCop Workflow_

## Running unit tests with GitHub Actions

Running unit tests or integration tests in GitHub Actions is similar to running RuboCop. You've already created a workflow that installs Ruby, installs your application dependencies, and runs a Ruby gem, which is the exact process we'll use to run tests. We can run RSpec tests with a workflow that installs Ruby, installs our gems, and then runs `bundle exec rspec`.

Create a new file called `.github/workflows/run-tests.yml` that looks like this:

```yml
name: Tests on: push: pull_request: branches: [ "main" ] jobs: rspec: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: ruby-version: '3.2.0' - run: bundle install - name: RSpec run: bundle exec rspec
```

The `on` section for this workflow is the same as the workflow for RuboCop. We run tests on every commit to any branch, and also when a pull request is opened to the main branch.

After you push this file, visiting the Actions tab in your repository will show you the running workflows, with a different entry for Docker, Linting, and RSpec!

You can change the workflow to work with other testing libraries, and even expand it to have database connectivity if your tests require that.

If your tests rely on a Postgres database, for example, you'll need to install Postgres first. Github Actions can still handle setting up your database, but you'll need [add some additional configuration to the testing workflow](https://thoughtbot.com/blog/rspec-rails-github-actions-configuration#wrapping-up) under a `services` key for your `rspec` job, install dependencies for Postgres in that job, and set the appropriate environment variables.

## Making workflows interact

In many cases, the steps in a CI pipeline will depend on each other. With minor tweaks, we can wait to build the Docker image until the tests pass. There are several ways to achieve this, but one is to use workflow\_run as the event trigger for our Docker workflow. Let's change the `on` key at the top of our `docker-publish.yml` to look like this:

```yml
on: workflow_run: workflows: [Tests] types: - completed
```

This will only run the Docker workflow if the `Test` workflow completes. Next, we'll change the `build` job in this workflow to only run if the completed workflow is successful.

```yml
jobs: build: if: ${{ github.event.workflow_run.conclusion == 'success' }} runs-on: ubuntu-latest
```

Now, when tests fail, the Docker image will not build and publish to a registry. When tests succeed, we'll trigger that workflow to build and publish the image.

## Building your own CI pipelines for Rails

In this tutorial, we've explained how to use GitHub Actions to quickly make a CI pipeline. By creating workflows that build Docker images, run tests, and enforce coding standards, we can build confidence that every code change is production-ready and enable continuous deployment. Defining our GitHub Actions CI pipeline as code makes it easy to make and track changes like you would to the application itself.

Hopefully, stepping through this practical GitHub actions pipeline example gives you the confidence to build your own pipeline. Adding CI to your Rails development process empowers your team to ship more reliable software! If you found this helpful, consider [signing up for the Honeybadger newsletter](https://www.honeybadger.io/newsletter/) to get more articles like this in your inbox.

---

## Try Honeybadger for FREE

Intelligent logging, error tracking, and Just Enough APM™ in one dev-friendly platform. Find and fix problems before users notice.

[Start free trial](https://app.honeybadger.io/users/sign_up)

[See plans and pricing](https://www.honeybadger.io/plans/)
