Pull Request Previews with Rails, Cloud Run, and GitHub Actions

Posted on · 1 minute read

If you work in a larger organization, chances are you have access to some sort of staging environment where you can preview work that is currently in progress. Staging environments can be immensely useful to get early feedback, be it from QA or other departments, which can speed up the development of new features significantly.

Several large platforms such as Netlify, Vercel, or Serverless offer preview deploys of one sort or another. There are also paid services such as Pullpreview, which runs on AWS and allows one-click deploys for GitHub pull requests.

We are going to be building something similar, but for a simple Ruby on Rails application, and using Google Cloud Run. We are going to use GitHub Actions to do the actual deployment and continuous integration part. In the end, we want a solution that can accomplish the following:

  • An isolated preview environment is created for each pull request. That environment is easily accessible using a direct link.
  • On pushing changes to the pull request, the preview should automatically update.
  • The preview environment should be fast to set up and tear down. No manual intervention should be necessary at any point.

To showcase how we can build such a solution we’ll do the following:

  1. Create a simple Rails application for demo purposes.
  2. Build an application Docker image.
  3. Create a Cloud Run Service that runs our application container.
  4. Set up a GitHub action that automatically builds our application container and deploys it to Google Cloud Run

This is a bit of a longer post, so if you are more of a TL;DR type head over to this repository to check out the complete solution.

This guide is intended for people who are already somewhat experienced with Rails, Docker and the Google Cloud SDK. I reference documentation where possible, but you will get the most out of this guide if you have a basic understanding of the involved technologies already. This is not a tutorial on how to work with Ruby on Rails or Docker or Google Cloud.

Application Setup

While this guide focuses on the whole deploying-a-pull-request part, we are going to need a simple application that we can actually deploy. We can keep it simple. How about an application that allows you to store some books? Perfect 👌

Make sure you have a recent version of Ruby and Yarn installed, then create the bookstore application. We’ll keep it minimal here, all we want is to store some books.

rails new bookstore \
  --database=postgresql \
  --webpack\
  --skip-active-storage \
  --skip-action-cable \
  --skip-system-test \
  --skip-action-text \
  --skip-action-mailer

To make it easier for us to configure our application down the line, let’s also add the dotenv gem to our Gemfile and run bundle install.

gem 'dotenv-rails'

Because we will deploy our application on Cloud Run, we’ll have to dockerize it sooner or later. To make it easier for us to test our process locally - trust me, this is quite handy - we’ll create a docker-compose file for our application and it’s dependencies. Once you have Docker and Docker Compose set up, let’s create our docker-compose.yml and add Postgres as a dependency.

version: "3"
services:
  bookstore-db:
    image: postgres
    volumes:
      - db-data:/var/lib/postgresql/data
    ports:
      - 5432:5432
    environment:
      postgres_password: bookstore
      postgres_user: bookstore

volumes:
  db-data: ~

Then start the database container.

docker-compose up -d

So far so good. Our bookstore doesn’t do anything yet. We’ll want to create, edit and delete books. But first, we have to make sure the application can connect to our Database. Open config/database.yml and update it with the following contents:

default: &default
  adapter: postgresql
  encoding: unicode
  host: <%= ENV['DATABASE_HOST'] %>
  username: <%= ENV['DATABASE_USERNAME'] %>
  password: <%= ENV['DATABASE_PASSWORD'] %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>

development:
  <<: *default
  database: bookstore_development

test:
  <<: *default
  database: bookstore_test

production:
  <<: *default
  database: "bookstore_production"

Next, create a .env file for dotenv to load and fill it with credentials matching what is in your docker-compose.yml. For now, let’s have the database be located at localhost, but we’ll change that soon.

DATABASE_HOST=localhost
DATABASE_USERNAME=bookstore
DATABASE_PASSWORD=bookstore

If we did well so far we should be able to set up our database and start the server.

rails db:setup
rails server

You should be able to see the application running on http://localhost:3000. Next, let’s create some books, migrate our data and restart the rails server.

rails g scaffold books title:string author:string publication_year:integer
rails db:migrate
rails server

We can now add some books on http://localhost:3000/books. As a final tweak, let’s update the config/routes.rb file so we can save ourselves from having to append /books to our URLs in the future.

Rails.application.routes.draw do
  root 'books#index'
  resources :books
end

That’s good enough for a basic app I’d say. Let’s get started on dockerizing it.

Dockerizing the Bookstore

We’ll be running our application in production mode within its container, so let’s prepare for that by setting up a secret key base for production.

rails secret
rails credentials:edit --environment production
secret_key_base: <your-secret-goes-here>

When editing your credentials for the first time take note of the generated master key. You can always check config/credentials/production.key later though. Do not commit this file to your version control.

To create a Docker image for an application, you need a Dockerfile. So let’s create that. Here’s one to get you started:

FROM ruby:3.0-slim as cache

RUN apt-get update -qq && apt-get install -y \
  curl \
  build-essential \
  libpq-dev \
  postgresql-client
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -

RUN curl -sL 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

RUN apt-get update -qq && apt-get install -y yarn

WORKDIR /bookstore
COPY Gemfile /bookstore/Gemfile
COPY Gemfile.lock /bookstore/Gemfile.lock
COPY package.json /bookstore/package.json
COPY yarn.lock /bookstore/yarn.lock

RUN bundle install --without development
RUN yarn install

FROM cache
COPY . /bookstore

ARG DATABASE_HOST=db
ARG RAILS_ENV=production

ENV DATABASE_HOST=$DATABASE_HOST
ENV RAILS_ENV=$RAILS_ENV

ENV RAILS_LOG_TO_STDOUT=true
ENV RAILS_SERVE_STATIC_FILES=true

RUN bundle exec rails assets:precompile

COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]

EXPOSE 3000

# Start the main process.
CMD ["rails", "server", "-b", "0.0.0.0"]

Adjust this to suit your needs. We set some particular environment variables.

RAILS_LOG_TO_STDOUT=true
RAILS_SERVE_STATIC_FILES=true

Most likely you would not set these in a ‘real’ application, but for demo purposes, letting Rails serve static assets simplifies the deployment process a lot, and logging to STDOUT makes debugging a lot easier when something goes awry.

You may have noticed that the Dockerfile references an entrypoint.sh file, so let’s create that as well.

#!/bin/bash

set -e

# Create the Rails production DB on the first run
bundle exec rails db:create

# Make sure we are using the most up to date
# database schema
bundle exec rails db:migrate

# Remove a potentially pre-existing server.pid for Rails.
rm -f /bookstore/tmp/pids/server.pid

# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

It is also a good idea to create a .dockerignore file - here is a sample - although this is not strictly necessary. With the Dockerfile complete, we should be able to build our bookstore image.

docker build -t bookstore .

If you try to run your container and access the bookstore application now, you’ll notice some errors due to the database container no longer being accessible.

To fix that while keeping things simple, let’s just move our bookstore image to the docker-compose.yml as well.

version: "3"
services:
  ...

  bookstore:
    build: .
    image: gcr.io/rails-gcloud-bookstore/bookstore
    ports:
      - 3000:3000
    environment:
      DATABASE_HOST: bookstore-db
      DATABASE_USERNAME: bookstore
      DATABASE_PASSWORD: bookstore
    depends_on:
      - bookstore-db

volumes:
  db-data: ~

Note the image name, as you’ll have to adjust it later. It has to match the name of your Google Cloud Project, which we’ll create when we push our image to the Google Cloud Registry.

Running docker-compose up now should build the bookstore application image and start the container. A production version of your application should now be accessible under http://localhost:3000.

If your container won’t start it can be helpful to connect to it using docker-compose run --entrypoint /bin/bash bookstore to verify the contents of environment variables or the presences of files within the container.

Set Up Google Cloud

Let’s recap. We built a simple Rails application - our trusty bookstore - and made sure we can build a production-ready Docker image for it. Now let’s set up the necessary Google Cloud infrastructure.

First things first. Make sure you have installed the Google Cloud SDK. Once you have it installed log in.

gcloud auth login

This guide uses Google Cloud SDK for convenience reasons. Copy-pasting a bunch of commands is just a lot easier than reproducing what’s shown on some screenshot. That being said, you can accomplish everything described thereafter using the Google Cloud Console. Pick your poison.

Let’s set up a new Google Cloud Project. I’ll name mine rails-gcloud-bookstore. You’ll likely have to pick a different one because project names must be unique. Then set the new project as default project.

gcloud projects create rails-gcloud-bookstore
gcloud config set project rails-gcloud-bookstore

Next, we’ll enable the Container Registry because that is where we are going to push our Docker image.

You’ll have to enable billing for your project, because storing containers is not free. Head over to Billing and set your billing account. You can find more information on billing in the official documentation. Do not worry, following this guide will cost you a couple of cents at the most 😉

The Cloud SDK provides a helpful Docker utility that makes it easy to push local images to the Google Container Registry, so let’s enable that.

gcloud services enable containerregistry.googleapis.com
gcloud components install docker-credential-gcr
gcloud auth configure-docker

Now you should be able to push your local image:

docker push gcr.io/rails-gcloud-bookstore/bookstore

This might take a while. Afterward, you can check that your image was indeed pushed by either running gcloud container images list or verifying the contents of the container registry in the Cloud Console.

Before we can run our application on Cloud Run, we’ll have to take care of setting up Postgres in the Cloud. We need a place to store our books after all.

Let’s set up a Cloud SQL instance and configure it. You can pick any of the available regions. For the remainder of the guide, I’m going with europe-north1 wherever a region needs to be specified.

gcloud sql instances create bookstore-db \
  --root-password="bookstore" \
  --database-version=POSTGRES_13 \
  --region=europe-north1 \
  --tier=db-f1-micro \
  --no-backup
gcloud sql users create bookstore --instance=bookstore-db --password='bookstore'

Cloud SQL instances are not publicly accessible without additional configuration. As such, there is no need to worry too much about picking a password for demo purposes. For production uses you should still pick a strong password.

Almost everything we need is in place. Let’s enable Cloud Run.

gcloud services enable run.googleapis.com

To make sure that your application can connect to the Cloud SQL instance we will need to set the necessary permissions. List all available service accounts and give the compute service account the Cloud SQL Client role. Google Cloud automatically uses this service account when running containers in Cloud Run.

gcloud iam service-accounts list
gcloud projects add-iam-policy-binding rails-gcloud-bookstore \
  --member serviceAccount:<projectid>[email protected] \
  --role roles/cloudsql.client

Now, let’s deploy the bookstore app.

Make sure that the environment variables match what you have configured previously. In particular, the database host variable should contain the SQL connection name, and the production key should match the key you got when setting up production credentials for the bookstore app previously. To find the SQL connection name you can run gcloud sql instances describe bookstore-db.

gcloud run deploy bookstore-preview \
  --image=gcr.io/rails-gcloud-bookstore/bookstore \
  --platform=managed \
  --port=3000 \
  --add-cloudsql-instances=rails-gcloud-bookstore:europe-north1:bookstore-db  \
  --set-env-vars DATABASE_HOST=/cloudsql/rails-gcloud-bookstore:europe-north1:bookstore-db \
  --set-env-vars DATABASE_USERNAME='bookstore' \
  --set-env-vars DATABASE_PASSWORD='bookstore' \
  --set-env-vars RAILS_PRODUCTION_KEY="your-production-master-key" \
  --region=europe-north1 \
  --allow-unauthenticated

If everything was successful running the deploy command should give you a link that you can visit to view your application running on Cloud Run :ok_hand:

Creating the GitHub Action

We are almost there. Before we create our GitHub action that automates the deployment process we’ll need to create a Google Cloud service account that has the necessary roles to:

  • Push our image to the Container Registry
  • Deploy the image to Cloud Run

Run the following commands to create a service account and give it the permissions required.

gcloud iam service-accounts create \
  bookstore-build \
  --display-name="Bookstore Builder"
gcloud projects add-iam-policy-binding rails-gcloud-bookstore \
  --member serviceAccount:[email protected]  \
  --role roles/run.admin
gcloud projects add-iam-policy-binding rails-gcloud-bookstore \
  --member serviceAccount:[email protected]  \
  --role roles/storage.admin
gcloud projects add-iam-policy-binding rails-gcloud-bookstore \
  --member serviceAccount:[email protected]  \
  --role roles/iam.serviceAccountUser

Verify that all roles are set correctly either by running the following command or by checking the Cloud Console IAM section.

 gcloud projects get-iam-policy rails-gcloud-bookstore  \
  --flatten="bindings[].members" \
  --format='table(bindings.role)' \
  --filter="bindings.members:[email protected]"

To use this service account in the GitHub action download the service account key.

gcloud iam service-accounts keys create bookstore-build.json \
  --iam-account [email protected]

Make sure to not commit this file, as to not give other persons access to you Google Cloud Resources. If you did so by accident you can always remove the compromised key and recreate a new one on the service accounts page.

If you haven’t already create a new GitHub repository.

Copy the contents of the service account key file (yes, the entire content) and add them to your GitHub secrets as GCP_SA_KEY. While you are at it, also add your RAILS_PRODUCTION_KEY, because we’ll need that as well. Your action secrets page should look something like this after you are done.

secrets

Now let’s create a new workflow file. If you haven’t worked with GitHub actions before, you can learn more about them here.

You can create a GitHub action using the GitHub web interface, but we’ll use the command line as usual. In your bookstore repository create a new workflows folder.

mkdir -p .github/workflows

Create a workflow file in that new folder (I named mine preview.yml, but you can call it whatever you want), and let’s add the following content.

name: Preview

on:
  pull_request:
    branches:
      - "main"
    types: [synchronize, opened, reopened]

jobs:
  tests:
    runs-on: ubuntu-latest
    env:
      RAILS_ENV: test
      DATABASE_HOST: localhost
      DATABASE_USERNAME: bookstore
      DATABASE_PASSWORD: bookstore
    services:
      postgres:
        image: postgres:13
        env:
          POSTGRES_PASSWORD: bookstore
          POSTGRES_USER: bookstore
        ports:
          - 5432:5432

    steps:
      - uses: actions/checkout@v2
      - name: Set up Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: 3.0
          bundler-cache: true
      - name: Set up Node
        uses: actions/setup-node@v1
        with:
          node-version: 14.9
      - name: Install dependencies
        run: |
          sudo apt-get -yqq install libpq-dev build-essential libcurl4-openssl-dev
          gem install bundler
          bundle install --jobs 4
          yarn install
      - name: Setup databases
        run: |
          bin/rails db:setup
      - name: Run tests
        run: bin/rails test

  container:
    runs-on: ubuntu-latest
    needs: [tests]
    steps:
      - uses: actions/checkout@v2
      - name: Setup Google Cloud SDK
        uses: google-github-actions/setup-gcloud@master
        with:
          project_id: rails-gcloud-bookstore
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          export_default_credentials: true
      - name: Install Google Cloud Docker
        run: gcloud components install docker-credential-gcr
      - name: Setup Docker for GCR
        run: gcloud auth configure-docker

      - name: Build Docker Image
        run: |
          echo ${{ secrets.RAILS_PRODUCTION_KEY }} > config/credentials/production.key 
          docker build -t gcr.io/rails-gcloud-bookstore/bookstore:${{github.event.number}} .
      - name: Push Docker Image
        run: docker push gcr.io/rails-gcloud-bookstore/bookstore:${{github.event.number}}

  preview:
    runs-on: ubuntu-latest
    needs: [container]
    steps:
      - uses: actions/checkout@v2
      - name: Setup Google Cloud SDK
        uses: google-github-actions/setup-gcloud@master
        with:
          project_id: rails-gcloud-bookstore
          service_account_key: ${{ secrets.GCP_SA_KEY }}
          export_default_credentials: true
      - name: Get HEAD Commit Hash
        id: commit
        run: echo "::set-output name=hash::$(git rev-parse --short HEAD)"
      - name: Deploy Revision On Cloud Run
        run: |
          gcloud run deploy bookstore-preview \
          --image=gcr.io/rails-gcloud-bookstore/bookstore:${{github.event.number}} \
          --platform=managed \
          --port=3000 \
          --revision-suffix=${{github.event.number}}-${{steps.commit.outputs.hash}} \
          --add-cloudsql-instances=rails-gcloud-bookstore:europe-north1:bookstore-db  \
          --set-env-vars DATABASE_HOST=/cloudsql/rails-gcloud-bookstore:europe-north1:bookstore-db \
          --set-env-vars DATABASE_USERNAME='bookstore' \
          --set-env-vars DATABASE_PASSWORD='bookstore' \
          --set-env-vars DATABASE_NAME=bookstore_production_${{github.event.number}} \
          --set-env-vars RAILS_PRODUCTION_KEY=${{ secrets.RAILS_PRODUCTION_KEY }} \
          --allow-unauthenticated \
          --region=europe-north1
      - name: Update Traffic
        run: |
          gcloud components install beta
          gcloud beta run services update-traffic bookstore-preview \
            --update-tags pr-${{github.event.number}}=bookstore-preview-${{github.event.number}}-${{steps.commit.outputs.hash}} \
            --platform=managed \
            --region=europe-north1
      - name: Get Preview URL
        id: preview-url
        run: |
          url=$(gcloud run services describe bookstore-preview --format 'value(status.url)' --platform=managed --region=europe-north1 | sed 's|https://|https://pr-${{github.event.number}}---|g')
          echo "::set-output name=url::$url"
      - name: Post PR comment with preview deployment URL
        uses: mshick/add-pr-comment@v1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          message: |
            Successfully deployed preview at ${{steps.preview-url.outputs.url}}
          allow-repeats: false

There’s a fair bit to unpack here, so let’s go over this step by step.

First, we need to define when our workflow should run. We want to deploy to our preview environment whenenver a pull request against our main branch is opened.

Furthermore, we want to do the same when a Pull Request is reopened or when new code is pushed to our pull request branch (which fires the synchronize event).

on:
  pull_request:
    branches:
      - "main"
    types: [synchronize, opened, reopened]

To specify what our workflow should actually do we define several jobs. Testing the application before doing a deployment is not strictly necessary, but it doesn’t hurt either. You don’t want to deploy broken code, even if it is just to a preview environment :wink:

tests:
  runs-on: ubuntu-latest
  env:
    RAILS_ENV: test
    DATABASE_HOST: localhost
    DATABASE_USERNAME: bookstore
    DATABASE_PASSWORD: bookstore
  services:
    postgres:
      image: postgres:13
      env:
        POSTGRES_PASSWORD: bookstore
        POSTGRES_USER: bookstore
      ports:
        - 5432:5432

  steps:
    - uses: actions/checkout@v2
    - name: Set up Ruby
      uses: ruby/setup-ruby@v1
      with:
        ruby-version: 3.0
        bundler-cache: true
    - name: Set up Node
      uses: actions/setup-node@v1
      with:
        node-version: 14.9
    - name: Install dependencies
      run: |
        sudo apt-get -yqq install libpq-dev build-essential libcurl4-openssl-dev
        gem install bundler
        bundle install --jobs 4
        yarn install
    - name: Setup databases
      run: |
        bin/rails db:setup
    - name: Run tests
      run: bin/rails test

We can then repeat the steps which we already performed locally - but with some twists. Our container build job depends on our test job, as we don’t want to run this step if the tests failed. We set up Google Cloud and authenticate with the service account we previously created, then build our image and push it to the Container Registry.

Note that we inject the production key into our image. We also tag the created image with the pull request number - this is important so we can deploy multiple pull requests side by side.

container:
  runs-on: ubuntu-latest
  needs: [tests]
  steps:
    - uses: actions/checkout@v2
    - name: Setup Google Cloud SDK
      uses: google-github-actions/setup-gcloud@master
      with:
        project_id: rails-gcloud-bookstore
        service_account_key: ${{ secrets.GCP_SA_KEY }}
        export_default_credentials: true
    - name: Install Google Cloud Docker
      run: gcloud components install docker-credential-gcr
    - name: Setup Docker for GCR
      run: gcloud auth configure-docker

    - name: Build Docker Image
      run: |
        echo ${{ secrets.RAILS_PRODUCTION_KEY }} > config/credentials/production.key 
        docker build -t gcr.io/rails-gcloud-bookstore/bookstore:${{github.event.number}} .
    - name: Push Docker Image
      run: docker push gcr.io/rails-gcloud-bookstore/bookstore:${{github.event.number}}

Now we need to deploy our newly-created docker image. We won’t create a new service for each of our pull requests - instead, we’ll use revisions. Old revisions of Cloud Run services are automatically removed, which simplifies the clean up we’ll have to do when the pull request is closed.

However, we need some way to distinguish revisions and route traffic for the pull request to the most recent one - we’ll use the commit hash to identify those.

 - name: Get HEAD Commit Hash
    id: commit
    run: echo "::set-output name=hash::$(git rev-parse --short HEAD)"

Then we deploy the new revision. This command is similar to the one we used locally, with some minor changes.

  • We use the image tagged with the pull request number
  • We define a revision using --revision-suffix that contains the commit hash
  • We set the database name to include the PR number, so that pull requests use independent databases.
  • We set the production master key using the previously created secret
  • We configure the revision to not receive any traffic.
- name: Deploy Revision On Cloud Run
  run: |
    gcloud run deploy bookstore-preview \
    --image=gcr.io/rails-gcloud-bookstore/bookstore:${{github.event.number}} \
    --platform=managed \
    --port=3000 \
    --revision-suffix=pr-${{github.event.number}}-${{steps.commit.outputs.hash}} \
    --add-cloudsql-instances=rails-gcloud-bookstore:europe-north1:bookstore-db  \
    --set-env-vars DATABASE_HOST=/cloudsql/rails-gcloud-bookstore:europe-north1:bookstore-db \
    --set-env-vars DATABASE_USERNAME='bookstore' \
    --set-env-vars DATABASE_PASSWORD='bookstore' \
    --set-env-vars DATABASE_NAME=bookstore_production_${{github.event.number}} \
    --set-env-vars RAILS_PRODUCTION_KEY=${{ secrets.RAILS_PRODUCTION_KEY }} \
    --allow-unauthenticated \
    --region=europe-north1

We have created a new revision of our bookstore service serving the code from our pull request. Now we only need to make sure the revision is easily reachable. We can accomplish this using the update-traffic command.

Setting a tag allows us to reach to revision we created using some prefix. Assuming a base URL for accessing the bookstore application that looks like https://bookstore-abcd12345-ab.a.run.app and a tag pr-1234 the revision can be reached under https://pr-1234---bookstore-abcd12345-ab.a.run.app.

- name: Update Traffic
  run: |
    gcloud components install beta
    gcloud beta run services update-traffic bookstore-preview \
      --update-tags pr-${{github.event.number}}=bookstore-preview-${{github.event.number}}-${{steps.commit.outputs.hash}} \
      --platform=managed \
      --region=europe-north1

Last but not least, let’s post a comment to our PR which contains the link to the preview environment. We can retrieve the base URL of our bookstore service and change it to include the tag we previously set. Once we have that, we use the add-pr-comment action to post the comment.

- name: Get Preview URL
  id: preview-url
  run: |
    url=$(gcloud run services describe bookstore-preview --format 'value(status.url)' --platform=managed --region=europe-north1 | sed 's|https://|https://pr-${{github.event.number}}---|g')
    echo "::set-output name=url::$url"
- name: Post PR comment with preview deployment URL
  uses: mshick/add-pr-comment@v1
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  with:
    message: |
      Successfully deployed preview at ${{steps.preview-url.outputs.url}}
    allow-repeats: false

Add your GitHub repository as remote for your application and push your local changes.

git remote add origin [email protected]:<your-repository-url>
git add . && git commit -m 'Add pull request reviews'
git push --set-upstream origin master

Opening a pull request should execute the GitHub workflow we defined. Congratulations for making it all the way!

Closing

I hope this guide has given you a rough idea of how Cloud Run and GitHub Actions can be leveraged to create preview deployments for your pull requests.

I took a lot of inspiration from other posts - big shout out to @MaximeHeckel whose in depth post helped me out a lot.

Building on existing solutions to make them work even for a simple Rails application was a nice challenge, and I had a lot of fun working on this. That being said, I’m aware that there is a lot of potential for improving the current solution.

We haven’t taken care of cleaning up after us at all. Right now each pull request creates a new database in our Cloud SQL instance. Both the GitHub Action as well as the container build can be made a lot more performant.

And of course, you can increase the complexity of the base application. How does this solution behave when using ActiveStorage? How can we deal with dependencies to other services, like an internal authentication service? The list goes on and on.

But this guide is long enough as it is.

I leave the rest for you to explore 🙂