CI/CD Pipeline with GitHub Actions

Build a pipeline that lints, tests, and deploys a React app automatically.

Introduction

30 Second Summary

Every time you change your code, there is a gap between "it works on my machine" and "it is live for users." Bridging that gap manually is slow, error-prone, and honestly boring.

In this project, you will build a CI/CD pipeline with GitHub Actions that automatically tests and deploys a React app every time you push code. No more manual deployments or untested code reaching your users.

What You'll Build

A complete CI/CD pipeline that takes a React app from code push to live deployment automatically, running linting, testing, and building without you lifting a finger.

By the end of this project, you'll have:

  • A Vite React app with automated unit testing powered by Vitest.
  • A GitHub Actions workflow YAML file with lint, test, and deploy jobs that run on every push.
  • A live site on GitHub Pages that auto-updates whenever you push to main.
  • Secret Mission: Add a pull request workflow that runs checks on PRs before they can be merged.

Are there any prerequisites?

You should be familiar with the basics of React, Node.js, Git, and GitHub (pushing, committing, and creating repositories). No prior CI/CD experience is needed.

Before We Start

Before We Start...

Scaffold the React App and Push to GitHub

Every CI/CD pipeline needs code to deploy. Before you can automate builds, tests, and deployments, you need a working app and a repository that the pipeline can monitor for changes.

In this step, you'll use Vite to scaffold a React app and push it to a public GitHub repository. This gives your future pipeline something to build, test, and deploy.

In this step, get ready to:
  • Scaffold a React app with Vite and verify it runs locally.
  • Push the project to a new GitHub repository.
  • Confirm your code is visible on GitHub.
Scaffold the React app

Vite is a modern build tool for web apps. Its scaffold command creates a ready-to-run React project with a dev server, build configuration, and linting already wired up.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your React app by running this command:
npm create vite@latest ci-cd-react-app -- --template react

What does this command do?

  • npm create vite@latest runs Vite's project scaffold tool at its latest version.
  • ci-cd-react-app names your new project folder.
  • -- --template react tells Vite to use the React template, which includes JSX support and a dev server.

Seeing a permission prompt?

npm may ask you to confirm installing the create-vite package. Type y and press Enter to proceed.

  • Move into the project folder and install dependencies by running these commands:
cd ci-cd-react-app
npm install

What does npm install do?

npm install reads the package.json file and downloads all the libraries your React app needs into a node_modules folder. The scaffold only creates the configuration. This command fetches the actual code.

  • Start the development server by running:
npm run dev
  • Open your browser and navigate to http://localhost:5173.
  • You should see the default Vite + React page with the Vite and React logos and a counter button.

Don't see the app in your browser?

  • Check that your terminal shows a local URL (it prints the address when the server starts). Copy that URL into your browser.
  • Make sure npm install finished without errors. If you see red error text, try deleting the node_modules folder and running npm install again.
  • help me debug why my Vite dev server isn't starting
  • Press Ctrl+C in your terminal to stop the development server.
Create a GitHub repository and push your code

Your app needs a home on GitHub so the CI/CD pipeline can detect code changes and run automatically. You'll create a public repository because GitHub Actions is free for public repositories with unlimited minutes.

  • Open your browser and navigate to https://github.com/new.
  • Enter ci-cd-react-app in the Repository name field.
  • Select Public for repository visibility.
  • Leave all checkboxes unchecked (no README, no .gitignore, no license).
  • Click Create repository.

Why skip the README and .gitignore?

The Vite scaffold already created a .gitignore file for you. Adding one from GitHub would create a conflict when you push. You want a completely empty repository so your local code pushes cleanly.

  • Back in your terminal, make sure you are inside the ci-cd-react-app folder on your Desktop.
  • Initialize Git and push your code to GitHub by running these commands:
git init
git add .
git commit -m "Initial commit"
git branch -M main
git remote add origin [[REPO_URL="https://github.com/your-username/ci-cd-react-app.git"]]
git push -u origin main

What do these commands do?

  • git init turns your project folder into a Git repository.
  • git add . stages all files for the first commit.
  • git commit -m "Initial commit" saves a snapshot of your code with a message.
  • git branch -M main ensures your default branch is named main.
  • git remote add origin connects your local repo to the GitHub repository you just created.
  • git push -u origin main uploads your code to GitHub.

Find your repository URL

After creating the repository, GitHub shows a Quick Setup page with your repository URL. Copy the HTTPS URL from that page and use it in the git remote add origin command.

Push failed or authentication error?

  • If you see a permission or authentication error, GitHub requires a personal access token or SSH key instead of a password. Check that your Git credentials are configured.
  • If you see an error about the remote already existing, run git remote remove origin and then try the git remote add origin command again with the correct URL.
  • help me fix my git push authentication error
Verify your repository on GitHub
  • In your browser, navigate to your repository at https://github.com/your-username/ci-cd-react-app.
  • You should see all your project files listed, including package.json, vite.config.js, and the src/ folder.
  • Click into the src/ folder and confirm you see App.jsx listed.

Repository looks empty?

  • Go back to your terminal and run git status. If it says files are untracked or uncommitted, repeat the git add . and git commit commands.
  • Make sure you ran the commands from inside the ci-cd-react-app folder, not from the Desktop.
  • help me figure out why my GitHub repository is empty after pushing

Your React app is live on GitHub and ready for automation. Next up, you'll add linting and testing so your pipeline has real checks to run.

Add Linting and Testing with Vitest

Your React app is on GitHub. Before you automate anything, your pipeline needs quality checks to run against your code.

A CI/CD pipeline is only as useful as the checks it runs. In this step, you'll set up Vitest (a testing framework built for Vite) and confirm that both linting and testing pass locally before you automate them.

In this step, get ready to:
  • Install testing dependencies and configure Vitest.
  • Write a unit test for the App component.
  • Run lint and test commands locally to confirm they pass.
Install testing dependencies and configure Vitest

The Vite scaffold includes linting out of the box, but testing requires a few additional packages.

  • In your terminal, make sure you're inside the ci-cd-react-app folder.
  • Install Vitest, jsdom, and React Testing Library as dev dependencies by running:
npm install -D vitest jsdom @testing-library/react

What are these packages?

vitest is the test runner. It integrates natively with Vite so it shares the same config and transforms.

jsdom simulates a browser environment so your React components can render during tests without a real browser.

@testing-library/react provides utilities like render and screen for testing components the way a user would interact with them.

  • You should see packages added to your devDependencies in the terminal output.

Install failed?

Make sure you're inside the ci-cd-react-app folder (where package.json lives). If you get permission errors on macOS, try prefixing with sudo.

Help me fix my npm install error for Vitest

Next, you need to tell your project how to run tests. The Vite scaffold created a package.json with scripts for dev, build, lint, and preview. You need to add a test script.

  • Open package.json in VS Code.
  • Find the "scripts" section. It looks like this:
"scripts": {
  "dev": "vite",
  "build": "vite build",
  "lint": "eslint .",
  "preview": "vite preview"
}
  • Add a "test" script after the "preview" line, so the scripts section now looks like this:
"scripts": {
  "dev": "vite",
  "build": "vite build",
  "lint": "eslint .",
  "preview": "vite preview",
  "test": "vitest run"
}

Why vitest run instead of just vitest?

Running vitest without run starts watch mode, which re-runs tests whenever files change. That's great for local development.

The vitest run command executes tests once and exits. This is what your CI pipeline needs. Vitest also auto-detects CI environments and switches to run mode automatically, but being explicit in your script makes the intent clear.

  • Save package.json.

Finally, Vitest needs to know which environment to use when running tests. Since React components render in the browser, you need to tell Vitest to simulate one using jsdom.

  • Open vite.config.js and update it to include the test configuration:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  // Tell Vitest to simulate a browser environment for React tests
  test: {
    environment: 'jsdom',
  },
})

What does this config do?

The test block tells Vitest to use jsdom as the test environment. This creates a simulated browser (with document, window, etc.) so your React components can render during tests.

Vitest reads vite.config.js automatically. No separate test config file needed.

  • Save vite.config.js.

✔️ Awesome, I've got everything!

Great. Double check you've saved both files before moving on.

ⓧ I'd like to double check the full code

Here's the complete vite.config.js file:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  test: {
    environment: 'jsdom',
  },
})
Write a unit test for the App component

With Vitest configured, you need an actual test to run. You'll write a simple test that checks whether the App component renders its heading correctly.

  • Create a new file called App.test.jsx inside the src folder.
  • Add the following test code:
import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import App from './App'

// Group related tests under a describe block
describe('App', () => {
  // Check that the main heading renders
  it('renders the Vite + React heading', () => {
    render(<App />)
    expect(screen.getByText('Vite + React')).toBeDefined()
  })
})

What does this test do?

  • describe('App', ...) groups all tests for the App component together.
  • render(<App />) renders the component into the simulated jsdom browser.
  • screen.getByText('Vite + React') looks for an element containing that text (the <h1> in App.jsx).
  • expect(...).toBeDefined() asserts that the element exists. If it doesn't, the test fails.
  • Save src/App.test.jsx.

✔️ Awesome, I've got everything!

Great. Make sure the file is saved before running your tests.

ⓧ I'd like to double check the full code

Here's the complete src/App.test.jsx file:

import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import App from './App'

describe('App', () => {
  it('renders the Vite + React heading', () => {
    render(<App />)
    expect(screen.getByText('Vite + React')).toBeDefined()
  })
})
Run lint and test commands locally

Before you automate these checks in a pipeline, confirm they pass on your machine first. This way you know any future failures are real problems, not setup issues.

  • Run the linter by running this command:
npm run lint
  • You should see no errors or warnings in the output. A clean lint run produces no output at all.

What is linting?

Linting is automated code quality checking. ESLint (the linter included in the Vite scaffold) scans your code for common mistakes, unused variables, and style issues. It catches problems without running your app.

  • Now run the tests by running this command:
npm run test
  • You should see output showing 1 test passed with a green checkmark. Look for something like 1 passed in the results.

Test failing or erroring out?

  • If you see an error about jsdom not being found, double check that vite.config.js has test: { environment: 'jsdom' } and that you saved the file.
  • If the test itself fails (not an error, but an assertion failure), make sure your App.jsx still contains <h1>Vite + React</h1>. The test looks for that exact text.
  • If you see a module not found error for @testing-library/react, re-run the install command from earlier.

Help me debug my failing Vitest test

Both checks pass locally. You now have a linter and a test suite that can catch problems before code reaches production. Next up, you'll create a GitHub Actions workflow that runs these checks automatically on every push.

Create the GitHub Actions CI/CD Workflow

Your React app has linting and testing ready to go locally. The next step is to automate these checks so they run every time you push code.

You'll create a GitHub Actions workflow file that defines your entire CI/CD pipeline. This single YAML file tells GitHub exactly what to do with your code after every push: lint it, test it, and deploy it.

In this step, get ready to:
  • Create the workflow directory and YAML file.
  • Write a workflow with lint, test, and deploy jobs.
  • Configure job dependencies so deploy waits for checks to pass.
Create the workflow directory and file

GitHub Actions looks for workflow files in a specific directory: .github/workflows/. Any YAML file placed here becomes a pipeline that GitHub can run.

  • In your terminal, make sure you're inside the ci-cd-react-app folder. Create the workflow directory by running this command:
mkdir -p .github/workflows
  • Create the workflow file by running:
touch .github/workflows/deploy.yml
  • Confirm the directory and file exist by running:
ls .github/workflows/
  • You should see deploy.yml in the output.

Why the .github directory?

GitHub automatically scans the .github/workflows/ directory in every repository. Any valid YAML file it finds there becomes a workflow it can execute. The dot prefix (.github) makes it a hidden folder on most systems.

Write the workflow header with lint and test jobs

The workflow file defines when the pipeline triggers and what jobs it runs. You'll start with the configuration header and two checking jobs: one for linting and one for testing.

  • Open .github/workflows/deploy.yml in VS Code and paste the following code:
name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm run test

What does this code do?

The top section configures when and how the workflow runs:

  • on: push: branches: - main triggers the workflow every time code is pushed to the main branch.
  • permissions grants the workflow access to read your code and write to GitHub Pages.
  • concurrency ensures only one deployment runs at a time. If you push again while one is running, the old run gets cancelled.

The lint and test jobs each check out your code, install dependencies with npm ci, and run one check. They run in parallel on separate Ubuntu machines.

Why npm ci instead of npm install?

The npm ci command is designed for CI environments. It installs exact versions from your package-lock.json and deletes any existing node_modules first. This makes builds reproducible and faster than npm install.

Configure job dependencies with the deploy job

The deploy job should only run after both lint and test pass. If either check fails, deployment must be blocked. This is the core safety mechanism of CI/CD.

  • Add the deploy job below the test job in deploy.yml. Make sure it's indented at the same level as lint: and test: (two spaces under jobs:):
  deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest
    needs: [lint, test]
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Setup Pages
        uses: actions/configure-pages@v5

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: './dist'

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

What does this code do?

The needs: [lint, test] line is the key. It tells GitHub Actions that this job depends on both lint and test completing successfully. If either one fails, the deploy job is skipped entirely.

The deploy job builds your app with npm run build, packages the dist folder as an artifact, and pushes it to GitHub Pages.

  • Save deploy.yml.

✔️ Awesome, I've got everything!

Double check you've saved the file. YAML is very sensitive to indentation, so make sure everything lines up correctly.

ⓧ I'd like to double check the full code

name: Deploy to GitHub Pages

on:
  push:
    branches:
      - main

permissions:
  contents: read
  pages: write
  id-token: write

concurrency:
  group: pages
  cancel-in-progress: true

jobs:
  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run ESLint
        run: npm run lint

  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Run tests
        run: npm run test

  deploy:
    name: Build and Deploy
    runs-on: ubuntu-latest
    needs: [lint, test]
    environment:
      name: github-pages
      url: ${{ steps.deployment.outputs.page_url }}
    steps:
      - name: Checkout code
        uses: actions/checkout@v6

      - name: Setup Node.js
        uses: actions/setup-node@v6
        with:
          node-version: '20'
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Build
        run: npm run build

      - name: Setup Pages
        uses: actions/configure-pages@v5

      - name: Upload artifact
        uses: actions/upload-pages-artifact@v4
        with:
          path: './dist'

      - name: Deploy to GitHub Pages
        id: deployment
        uses: actions/deploy-pages@v4

Seeing YAML errors in VS Code?

YAML relies on consistent indentation (spaces only, no tabs). Each nested level uses exactly 2 spaces. If your editor shows red squiggles, check that you haven't mixed tabs and spaces.

help me fix indentation errors in my deploy.yml file

  • Commit and push the workflow to GitHub by running these commands:
git add .
git commit -m "Add CI/CD workflow"
git push
  • Open your repository on GitHub in your browser.
  • Click the Actions tab at the top of your repository page.

You should see your workflow running (or recently completed). The lint and test jobs will pass, but the deploy job will fail. That's expected because GitHub Pages isn't configured in your repository settings yet.

Why does the deploy job fail?

GitHub Pages needs to be explicitly enabled in your repository settings before the deploy job can publish anything. You'll do this in the next step. The important thing is that lint and test are already working.

Your CI/CD pipeline is live. Lint and test jobs are catching problems automatically. Next up, you'll configure GitHub Pages and get the deploy job working so your app goes live on every push.

Configure GitHub Pages and Trigger the Pipeline

Your workflow is committed and waiting on GitHub, but it needs somewhere to deploy to. GitHub Pages can host your built React app for free, and your workflow already has the deploy job ready to push to it.

There are two things left to connect: Vite needs to know the URL path your site lives at, and GitHub needs to know that your workflow is responsible for deployments. Once both are set, pushing code will trigger the full pipeline and put your app live.

In this step, get ready to:
  • Update your Vite config with the correct base path for GitHub Pages.
  • Enable GitHub Pages with GitHub Actions as the source.
  • Push your changes and watch the pipeline deploy your app live.
Update the Vite base path for GitHub Pages

When you deploy to a GitHub Pages project site, your app lives at a subpath like username.github.io/ci-cd-react-app/. Vite needs to know this path so it loads CSS, JavaScript, and images from the correct location.

  • In VS Code, open vite.config.js.
  • Find this line:
plugins: [react()],
  • Add the following line directly below it:
base: '/ci-cd-react-app/',

Your full vite.config.js should now look like this:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: '/ci-cd-react-app/',
  test: {
    environment: 'jsdom',
  },
})

Why does the base path matter?

Without base, Vite assumes your app lives at the root (/). On GitHub Pages, project sites live at /repo-name/, so all asset URLs need that prefix. If you skip this, your deployed site loads a blank page because the browser looks for files in the wrong location.

  • Save vite.config.js.

✔️ Awesome, I've got everything!

Great. Double check you have saved your file before moving on.

ⓧ I'd like to double check the full code

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  base: '/ci-cd-react-app/',
  test: {
    environment: 'jsdom',
  },
})
Enable GitHub Pages in your repository settings

GitHub needs to know that your workflow handles deployments instead of the default branch-based publishing. You configure this in the repository settings on GitHub.

  • Open your browser and go to your ci-cd-react-app repository on GitHub.
  • Click Settings in the top navigation bar of your repository.
  • In the left sidebar under Code and automation, click Pages.
  • Under Build and deployment, find the Source dropdown.
  • Select GitHub Actions from the dropdown.

What does this setting do?

By selecting GitHub Actions as the source, you tell GitHub that your workflow is responsible for building and deploying the site. GitHub will not try to build your code with Jekyll or look for a specific branch. It will wait for your workflow to upload the deployment artifact.

Don't see the Settings tab?

The Settings tab is only visible if you are the repository owner or have admin permissions. Make sure you are logged in and looking at your own repository, not a fork you do not own.

help me find the Settings tab in my GitHub repository

Push your changes and watch the pipeline run

Everything is wired up. Pushing the updated config to main triggers your workflow automatically.

  • Commit and push your changes by running these commands in the terminal:
git add .
git commit -m "Add base path for GitHub Pages"
git push

What happens after you push?

Your workflow triggers because the on: push event matches the main branch. The lint and test jobs run in parallel. Once both pass, the deploy job builds your app, uploads the artifact, and publishes it to GitHub Pages.

  • Go back to your repository on GitHub.
  • Click the Actions tab at the top.
  • Click on the latest workflow run (it will be named after your commit message).
  • Watch the three jobs (Lint, Test, Build and Deploy) complete with green checkmarks.

Once all three jobs pass, your site is live.

  • Go back to Settings then Pages in your repository.
  • Click the Visit site link at the top of the page to open your live app.

You should see the default Vite + React starter page with the Vite and React logos and a counter button.

Seeing a 404 or blank page?

If you see a 404 error, the workflow may still be running. Wait a minute and refresh the page.

If the page is blank (white screen), check that base: '/ci-cd-react-app/' in vite.config.js matches your repository name exactly, including the forward slashes.

If the workflow failed, click the red X in the Actions tab to see which job failed and read the error logs.

help me debug my GitHub Pages deployment

Your pipeline is fully operational. Code goes in, linting and tests run, and a live site comes out the other end. Next up, you will break the build on purpose and see how CI protects your production site from bad code.

Break the Build and Watch CI Protect You

Your pipeline is live and your site is deployed. But how do you know the pipeline is actually protecting you from shipping broken code?

In this step, you will deliberately break your test to simulate a real-world scenario where a bug slips into the codebase. You will watch the pipeline catch it and block deployment, then fix it and confirm everything recovers.

In this step, get ready to:
  • Introduce a failing test to simulate a bug.
  • Push broken code and watch the pipeline block deployment.
  • Fix the test and confirm the pipeline recovers.
Introduce a failing test

Your test currently checks that the heading Vite + React renders on the page. You will change the expected text to something that does not exist in the app, which guarantees the test fails.

  • In VS Code, open src/App.test.jsx.
  • Find this line inside the test:
    expect(screen.getByText('Vite + React')).toBeDefined()
  • Replace 'Vite + React' with 'Wrong Text', so the line now looks like this:
    expect(screen.getByText('Wrong Text')).toBeDefined()
  • Save src/App.test.jsx.

Why 'Wrong Text'?

Your app renders a heading that says Vite + React. By telling the test to look for Wrong Text instead, the test will fail because that string does not exist anywhere in the rendered output.

Push the broken code and observe the pipeline fail

Now you will push this intentionally broken test to GitHub. The pipeline will run automatically and the test job will fail, which blocks the deploy job from running.

  • Commit and push the broken test by running these commands:
git add .
git commit -m "break test to demo CI"
git push
  • Open your repository on GitHub in your browser.
  • Click the Actions tab at the top of your repository.
  • Click on the latest workflow run (it will be named break test to demo CI).
  • Wait for the run to complete. This takes about 30-60 seconds.

You should see the Test job marked with a red X, indicating failure. The Deploy job is skipped entirely because it depends on the test job passing.

What just happened?

This is Continuous Integration doing its job. The needs: [lint, test] line in your workflow means the deploy job will only run if both lint and test succeed.

Your live site is still showing the last working version. Broken code never reached production.

Pipeline passed when it should have failed?

Make sure you saved src/App.test.jsx before committing. Unsaved changes are not picked up by git add.

Double check that you changed the string inside getByText() and not somewhere else in the file.

help me figure out why my pipeline passed when I expected it to fail

Fix the test and confirm the pipeline recovers

Now you will revert the test back to its correct state and push again. This proves the pipeline recovers and deploys successfully once the code is fixed.

  • In src/App.test.jsx, find this line:
    expect(screen.getByText('Wrong Text')).toBeDefined()
  • Replace 'Wrong Text' with 'Vite + React', so the line looks like this again:
    expect(screen.getByText('Vite + React')).toBeDefined()
  • Save src/App.test.jsx.
  • Commit and push the fix by running these commands:
git add .
git commit -m "fix test"
git push
  • Go back to the Actions tab in your GitHub repository.
  • Click on the latest workflow run (named fix test).
  • Wait for the run to complete.

You should see all three jobs marked with green checkmarks: Lint, Test, and Build and Deploy.

  • Visit your live site to confirm it is still working correctly.

Why does this matter?

You just experienced the full CI/CD safety loop. Broken code was caught before it reached users. The fix went through the same automated checks and only deployed after passing.

In a professional team, this means no one can accidentally ship a bug to production by pushing directly to main. The pipeline is the gatekeeper.

Pipeline still failing after the fix?

Confirm that the string in your test exactly matches the heading in src/App.jsx. The heading renders Vite + React (capital V and R, with spaces around the plus sign).

Make sure you committed and pushed after saving. Run git status to check for uncommitted changes.

help me debug why my pipeline is still failing after I fixed the test

✔️ Awesome, I've got everything!

Great. Double check you have saved src/App.test.jsx with the correct test restored.

ⓧ I'd like to double check the full code

Here is the complete src/App.test.jsx file as it should look now:

import { describe, it, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import App from './App'

describe('App', () => {
  it('renders the Vite + React heading', () => {
    render(<App />)
    expect(screen.getByText('Vite + React')).toBeDefined()
  })
})

Secret mission

Add a Pull Request Check Workflow

Real engineering teams never merge code directly to main. They open pull requests that must pass automated checks first. Can you add a separate CI workflow that gates every PR?

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

This project uses GitHub Actions and GitHub Pages on a public repository, which are both completely free. There are no ongoing costs.

Resources you used:

  • The ci-cd-react-app local project folder on your Desktop.
  • The ci-cd-react-app GitHub repository (includes GitHub Pages site and Actions workflows).

Keep everything running

No action needed. Choose this if you're still actively building or want to keep your live GitHub Pages site running.

  • Your GitHub Pages site stays live at its URL.
  • GitHub Actions continues to run on every push to main.
  • Your local project folder remains on your Desktop.

Pause - I'll come back to this later

Disable the live site and stop workflow runs while keeping your code intact.

  • In your GitHub repository, click Settings.
  • In the left sidebar under Code and automation, click Pages.
  • Under Build and deployment, change the Source dropdown to None and click Save. This takes down your live site.
  • Your repository and local files stay intact. You can re-enable Pages later by setting the source back to GitHub Actions.

Delete - I don't want to use this again

Remove all project resources completely. This cannot be undone.

  • Delete the GitHub repository:
  • In your GitHub repository, click Settings.
  • Scroll down to the Danger Zone section.
  • Click Delete this repository.
  • Click I want to delete this repository, type the repository name to confirm, then click Delete this repository again.

This also removes your GitHub Pages site and all Actions history.

  • Delete the local project folder:

macOS

  • Remove the project folder by running:
rm -rf ~/Desktop/ci-cd-react-app

Windows

  • Remove the project folder by running:
rmdir /s /q %USERPROFILE%\Desktop\ci-cd-react-app

Nice Work!

Nice Work!

You just built a professional CI/CD pipeline from scratch. Every time you push code to main, your pipeline automatically lints, tests, and deploys your app to a live URL.

You've learned how to:

  • Create a GitHub Actions workflow with lint, test, and deploy jobs that run automatically on every push.
  • Deploy a live React app to GitHub Pages that auto-updates whenever new code lands on main.
  • Watch CI protect your production site by catching a broken build before it reaches users.
  • Secret Mission: Added pull request checks and branch protection rules like a professional engineering team.

Ready to quiz yourself?