CI/CD Pipeline for a Rails + React Application Using GitHub Actions

CI/CD is an essential part of modern software development. It helps teams automatically test, build, and deploy applications whenever code changes.

In this guide, we’ll build an interview-ready understanding of CI/CD using Ruby on Rails API + React + PostgreSQL + GitHub Actions, then look at how CircleCI provides similar capabilities.

1. What Is CI/CD?

Continuous Integration (CI)

CI automatically validates code changes whenever a developer pushes code or opens a pull request.

Typical tasks include:

  • Running RSpec tests.
  • Running React component tests.
  • Checking code quality with RuboCop and ESLint.
  • Building the React application.
  • Running security and dependency checks.

The goal is to detect problems before code reaches the main branch.

Continuous Delivery vs. Continuous Deployment

Continuous Delivery: Every change that passes the pipeline is ready to deploy, but production deployment may require manual approval.

Continuous Deployment: Every change that passes the required checks is automatically deployed to production.

A common workflow is:

Developer pushes code
        |
        v
GitHub Actions starts CI
        |
        v
Run tests and linting
        |
        v
Build React application
        |
        v
All checks pass?
    /         \
   No         Yes
   |           |
 Fail       Merge to main
               |
               v
          Deploy to staging
               |
               v
        Smoke tests / approval
               |
               v
         Deploy to production

⚙️ 2. What Is GitHub Actions?

GitHub Actions is GitHub’s automation platform. It executes workflows when configured events occur, such as a push, pull request, scheduled time, or manual trigger.

Important concepts:

ConceptMeaning
WorkflowComplete automated process defined in YAML
EventTrigger, such as push or pull_request
JobGroup of steps executed on a runner
RunnerMachine that executes a job
StepIndividual command or reusable action
ActionReusable unit of automation
SecretSensitive value such as a deployment token
ArtifactFile produced by a workflow, such as a build output or test report
CacheReusable dependency data that can speed up future runs

A workflow lives inside:

.github/
  workflows/
    ci.yml

GitHub’s official guides explain workflow concepts, Ruby CI, and Node.js CI.

🏗️ 3. GitHub Actions for Rails + React

Assume this project structure:

rails_react_app/
├── app/
├── config/
├── spec/
├── Gemfile
├── .ruby-version
├── .github/
│   └── workflows/
│       └── ci.yml
└── frontend/
    ├── src/
    ├── package.json
    ├── package-lock.json
    └── .nvmrc

We’ll create two CI jobs:

  • rails-test: Prepare PostgreSQL, run database setup, RSpec, and RuboCop.
  • frontend-test: Install JavaScript dependencies, run frontend tests, and build React.

GitHub Actions can execute these independent jobs in parallel, reducing overall pipeline time.

Step 1: Create the workflow

Create .github/workflows/ci.yml:

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

concurrency:
  group: ci-${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true

jobs:
  rails-test:
    runs-on: ubuntu-latest

    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: rails_react_test
        ports:
          - 5432:5432
        options: >-
          --health-cmd "pg_isready -U postgres -d rails_react_test"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5

    env:
      RAILS_ENV: test
      DATABASE_URL: postgresql://postgres:postgres@localhost:5432/rails_react_test

    steps:
      - uses: actions/checkout@v6

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

      - name: Prepare database
        run: bundle exec rails db:prepare

      - name: Run RSpec
        run: bundle exec rspec

      - name: Run RuboCop
        run: bundle exec rubocop

  frontend-test:
    runs-on: ubuntu-latest

    defaults:
      run:
        working-directory: frontend

    steps:
      - uses: actions/checkout@v6

      - uses: actions/setup-node@v7
        with:
          node-version-file: frontend/.nvmrc
          cache: npm
          cache-dependency-path: frontend/package-lock.json

      - name: Install dependencies
        run: npm ci

      - name: Lint React code
        run: npm run lint

      - name: Test React components
        run: npm run test:ci

      - name: Build React
        run: npm run build

This example assumes your Rails project uses RSpec, RuboCop, PostgreSQL, and a React frontend with the specified npm scripts. Configure the frontend’s test:ci script to run your test runner in CI mode, such as vitest run. Adjust the PostgreSQL and runtime versions to match your application.

Step 2: Understand what happens

When a pull request targets main:

  1. GitHub starts the workflow.
  2. Two isolated runner jobs start.
  3. The Rails job prepares PostgreSQL and runs backend checks.
  4. The frontend job installs dependencies, tests React, and produces a production build.
  5. If either job fails, the workflow fails.

Configure these jobs as required status checks in your repository’s branch protection or ruleset settings.

A successful workflow does not automatically mean that code is deployed. This configuration implements CI, not CD.

4. Caching and Artifacts

These two features serve different purposes.

Caching avoids repeatedly downloading dependencies. For example, bundler-cache: true handles Bundler dependency installation and caching, while setup-node can cache npm’s download cache.

Artifacts preserve files generated during a workflow. Examples include test reports, screenshots, coverage reports, or compiled application output. Use artifact upload/download actions when another job needs those files.

Do not confuse dependency caches with deployable release artifacts. For more detail, see GitHub’s caching guide and artifact guide.

5. How Do We Implement CD?

After CI passes and the code is merged into main, we can add a deployment job.

A simplified example:

  deploy-staging:
    needs: [rails-test, frontend-test]
    if: >-
      github.event_name == 'push' &&
      github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging

    permissions:
      contents: read

    steps:
      - uses: actions/checkout@v6

      - name: Deploy application
        run: ./script/deploy-staging.sh

The deployment script must implement the actual deployment for your platform. For example, it could deploy a container image to a container platform or invoke your hosting provider’s deployment tooling.

For production, configure a separate GitHub environment with restricted deployment branches and approval rules where appropriate.

Security best practices:

  • Store credentials in GitHub Actions secrets or environment secrets, never in source code.
  • Prefer OpenID Connect (OIDC) with short-lived cloud credentials when supported, rather than long-lived cloud access keys.
  • Restrict workflow permissions to the minimum required.
  • Pin third-party actions to reviewed commit SHAs in security-sensitive production workflows.
  • Run database migrations as part of a controlled release process, and make migrations compatible with rolling deployments.

See GitHub Actions deployment security for OIDC and environment protection.

6. How Does CircleCI Fit In?

CircleCI is another CI/CD platform. It provides the same broad capabilities: executing tests, building applications, caching dependencies, coordinating jobs, and deploying successful builds.

The main difference is the configuration platform and its integration with your repository.

In GitHub Actions, workflow files live in .github/workflows/. In CircleCI, the primary configuration file is:

.circleci/config.yml

A simplified CircleCI configuration for the same Rails + React project looks like this:

version: 2.1

jobs:
  test:
    docker:
      - image: cimg/ruby:3.3-node
      - image: cimg/postgres:16.0
        environment:
          POSTGRES_USER: postgres
          POSTGRES_PASSWORD: postgres
          POSTGRES_DB: rails_react_test

    environment:
      RAILS_ENV: test
      DATABASE_URL: postgresql://postgres:postgres@127.0.0.1:5432/rails_react_test

    steps:
      - checkout

      - run: bundle install
      - run: bundle exec rails db:prepare
      - run: bundle exec rspec
      - run: bundle exec rubocop

      - run:
          name: Test and build React
          command: |
            cd frontend
            npm ci
            npm run lint
            npm run test:ci
            npm run build

workflows:
  test:
    jobs:
      - test

This is a starter example. Match the Ruby and Node versions to your project, configure database readiness and dependency caching, and verify that the selected images meet your requirements. A production setup would typically separate backend and frontend jobs when parallel execution is beneficial.

How do we integrate CircleCI?

  1. Create a CircleCI account and connect your GitHub repository.
  2. Add .circleci/config.yml.
  3. Configure project permissions, environment variables, secrets, and required contexts.
  4. Run the pipeline and inspect failures in CircleCI’s job logs.
  5. Add deployment jobs that depend on successful test jobs and use protected deployment environments.

For more details, see the CircleCI configuration guide and JavaScript quickstart.

GitHub Actions vs. CircleCI: GitHub Actions is a natural starting point when your source code and pull requests already live on GitHub. CircleCI is also a strong option, especially when a team already uses its workflows, caching, test-splitting capabilities, and deployment integrations. You usually don’t need both platforms running the same pipeline unless there is a clear reason.

🎯 7. Questions to Remember

Q: What is the difference between CI and CD?

CI automatically builds and tests code changes. CD automates making validated changes ready for release or deploying them, depending on whether the process is continuous delivery or continuous deployment.

Q: Why use separate jobs for Rails and React?

They have different runtime dependencies and can run independently. Separate jobs allow parallel execution, clearer logs, and more targeted debugging.

Q: What happens if the tests fail?

The job fails, and dependent jobs such as deployment are skipped. Configure branch protection to prevent merging unless the required checks pass.

Q: How do you make a pipeline faster?

Cache dependencies, parallelize independent jobs, split large test suites, remove redundant work, and run expensive E2E tests selectively.

Q: How do you secure deployments?

Use least-privilege workflow permissions, protected environments, restricted secrets, reviewed workflow actions, and short-lived cloud credentials through OIDC where supported.

🎯 Final Recommendation

For a new Rails API + React application, start with GitHub Actions because it integrates directly into your repository and pull-request workflow.

Build the pipeline incrementally: lint and test → build → deploy to staging → smoke test → deploy to production. Add caching, parallelization, protected environments, security scans, and deployment approvals as your application’s requirements grow.

The goal of CI/CD is not simply to automate deployments. It is to make every release repeatable, testable, secure, and recoverable.