July 7, 2026
How to Run Playwright Tests in GitHub Actions
Learn how to run Playwright tests in GitHub Actions with browser installation, caching, reports, artifacts, and practical troubleshooting tips.
If you are already using Playwright locally, the next natural step is to run the same browser tests in CI. For most teams, that means GitHub Actions. The basic setup is straightforward, but the details matter if you want stable runs, readable reports, and fast pipelines instead of flaky red builds and mystery failures.
In this guide, I will walk through how I set up Playwright in GitHub Actions, what to install, how to cache dependencies, how to publish reports and artifacts, and which configuration choices usually save time later. I will also point out where a managed platform such as Endtest can be a simpler option if your team wants CI-triggered browser tests without owning the Playwright CI stack.
What you need before wiring up GitHub Actions
You do not need a huge amount of infrastructure to start. You do need a Playwright project that already runs locally and a GitHub repository with Actions enabled.
At a minimum, you should have:
- A Playwright test suite
- Node.js in your project, usually via
package.json - A
playwright.config.tsorplaywright.config.js - A GitHub repo with permission to create workflows
If you are new to the tool itself, the official Playwright docs are the best starting point: Playwright documentation.
The mental model for Playwright CI
Before we get into YAML, it helps to understand what GitHub Actions is doing for browser tests.
A CI run usually needs to:
- Check out your code
- Install Node.js and project dependencies
- Install Playwright browser binaries if they are not already present
- Run the tests
- Save reports and screenshots or videos when something fails
The biggest mistake I see is treating CI like a copy of the laptop setup. In CI, consistency matters more than convenience. Install what you need explicitly, cache carefully, and make failures easy to debug.
GitHub Actions is a hosted CI system from GitHub, so the runners are ephemeral. Every workflow run starts on a clean machine. That is good for isolation, but it also means you need to account for repeated setup work. The official docs are here: GitHub Actions.
A minimal Playwright workflow
This is the smallest practical workflow I would use as a starting point.
name: Playwright Tests
on: push: branches: [main] pull_request:
jobs: test: runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
That workflow is enough for many projects. It checks out code, installs dependencies, downloads browser binaries, and runs tests.
Why npm ci instead of npm install
For CI, I prefer npm ci because it installs from the lockfile and gives you reproducible dependency resolution. If package-lock.json changes unexpectedly, the workflow will fail instead of silently drifting.
Why npx playwright install --with-deps
The --with-deps flag tells Playwright to install any operating system dependencies required for the browsers on Linux runners. That is especially useful on GitHub-hosted Ubuntu machines.
If you skip browser installation, your tests may fail with missing executable errors. If you skip dependencies, you can get runtime failures that only appear in CI.
A Playwright config that works well in CI
The workflow is only half the story. Your Playwright configuration should also be aware of CI.
A practical playwright.config.ts might look like this:
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({ testDir: ‘./tests’, fullyParallel: true, retries: process.env.CI ? 2 : 0, workers: process.env.CI ? 2 : undefined, reporter: [ [‘html’], [‘junit’, { outputFile: ‘test-results/junit.xml’ }], [‘list’] ], use: { baseURL: ‘https://example.com’, trace: ‘on-first-retry’, screenshot: ‘only-on-failure’, video: ‘retain-on-failure’ }, projects: [ { name: ‘chromium’, use: { …devices[‘Desktop Chrome’] } }, { name: ‘firefox’, use: { …devices[‘Desktop Firefox’] } }, { name: ‘webkit’, use: { …devices[‘Desktop Safari’] } } ] });
A few things are worth calling out.
Retries in CI are not cheating
Retries are not a substitute for fixing flaky tests, but they are useful for reducing noise while you work on stability. I usually allow a small retry count in CI and keep it at zero locally.
Trace and video settings help with debugging
When a test fails in GitHub Actions, I want enough context to reproduce it quickly. Playwright traces are one of the best debugging tools in browser automation. The trace: 'on-first-retry' setting is a good compromise between visibility and storage cost.
Parallelism should match your runner size
More workers are not always better. On small GitHub runners, too much parallelism can increase contention and make failures harder to diagnose. Start conservatively and increase only after checking test behavior and runtime.
A more complete GitHub Actions workflow
Once the basics work, the next step is usually caching and artifact upload. Here is a more production-ready example.
name: Playwright Tests
on: push: branches: [main] pull_request:
jobs: test: runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- name: Install dependencies
run: npm ci
- name: Install Playwright browsers
run: npx playwright install --with-deps
- name: Run tests
run: npx playwright test
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
with:
name: test-results
path: test-results/
retention-days: 7
Why artifact upload matters
If a pipeline fails, the HTML report is often the fastest way to understand why. Uploading playwright-report/ and raw test results keeps the evidence attached to the build.
Useful artifacts often include:
- HTML report
- Traces
- Screenshots
- Videos
- JUnit XML for downstream reporting systems
If your team uses another system for test analytics, JUnit XML is a common bridge. If not, the Playwright HTML report is usually enough to start with.
Caching browser downloads and dependencies
Caching can speed up Playwright CI, but it is easy to overcomplicate. I would separate the problem into two caches:
- Node dependencies, handled by
setup-nodewhen usingcache: npm - Playwright browser binaries, which can be cached manually if your pipeline is large enough to justify it
For smaller teams, browser installation time is often acceptable. For larger suites, cache can help, but only if it is reliable and simple.
Here is a pattern that caches Playwright browsers using the default cache directory.
- name: Cache Playwright browsers
uses: actions/cache@v4
with:
path: ~/.cache/ms-playwright
key: $-playwright-$
That said, browser caching is one of those areas where simplicity often wins. If the cache becomes flaky or hard to invalidate, I would rather install browsers deterministically than save a minute and create uncertainty.
My rule of thumb is simple, if a cache is hard to reason about during an incident, it is probably too clever.
Running browser tests across multiple browsers
One reason teams choose Playwright is cross-browser coverage. GitHub Actions makes it easy to run a browser matrix.
name: Playwright Matrix
on: [push, pull_request]
jobs: test: runs-on: ubuntu-latest strategy: matrix: browser: [chromium, firefox, webkit]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npx playwright install --with-deps $
- run: npx playwright test --project=$
This is helpful when browser-specific bugs matter. It is also a good way to avoid discovering late that a feature works in Chromium but fails elsewhere.
The tradeoff is runtime. If your suite is already large, a matrix can make CI longer and more expensive. Some teams split fast smoke tests from full browser coverage to balance speed and confidence.
How to handle flaky tests in GitHub Actions
If your first few runs are unstable, do not assume GitHub Actions is the problem. Browser automation has a lot of moving parts, especially when tests depend on timing, animations, network behavior, or non-deterministic selectors.
The usual checklist I use is:
- Prefer stable locators, not brittle CSS chains
- Wait on user-visible states, not arbitrary timeouts
- Avoid shared state between tests
- Reset test data between runs
- Use Playwright tracing to inspect failure sequences
- Keep retries low, then fix the root cause
A flaky selector often looks fine locally because your machine is fast and idle. CI is a better judge of whether your waits are actually correct.
Here is a simple example of a more resilient locator style.
typescript
await page.getByRole('button', { name: 'Sign in' }).click();
await expect(page.getByRole('heading', { name: 'Dashboard' })).toBeVisible();
That is easier to maintain than targeting a deep DOM path that changes whenever a designer tweaks markup.
Debugging failed runs
When a workflow fails, I usually look at three layers of evidence.
1. The raw job log
This is the first place to check for missing dependencies, failed installs, or browser launch issues.
2. The Playwright report
The HTML report gives you test names, retries, attachments, and timing.
3. The trace
Trace viewer is excellent for understanding what the browser did right before the failure. If a test fails intermittently, the trace is often the fastest route to the cause.
If you want to make trace files easier to inspect, keep them as artifacts and name them clearly.
Common GitHub Actions browser test mistakes
I see the same mistakes over and over when teams first run Playwright tests in GitHub Actions.
Installing browsers in the wrong place
Browsers should be installed as part of the workflow, not assumed to exist on the runner.
Forgetting OS dependencies
On Linux, --with-deps prevents a lot of avoidable launch errors.
Not storing reports
Without artifacts, a failed build is much harder to debug after the fact.
Running too many workers too early
Parallelism can make a flaky suite fail faster, but it does not make it healthier.
Mixing app deployment and test execution without a clear handoff
If your tests hit an environment that is still starting up, add an explicit readiness check.
When to use Playwright in GitHub Actions, and when not to
For teams already invested in code-based testing, Playwright GitHub Actions is a strong default. You get:
- Version-controlled tests
- A familiar developer workflow
- Rich browser automation features
- Easy integration with CI/CD
But there are cases where the maintenance cost is not worth it. If your team wants CI-triggered browser coverage without managing runners, browser installs, reporters, and CI glue, a managed platform can be simpler. That is where something like Endtest’s Playwright comparison becomes relevant, especially if you want a platform that handles execution infrastructure and lets non-developers participate in test authoring through low-code or no-code workflows with agentic AI support.
I would not use that as the default argument against Playwright. I would use it as a decision point. If your team has the engineering bandwidth to own the pipeline, Playwright is a powerful choice. If you need the coverage but not the setup overhead, managed Test automation is worth evaluating.
A practical rollout plan
If I were introducing Playwright CI to an existing team, I would do it in this order:
- Run one or two smoke tests in GitHub Actions
- Add browser installation and artifact upload
- Enable retries and tracing for failures
- Expand to a browser matrix if cross-browser coverage matters
- Add caching only after the baseline is stable
- Split fast checks from longer end-to-end flows when the suite grows
This keeps the first version small enough to debug. The goal is not to build the most elegant pipeline on day one. The goal is to get a trustworthy signal from CI and improve it incrementally.
Final thoughts
Running Playwright tests in GitHub Actions is not hard, but running them well takes a little discipline. Install browsers explicitly, keep your config CI-aware, upload artifacts, and treat flaky failures as a test design problem first, not a CI problem.
If you want a lightweight starting point, the minimal workflow is enough. If you want a pipeline that is easier to operate over time, add reports, traces, and a sensible browser matrix. And if your team wants browser coverage without owning the CI setup at all, it may be worth comparing Playwright with a managed platform before you commit to maintaining the whole stack yourself.
Either way, the core pattern is the same, make browser tests repeatable, observable, and boring enough that your team trusts the result.