Ruby on Rails

Making sure local CI is green before deploying to Heroku


Note: this article assumes you’re using GitHub, something similar can probably also be cooked up for other hosting providers.

Rails 8.1 was released recently with a new ‘Local CI’ feature. This allows you to easily define and run the steps you traditionally run on an external system (like GitHub Actions) on your own machine. Modern hardware is fast enough to even run large test suites nowadays.

One benefit of using an external system is that they can run your CI steps structurally and independently and you can prevent PRs from being merged if the linters and test suites are failing, adding famous green checks to your pull requests.

Local CI also does this by including a “signoff” step which basically signals to GitHub that your commit has successfully run the CI suite locally (Works On My Machine™). This is great and allows you to keep some certainty of the state of your code and your pull requests.

With local CI there is a missing piece though: once a PR gets merged there is no final check for the merge commit on your main branch. All external tools I know run another build for the merge commit and I usually wait for those before deploying (or automated deploy processes usually do).

Since I deploy directly to Heroku through the command line (git push heroku main 🤘) there is no such check anymore and I have to remember to run and check the command myself.

To prevent myself from forgetting I’ve written a small git pre push hook that runs the local CI suite if necessary and holds the push if it fails. This serves as a nice backstop while keeping all the benefits from running everything locally.

What it basically does:

  • Check if the current commit has already been signed off.
  • If not: run the CI suite (which then marks the commit as signed off).
  • Blocks the push if it fails, else continue and deploy.

This gives me peace of mind that no bugs will make it to production (right 🥹).

Setup

  • Install the gh cli tool and authenticate with GitHub (gh auth login).
  • Create a new file in your app’s folder: .git/hooks/pre-push:
#!/usr/bin/env bash

set -euo pipefail

# Pre-push hook that only runs on main branch when pushing to Heroku

remote="$1"

while IFS=' ' read -r local_ref local_sha remote_ref remote_sha; do
  # Only check main branch pushes to Heroku
  if [[ "$remote_ref" != "refs/heads/main" ]] || [[ "$remote" != "heroku" ]]; then
    continue
  fi

  echo "Pushing to main branch on Heroku..."
  echo "Checking if commit $local_sha has passed status checks on GitHub..."

  # Get commit status from GitHub
  status_response=$(gh api "repos/:owner/:repo/commits/$local_sha/status")
  status=$(echo "$status_response" | jq -r '.state // "unknown"')

  case "$status" in
  success)
    echo "✅ Commit has passed GitHub status checks, skipping bin/ci"
    ;;
  pending)
    echo "⏳ GitHub checks are still pending. Running local CI..."
    bin/ci || {
      echo "❌ bin/ci failed! Push aborted."
      exit 1
    }
    echo "✅ bin/ci passed!"
    ;;
  failure | error)
    echo "❌ GitHub checks failed with status: $status"
    echo "Running local CI to verify..."
    bin/ci || {
      echo "❌ bin/ci failed! Push aborted."
      exit 1
    }
    echo "✅ bin/ci passed!"
    ;;
  *)
    echo "⚠️ Unknown GitHub status: '$status'. Running local CI..."
    bin/ci || {
      echo "❌ bin/ci failed! Push aborted."
      exit 1
    }
    echo "✅ bin/ci passed!"
    ;;
  esac
done

exit 0

And you’re done ✅! Only green commits will be deployed to Heroku.

Need help with Ruby on Rails? I specialize in building scalable Rails applications and can help optimize your existing codebase.

Get in touch