DEV Community

Cover image for Automating CI/CD Pipelines for Cypress Testing with GitHub Actions
Sanskriti Harmukh for Vultr

Posted on with Aashish Chaurasiya Originally published at docs.vultr.com

Automating CI/CD Pipelines for Cypress Testing with GitHub Actions

Continuous Integration and Continuous Deployment (CI/CD) pipelines streamline software development by automating code integration, testing, and deployment, and incorporating end-to-end (E2E) testing ensures that code changes align with real user workflows before deployment. GitHub Actions and Cypress offer a seamless solution for automating testing, reducing manual effort, and accelerating feedback cycles. This guide walks through building a sample Next.js application, writing a Cypress E2E test for it, and wiring both into a GitHub Actions CI/CD pipeline. By the end, you'll have a workflow that runs Cypress tests on every push to main, uploads screenshots for failed tests, and sends Slack alerts when a run fails.


Prerequisites

  • Create a GitHub repository to store your project and set up new workflows. For example, cypress-dev-test.
  • Have access to a Linux desktop workstation such as Ubuntu 24.04.
  • Install and initialize Git on your workstation.
  • An existing Slack Workspace to receive notifications for Cypress tests.

Create a Sample Next.js Application

Next.js is a React framework that streamlines the development of production-ready web applications with features such as server-side rendering, static site generation, and API routes.

1. Update the APT package index:

$ sudo apt update
Enter fullscreen mode Exit fullscreen mode

2. Install Node.js and NPM:

$ sudo apt install nodejs npm -y
Enter fullscreen mode Exit fullscreen mode

3. Verify that the installed Node.js version is 18.x or later:

$ node -v
Enter fullscreen mode Exit fullscreen mode

Your output should be similar to the one below:

v18.19.1
Enter fullscreen mode Exit fullscreen mode

4. Switch to your user's home directory:

$ cd
Enter fullscreen mode Exit fullscreen mode

5. Initialize a Next.js project using npx. Replace my-project with your desired project name:

$ npx create-next-app@latest my-project
Enter fullscreen mode Exit fullscreen mode

Press Enter when prompted to install the create-next-app package, and press Enter again to keep the default options for TypeScript, ESLint, Tailwind CSS, the src/ directory, App Router, Turbopack, and the import alias.

6. Switch to the my-project directory:

$ cd my-project
Enter fullscreen mode Exit fullscreen mode

7. List all files and verify the project directory structure:

$ ls
Enter fullscreen mode Exit fullscreen mode

8. Allow connections to the Next.js application port 3000 through the firewall:

$ sudo ufw allow 3000
Enter fullscreen mode Exit fullscreen mode

Run the following command to install UFW if it's unavailable and allow SSH connections:

$ sudo apt install ufw -y && sudo ufw allow ssh
Enter fullscreen mode Exit fullscreen mode

9. Reload UFW to apply the firewall configuration changes:

$ sudo ufw reload
Enter fullscreen mode Exit fullscreen mode

10. Start the development server:

$ npm run dev
Enter fullscreen mode Exit fullscreen mode

11. Access the Next.js application port 3000 using your server's public IP address in a web browser and verify that the default Next.js page displays:

http://<SERVER-IP>:3000
Enter fullscreen mode Exit fullscreen mode

Press Ctrl + C in your terminal session to stop the development server.

Create a Contact Form

Simulate user interactions, allowing end-to-end (E2E) testing to detect usability issues, using a contact form with first name, last name, email, and message fields. The contact form logs the output to the console to validate the user input.

1. Print your working directory and verify it's the Next.js project:

$ pwd
Enter fullscreen mode Exit fullscreen mode

2. Back up the default app/page.tsx file:

$ mv app/page.tsx app/page.tsx.ORIG
Enter fullscreen mode Exit fullscreen mode

3. Create the app/page.tsx file using a text editor such as nano:

$ nano app/page.tsx
Enter fullscreen mode Exit fullscreen mode

4. Add the following contact form components to the file:

"use client";
import { useState } from "react";

export default function Contact() {
  const [data, setData] = useState({
    firstName: "",
    lastName: "",
    email: "",
    message: "",
  });

  const handleChange = (event: React.SyntheticEvent) => {
    const target = event.target as HTMLInputElement;
    setData((prev) => ({ ...prev, [target.name]: target.value }));
  };

  const handleSubmit = async (event: React.SyntheticEvent) => {
    event.preventDefault();
    console.log("Form Submitted:", data);
    setData({ firstName: "", lastName: "", email: "", message: "" });
  };

  return (
    <main className="w-[100%] h-[100%] absolute top-0 left-0 bg-[#8dd0fa] flex flex-col items-center justify-center">
      <h1 className="text-[26px] font-bold mb-4 text-black uppercase">
        CONTACT US
      </h1>
      <form
        onSubmit={handleSubmit}
        className="w-[450px] flex flex-col items-center text-black"
      >
        <div className="w-full flex justify-between my-2">
          <input
            type="text"
            name="firstName"
            placeholder="First Name"
            onChange={handleChange}
            value={data.firstName}
            required
            className="outline-none w-[48%] h-10 p-2 rounded shadow-sm"
          />
          <input
            type="text"
            name="lastName"
            placeholder="Last Name"
            onChange={handleChange}
            value={data.lastName}
            required
            className="outline-none w-[48%] h-10 p-2 rounded shadow-sm"
          />
        </div>
        <input
          type="email"
          name="email"
          placeholder="Email"
          onChange={handleChange}
          value={data.email}
          required
          className="outline-none w-full h-10 p-2 rounded my-2 shadow-sm"
        />
        <textarea
          name="message"
          id="message"
          placeholder="Message"
          rows={5}
          onChange={handleChange}
          value={data.message}
          required
          className="outline-none w-full p-2 rounded my-2 shadow-sm"
        />
        <button type="submit" className="bg-black text-white m-2 w-full py-2">
          Submit
        </button>
      </form>
    </main>
  );
}
Enter fullscreen mode Exit fullscreen mode

Save and close the file. This contact form configuration creates multiple input fields to validate the user input and simulate user interactions.

5. Start the development server as a background process to keep the application active on port 3000:

$ npm run dev &
Enter fullscreen mode Exit fullscreen mode

6. Access the application port 3000 using your server's IP address in a web browser window and verify that the contact form displays:

http://<SERVER-IP>:3000
Enter fullscreen mode Exit fullscreen mode

Install and Configure Cypress

1. Install all required dependencies for Cypress:

$ sudo apt install libgtk2.0-0t64 libgtk-3-0t64 libgbm-dev libnotify-dev libnss3 libxss1 libasound2t64 libxtst6 xauth xvfb -y
Enter fullscreen mode Exit fullscreen mode

2. Install Cypress as a development dependency:

$ npm install cypress --save-dev
Enter fullscreen mode Exit fullscreen mode

3. Run the following command to generate a new Cypress configuration and directory structure:

$ npx cypress open
Enter fullscreen mode Exit fullscreen mode

In the Cypress launchpad that opens, click Continue on the release information prompt, select E2E Testing, verify the generated configuration files and click Continue, select your desired browser and click Start E2E Testing, then click Create new spec and replace the default spec.cy.ts name with your desired filename, such as contact.cy.ts. Click Okay, run the spec once the spec is added successfully.

4. Switch to your terminal session and list all files in your my-project directory to verify the generated Cypress configurations:

$ ls
Enter fullscreen mode Exit fullscreen mode

5. Open the cypress.config.ts file:

$ nano cypress.config.ts
Enter fullscreen mode Exit fullscreen mode

6. Add the following baseUrl configuration after the e2e block to specify the default host for all tests on your workstation:

baseUrl: process.env.BASE_URL || 'http://localhost:3000',
Enter fullscreen mode Exit fullscreen mode

Save and close the file. Your modified cypress.config.ts should look like the one below:

import { defineConfig } from "cypress";

export default defineConfig({
  e2e: {
    baseUrl: process.env.BASE_URL || 'http://localhost:3000',
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

This configuration specifies the host URL to use in Cypress tests, limiting the hardcoding of new URLs in individual test specs.


Write a Cypress Test

Well-structured tests follow three phases — setup, action, and assertion — to define the initial state, execute user actions, and verify the expected outcomes. The example test below loads the contact page, fills out sample information, submits the form, and checks the expected data in the console log.

1. Back up the original contact.cy.ts file within the cypress/e2e directory:

$ mv cypress/e2e/contact.cy.ts cypress/e2e/contact.cy.ts.ORIG
Enter fullscreen mode Exit fullscreen mode

2. Create the contact.cy.ts file:

$ nano cypress/e2e/contact.cy.ts
Enter fullscreen mode Exit fullscreen mode

3. Add the following test code to the file:

describe('Contact Page Tests', () => {
  beforeEach(() => {
    cy.visit('/');
  });

  it('Submits the form successfully', () => {
    cy.window().then((win) => {
      cy.spy(win.console, "log").as("consoleLog");
    });

    cy.get('input[name="firstName"]').type('John');
    cy.get('input[name="lastName"]').type('Doe');
    cy.get('input[name="email"]').type('john_doe@example.com');
    cy.get('textarea[name="message"]').type('This is a test message.');
    cy.get('button[type="submit"]').click();

    cy.get("@consoleLog").should("be.calledWith", "Form Submitted:", {
      firstName: "John",
      lastName: "Doe",
      email: "john_doe@example.com",
      message: "This is a test message."
    });
  });
})
Enter fullscreen mode Exit fullscreen mode

Save and close the file.

4. Run the following command to perform a new Cypress test:

$ npx cypress run
Enter fullscreen mode Exit fullscreen mode

Verify that the run reports 1 passing, 0 failing. If you receive a Cypress failed to verify that your server is running error, run npm run dev to start the development server and start the Cypress test again.

Save Failed Test Results

Cypress automatically stores screenshots that offer a visual record of all failed tests for debugging purposes, which you can re-upload as artifacts in GitHub Actions for centralized troubleshooting.

1. Open the cypress.config.ts file:

$ nano cypress.config.ts
Enter fullscreen mode Exit fullscreen mode

2. Add the following configuration below the baseUrl directive:

screenshotOnRunFailure: true,  // Enable screenshots  
screenshotsFolder: "cypress/screenshots",  // Save location
Enter fullscreen mode Exit fullscreen mode

Save and close the file. Your modified cypress.config.ts should look like the one below:

import { defineConfig } from "cypress";

export default defineConfig({
  e2e: {
    baseUrl: process.env.BASE_URL || 'http://localhost:3000',
    screenshotOnRunFailure: true,  // Enable screenshots  
    screenshotsFolder: "cypress/screenshots",  // Save location
    setupNodeEvents(on, config) {
      // implement node event listeners here
    },
  },
});
Enter fullscreen mode Exit fullscreen mode

3. Back up the cypress/e2e/contact.cy.ts file:

$ cp cypress/e2e/contact.cy.ts cypress/e2e/contact.cy.ts.ORIG
Enter fullscreen mode Exit fullscreen mode

4. Open the contact.cy.ts file to simulate a failed test:

$ nano contact.cy.ts
Enter fullscreen mode Exit fullscreen mode

5. Find the cy.get('input[name="email"]').type('john_doe@example.com'); line and change the email address to john@example.com:

...
cy.get('input[name="email"]').type('john@example.com');
...
Enter fullscreen mode Exit fullscreen mode

Save and close the file. This simulates a failed test with a wrong email address, so Cypress captures a screenshot of the failed test when it completes.

6. Run a new Cypress test and verify that the failed test is detected and a screenshot is stored in your project directory:

$ npx cypress run
Enter fullscreen mode Exit fullscreen mode

7. Navigate to cypress/screenshots/contact.cy.ts/ in your my-project directory and open the 'Contact Page Tests -- Submits the form successfully (failed).png' image to verify the failed Cypress test contents.


Configure GitHub Actions for CI/CD

GitHub workflows consist of jobs and steps that automate tasks in CI/CD pipelines. A job defines a set of instructions that execute in an isolated environment, while a step represents an individual task within a job, such as running a command.

1. Create a new .github/workflows directory:

$ mkdir -p .github/workflows
Enter fullscreen mode Exit fullscreen mode

2. Create a new cypress.yml file in the .github/workflows directory:

$ nano .github/workflows/cypress.yml
Enter fullscreen mode Exit fullscreen mode

3. Add the following configuration to the cypress.yml file:

name: CI/CD Pipeline
on: 
 push:
  branches:
      - main

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout the repository
        uses: actions/checkout@v4

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 18.x

      - name: Install dependencies
        run: npm install

      - name: Build the Next.js app
        run: npm run build

      - name: Run Cypress Tests
        uses: cypress-io/github-action@v6
        with:
          start: npm start
          browser: chrome

      - name: Upload Screenshots
        if: failure()
        uses: actions/upload-artifact@v4
        with:
          path: cypress/screenshots
Enter fullscreen mode Exit fullscreen mode

This configuration automates Cypress end-to-end testing by triggering workflows on pushes to the main branch. It uses the ubuntu-latest environment and performs the following steps:

  • Checkout Code: Uses actions/checkout@v4 to retrieve your repository information.
  • Set up Node.js: Configures Node.js 18 with actions/setup-node@v4.
  • Install and Build: Installs all required dependencies with npm install and runs npm run build to build the Next.js application.
  • Run Tests: Executes tests in Chrome using cypress-io/github-action@v6 and runs npm start to start the application.
  • Upload Screenshots: Uses actions/upload-artifact@v4 to store all failed test screenshots.

4. Initialize Git in your project directory:

$ git init
Enter fullscreen mode Exit fullscreen mode

5. Stage all files:

$ git add .
Enter fullscreen mode Exit fullscreen mode

6. Commit the changes with a custom message such as Cypress E2E test workflow:

$ git commit -m "Cypress E2E test workflow"
Enter fullscreen mode Exit fullscreen mode

7. Add your target GitHub repository to push the changes. Replace https://github.com/example-user/cypress-dev-test with your actual repository URL:

$ git remote add origin https://github.com/example-user/cypress-dev-test
Enter fullscreen mode Exit fullscreen mode

8. Push your code changes to the main branch:

$ git push -u origin main
Enter fullscreen mode Exit fullscreen mode

Analyze Logs in GitHub Actions

GitHub offers built-in logging tools for monitoring Cypress test workflows. Logs display each Cypress test step, allowing you to review all test results and identify possible issues in your application.

1. Navigate to the Actions tab in your GitHub repository.

2. Select the workflow run.

3. Verify the Cypress test results within the test summary section.

4. View all test screenshots within the Artifacts section.

5. Click test within the Jobs section on the left navigation bar to view all executed steps for the job, including the commands run, terminal outputs (test pass/fail messages), and the duration of each step.


Enable Slack Notifications for Cypress Tests

Slack notifications let you receive real-time alerts when Cypress tests fail in GitHub Actions.

1. Install the Incoming Webhooks app in your Slack workspace.

2. Configure the webhook for your desired channel (for example, #ci-cd-alerts), and select or create a channel to post incoming alerts.

3. Copy the generated Webhook URL.

4. Access your GitHub repository and navigate to Settings > Secrets and Variables > Actions.

5. Click New Repository Secret, enter SLACK_WEBHOOK_URL in the Name field, and paste the webhook URL you copied from Slack in the Secret field.

6. Switch to your terminal session and open the cypress.yml workflow file:

$ nano .github/workflows/cypress.yml
Enter fullscreen mode Exit fullscreen mode

7. Add the following configuration at the end of the file:

- name: Notify Slack
  if: failure()
  uses: rtCamp/action-slack-notify@v2
  env:
    SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}
    SLACK_COLOR: ${{ job.status }}
Enter fullscreen mode Exit fullscreen mode

Save and close the file. This configuration uses the SLACK_WEBHOOK_URL secret you set earlier to send Slack notifications to your target channel whenever the workflow fails.

8. Push the updated workflow and verify that a failed run triggers a Slack notification with a link to the GitHub Actions log.


Next Steps

You now have a CI/CD pipeline using GitHub Actions that automates Cypress end-to-end testing against a sample Next.js project, captures screenshots on failure, and sends Slack alerts for failed runs. From here, you can:

  • Add more Cypress specs to cover additional pages and user flows
  • Run tests across multiple browsers or in parallel to speed up the pipeline
  • Extend the workflow to deploy the application to a Kubernetes cluster after tests pass
  • Add code coverage reporting or linting steps to the same workflow

For the full guide with additional tips, visit the original article on Vultr Docs.

Top comments (1)

Collapse
 
raknaos profile image
Baptiste Le Bouquin

Solid walkthrough, and the Slack alert at the end is the part people skip until their first silent pipeline failure.

Two production lessons from running browser tests in CI: first, capture video and screenshots only on failure — the artifact upload becomes the bottleneck before the tests do, and a green run that uploads 40 videos makes you want to delete the whole job. Second, retries need to be a visible signal, not a fix. If you add retries: 2, log which specs needed a retry; that count is your flake debt, and a suite that passes only after retry is quietly telling you something about selectors or race conditions.

Which runner are you targeting for Cypress? I've found the headless browser choice changes flake rate more than any test code change, and free GitHub-hosted runners are not the same environment as your laptop. For the Next.js side, are you building once and reusing the artifact between the test job and the deploy job, or installing and building twice?