From 'It Works on My Machine' to 'It Works in CI': Cypress in (GitHub) Action
Learn how to run Cypress tests in GitHub Actions by replicating your local setup, with a clear workflow you can apply right away.
When we set up Cypress E2E tests for our project, it became clear early on that they needed to run before every production deployment. That meant one thing: CI/CD.
Next thing I know, DevOps is talking about setting up CI/CD for the tests. The conversation went something like this:
DevOps: “Who will set up CI/CD for Cypress tests?” Me: “Can I try?” DevOps: “Great, welcome to the DevOps team.” Me: “… Help? “
On a serious note, when I got the go-ahead, it was time for a little research on how to set up the action. All the articles on the web say the same thing: you have something called jobs, steps, and boom — the action is ready.
It all sounds simple enough in theory. But when you come from a QA background and have little to no knowledge of the DevOps world, that’s not very helpful.
Clearly, “boom - the action is ready” was missing a few steps for folks like me. So naturally, I did what anyone would do in my position - I turned to the DevOps team for help.
The best advice I can give you - the advice I got from the DevOps team - is this: everything you do to set up, launch the project, and run tests locally on your machine, you need to do essentially the same within your GitHub Action. Once I followed this approach, everything became much clearer.
Since you’re here, I’m guessing you already know Cypress and GitHub Actions and simply want the solution, so let’s skip definitions and jump straight into it.
Cypress + GitHub Actions: What Makes It Work
Implementation means getting your hands dirty with the actual setup. I’ll break down every step, making sure you understand what’s happening and why.
After that, you’ll see an example workflow file to help you visualize how to automate your Cypress tests in GitHub Actions. Let’s dive in!
Step 1. Creating a workflow file
GitHub Actions uses YAML files to define workflows. To create your first workflow:
- In the root of your project, create a folder called
.github
(if it doesn’t already exist). - Inside that folder, create another folder named
workflows
(if it doesn’t already exist). - Inside
.github/workflows/
, create a new file — you can name it something likerun-cypress.yml
.
Your final path should look like this:
your-project/
├── .github/
│ └── workflows/
│ └── run-cypress.yml
This is the file where your automated Cypress testing pipeline begins.
Step 2. Choosing the Right Trigger
Before our Cypress tests can run, something needs to tell GitHub Actions “Go”. That something is called a trigger - it defines when your CI workflow should start.
There are plenty of trigger options, but for test automation, only a select few are actually useful, such as - push
, pull_request
, workflow_dispatch
, schedule
, and optionally deployment_status
. Let’s break them down.
Trigger: push
Runs the workflow when code is pushed to the repository.
Best for:
- Running tests automatically whenever code is pushed to key branches like
main
ordevelop
.
Use it to:
- Automatically check that new code on key branches doesn’t break anything, verify hot-fixes or quick fixes pushed directly, and run tests on new version tags.
- Trigger:
push
Runs the workflow when code is pushed to the repository.
on:
push:
branches:
- main # Automatically check that new code on key branches doesn’t break anything
- develop # Same as above — check changes before merging to production
- 'hotfix/*' # Checking hotfixes or quick fixes pushed directly
tags:
- 'v*' # Running tests on new version tags (e.g., v1.0.0)
Trigger: pull_request
Runs the workflow when a pull request targets specific branches.
Best for:
- Catching bugs before merging code into important branches.
Use it to:
- Automatically run tests when a pull request is opened, updated, or reopened, ensure only working code is merged into specific branches, and block merges if tests fail using branch protection rules.
Note: Configuring branch protection rules happens in your GitHub repository settings.
on:
pull_request:
branches: # Run tests when a PR targets these branches (opened or updated)
- main
- develop
- 'release/*'
Trigger: workflow_dispatch
Runs the workflow manually when you trigger it via GitHub UI.
Best for:
- Running tests on demand, whenever you want.
Use it to:
- Rerun flaky or failing tests without pushing new code, manually test changes to your Cypress setup or pipeline, and run targeted test runs on demand.
on:
workflow_dispatch:
inputs:
namespace: # Deployment namespace selection
description: 'Target namespace' # Shown as prompt in GitHub Actions UI
required: true # Must provide this input to trigger workflow
default: 'development' # Preselected default value in UI
type: choice # Input type is a dropdown list
options: # Allowed options for the namespace input
- development
- staging
- production
branch: # Git branch to run the tests or deployment on
description: 'Test branch' # Input prompt description shown in UI
required: true # This input must be provided by the user
type: string # Accepts any string (branch name)
# Useful for running targeted test runs on demand
Trigger: schedule
Runs the workflow on a recurring cron schedule.
Best for:
- Running regular automated tests to catch issues even without code changes.
Use it to:
- Automatically run Cypress tests on a nightly, weekly, or custom schedule, verify that external services and APIs your app depends on are functioning correctly, and detect issues like expired credentials or unexpected changes before they affect users.
on:
schedule:
- cron: '0 3 * * *' # Runs tests nightly at 3:00 AM UTC
- cron: '0 0 * * 0' # Runs tests weekly on Sundays at midnight UTC
Trigger: deployment_status
Runs the workflow after a deployment is completed.
Best for:
- Running tests after the app goes live.
Use it to:
- Automatically run smoke tests after a successful deployment to ensure the app works as expected for real users and to catch post-deployment issues like broken pages, bad configurations, or failing APIs.
on:
deployment_status:
types:
- success # Run this workflow only after a successful deployment
Step 3. - Defining Jobs for Cypress Setup
Once we’ve got our triggers in place, we need to tell GitHub Action what to actually do. That’s the job of… well, the job
. Think of a job as a to-do list for your CI workflow - it installs what you need, starts the app, and finally runs your Cypress tests.
Let’s break down the main parts of a job and see how each step helps get your tests running smoothly.
#1
Each job runs on a specific runner — a machine where the steps execute. GitHub provides hosted runners like ubuntu-latest
, windows-latest
, and macos-latest
, or you can use your own self-hosted runner.
jobs:
cypress-run: # This is the job name — you can name it anything
runs-on: ubuntu-latest
#2
When the runner is set up, we need to check out the repository so the workflow can access the project files.
steps:
- name: Checkout the code/repository
uses: actions/checkout@v4
#3
Following that, the runner prepares the Node.js environment to match the project settings for dependable test execution.
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
#4
Then, dependencies are set up using the lockfile to avoid version mismatches.
- name: Install dependencies
run: npm ci
#5
These next steps launch your application and wait for it to become responsive before running tests. This is necessary when testing locally, but not required for deployed apps.
- name: Start the app
run: npm run start &
- name: Wait for the app to be ready
uses: jakejarvis/wait-action@v0.1.0
with:
url: '<http://localhost:3000>'
timeout: 60
# Waits up to 60 seconds until the app responds — prevents Cypress from running too early
#6
In this step, Cypress tests are triggered. If you’re testing locally, you need to start the app within the workflow (see step above) - but for deployed environments, simply point Cypress to the live URL.
- name: Run Cypress tests
uses: cypress-io/github-action@v6
with:
start: false
config: baseUrl=https://your-deployed-app.com # Skip local app startup; run tests against live site
#7
The last step uploads screenshots and videos only if tests fail, which helps you debug directly in the GitHub UI. This step is optional and can be included if you want easier access to test artifacts.
name: Upload test artifacts (on failure)
if: failure()
uses: actions/upload-artifact@v4
with:
name: cypress-artifacts
path: |
cypress/screenshots
cypress/videos
With the core pieces in place, you now have a clear picture of how Cypress fits into your CI workflow.
Next, let’s bring it all together. Below is the complete YAML file that puts everything into action.
Quick tip: Cypress has an official GitHub Action on the Marketplace to help you set up and run tests easily.
name: Cypress test run
on:
workflow_dispatch:
inputs:
namespace:
description: "Namespace to run tests against"
required: true
default: development
type: choice
options:
- development
- staging
branch:
description: "Branch to checkout"
required: true
type: string
default: main
jobs:
run-tests:
name: Cypress test run
runs-on: self-hosted
steps:
# Print some helpful info about how and where the workflow is running
- run: echo "🚀 This self-hosted job was automatically triggered by a ${{ github.event_name }} event on a ${{github.ref_name}}."
- run: echo "🐧 This job is now running on a runner with name ${{ runner.name }} on ${{ runner.os }} server."
- name: Checkout Repository
uses: actions/checkout@v4
with:
ref: ${{ inputs.branch }}
fetch-depth: 1
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
- run: echo "🖥️ The workflow is now ready to test your code on the runner."
- name: Cypress install
id: cypress-install
uses: cypress-io/github-action@v6
with:
runTests: false
- name: Run Cypress Tests
id: cypress-tests
if: steps.cypress-install.outcome == 'success'
uses: cypress-io/github-action@v6
with:
browser: electron
headed: false
config-file: cypress.config.js
config: baseUrl=${{ github.event.inputs.namespace == 'staging' && '<https://staging.com>' || '<https://development.com>' }}
record: false
env:
# Cypress and project-specific environment variables.
# These values are stored locally in a .env file during development,
# but are securely set as GitHub Secrets for use in this GitHub Action.
CYPRESS_CACHE_FOLDER: ~/.cache/Cypress
CYPRESS_AUTH_USERNAME: ${{ secrets.AUTH_USERNAME }}
CYPRESS_AUTH_PASSWORD: ${{ secrets.AUTH_PASSWORD }}
CYPRESS_ENV: ${{ github.event.inputs.namespace }}
- name: Outcome
if: steps.cypress-tests.outcome == 'success'
run: |
echo "🎉 The job was a success!"
And that’s it. Your setup might not be identical, but this should get you 90% of the way there.
Closing the Loop with Cypress CI/CD
Setting up Cypress in CI isn’t as scary as it seems. The key is to replicate the way you run tests locally within your workflow, making sure the process stays consistent no matter where it runs.
The above examples lay out the basics to get you going, helping you avoid confusion and wasted time.
You’ll need to tweak it to match your project’s requirements - there’s no one-size-fits-all solution.
But once it’s ready, you’ll have reliable tests running on every change, keeping your deployments smooth.