By combining GitHub Actions with Playwright, you can build a CI/CD pipeline that automatically runs E2E tests on every push — at no extra cost.
📌 Who This Article Is For
- QA engineers and developers who want to integrate Playwright E2E tests into CI/CD
- Anyone new to GitHub Actions who wants to learn how to write workflow config files from scratch
- Those who want to easily set up an environment where tests run automatically on every push
- Engineers who want to view test result reports directly on GitHub
✅ What You’ll Learn
- How to write a GitHub Actions YAML config file from scratch
- How to run Playwright × pytest tests automatically in a CI environment
- How to save and view HTML reports as GitHub Actions artifacts
👤 About the Author
Working as a QA engineer using Selenium, Playwright, and pytest for real-world test automation. I have hands-on experience integrating tests into GitHub Actions on actual projects, and I’ll walk you through the common pitfalls from a practitioner’s perspective. Code is publicly available on GitHub: github.com/YOSHITSUGU728/automated-testing-portfolio
📌 What You’ll Build in This Article
- Automated test execution: E2E tests run automatically on every push to main or on PRs
- Report storage: HTML test results saved as artifacts and viewable directly on GitHub
- Free to run: GitHub Actions is unlimited for public repos and includes 2,000 free minutes/month for private repos
Even if tests pass locally, it’s common for someone’s push to break things unexpectedly. By integrating E2E tests into CI/CD, you can build a system where tests run automatically on every push and you get notified the moment something fails. This article walks you through building a working CI/CD pipeline with GitHub Actions × Playwright × pytest — completely from scratch.
CI/CD and GitHub Actions: The Basics
CI (Continuous Integration) is a practice where builds and tests are triggered automatically whenever code is pushed or a PR is opened. GitHub Actions is GitHub’s built-in CI/CD service — all you need to do is create a .github/workflows/ folder in your repository and add a YAML file.
| Feature | Details |
|---|---|
| Runners | Ubuntu / Windows / macOS available |
| Free tier | Public repos: unlimited Private repos: 2,000 min/month |
| Triggers | push, pull_request, schedule, manual dispatch, and more |
| Config file | .github/workflows/*.yml |
| Playwright support | Official Docker image available, headless execution supported |
① Project Structure
Here’s the project structure we’ll be working with.
my_project/
├── .github/
│ └── workflows/
│ └── playwright.yml # GitHub Actions workflow file
├── tests/
│ ├── conftest.py
│ └── test_saucedemo.py # Playwright tests
├── requirements.txt # Package dependencies
└── pytest.ini # pytest configuration② Setting Up requirements.txt
List the packages to install in the CI environment in requirements.txt.
pytest
pytest-playwright
pytest-html
playwrightpytest==7.4.0. This ensures the same version is used in CI as locally, reducing environment-specific failures. playwright is listed explicitly because while pytest-playwright sometimes pulls it in as a dependency, this is not guaranteed in all environments — explicit is safer.③ Test Code Overview
Here’s the sample Playwright test that will run in CI. It tests the SauceDemo login flow.
# tests/test_saucedemo.py
from playwright.sync_api import Page
def test_login_success(page: Page):
page.goto("https://www.saucedemo.com/")
page.fill("#user-name", "standard_user")
page.fill("#password", "secret_sauce")
page.click("#login-button")
assert page.url.endswith("/inventory.html")
def test_login_wrong_password(page: Page):
page.goto("https://www.saucedemo.com/")
page.fill("#user-name", "standard_user")
page.fill("#password", "wrong_password")
page.click("#login-button")
error_msg = page.locator(".error-message-container").text_content()
assert "Epic sadface" in error_msg# pytest.ini
[pytest]
testpaths = tests
addopts = -v --tb=short --html=reports/report.html --self-contained-html④ Creating the GitHub Actions YAML File (Core)
This is the heart of the setup. Create .github/workflows/playwright.yml with the following content.
# .github/workflows/playwright.yml
name: Playwright E2E Tests
# ① Trigger: run automatically on push to main or on PRs
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
# ② Runner: use the latest Ubuntu
runs-on: ubuntu-latest
steps:
# ③ Check out the repository code
- name: Checkout repository
uses: actions/checkout@v4
# ④ Set up Python
- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"
# ⑤ Install dependencies (upgrade pip first for stability)
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
# ⑥ Install Playwright browsers
- name: Install Playwright browsers
run: playwright install chromium --with-deps
# ⑦ Create reports folder (prevents artifact upload failure)
- name: Create reports folder
run: mkdir -p reports
# ⑧ Run tests
- name: Run E2E tests
run: pytest
# ⑨ Upload test report as artifact
- name: Upload report
uses: actions/upload-artifact@v4
if: always() # run even if tests fail
with:
name: playwright-report
path: reports/
retention-days: 30Here’s what each step does:
| Step | What it does |
|---|---|
| ① Trigger | Runs automatically on push to main or when a PR is opened |
| ② runs-on | Uses the latest Ubuntu as the execution environment |
| ③ checkout | Clones the repository code into the CI environment |
| ④ setup-python | Installs Python 3.11 in the CI environment |
| ⑤ pip install | Upgrades pip first, then installs packages from requirements.txt |
| ⑥ playwright install | Installs Chromium browser and its Linux system dependencies |
| ⑦ mkdir -p reports | Pre-creates the reports folder (artifact upload fails if it doesn’t exist) |
| ⑧ pytest | Runs the tests (job is marked as failed if any test fails) |
| ⑨ upload-artifact | Saves the HTML report to GitHub for 30 days |
⑤ How to Check the Results
Once you push the YAML file to GitHub, the workflow starts automatically. Here’s how to check the results.
| Step | Action |
|---|---|
| ① Open the Actions tab | Click “Actions” in the top menu of your GitHub repository |
| ② Select the workflow | Select “Playwright E2E Tests” to see the run history |
| ③ Check logs | Click any step to expand its execution logs |
| ④ Download report | Download playwright-report from the “Artifacts” section at the bottom of the page |
Sample Execution Log
Run pytest
========================= test session starts ==========================
platform linux -- Python 3.11.0, pytest-7.4.0
collected 2 items
tests/test_saucedemo.py::test_login_success PASSED [ 50%]
tests/test_saucedemo.py::test_login_wrong_password PASSED [100%]
========================== 2 passed in 8.43s ===========================⑥ Advanced: Scheduled Runs & Multi-Browser Testing
Run tests automatically every night
If you want tests to run on a schedule rather than just on push, add a schedule trigger.
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
schedule:
- cron: "0 0 * * *" # every day at UTC 00:00 (midnight)Run tests in parallel across multiple browsers
Using strategy: matrix, you can run tests in parallel across Chromium, Firefox, and WebKit.
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
browser: [chromium, firefox, webkit]
steps:
# ... same as before ...
- name: Install Playwright browsers
run: playwright install ${{ matrix.browser }} --with-deps
- name: Run E2E tests (${{ matrix.browser }})
run: pytest --browser ${{ matrix.browser }}
- name: Upload report
uses: actions/upload-artifact@v4
if: always()
with:
name: report-${{ matrix.browser }}
path: reports/FAQ
Q. Is GitHub Actions really free to use?
Public repositories get unlimited free minutes. Private repositories get 2,000 free minutes per month. A typical Playwright × pytest test run takes 1–3 minutes, so the free tier is more than enough for personal projects and small teams.
Q. Can I get notified when a test fails?
Yes. Enable “Actions” notifications in your GitHub account settings to receive an email whenever a workflow fails. For Slack notifications, the slackapi/slack-github-action lets you set it up in just a few YAML lines.
Q. Why do tests that pass locally fail in CI?
The most common causes are: ① environment differences (Python version, OS), ② rendering timing differences in headless mode, and ③ network connectivity to the target service. Start by pinning versions in requirements.txt and checking whether the failure is headless-specific by reviewing the stack trace carefully.
Q. What if my tests target a localhost server?
You can start the server as a step within the GitHub Actions job. For a Flask app, for example, add run: python app.py & to start it in the background, then run pytest in the next step. For external services like databases or API servers, use the services key to spin up Docker containers.
⚠️ 8 Common Pitfalls with GitHub Actions × Playwright
CI environments come with their own unique set of headaches, especially with GitHub Actions × Playwright. Here are the issues most likely to catch you off guard — knowing them in advance will save you a lot of frustrating debugging.
① Forgetting playwright install — browser not found
CI environments don’t come with Playwright browsers pre-installed. Even after running pip install pytest-playwright, you still need to separately install the browser with playwright install chromium --with-deps. Omitting --with-deps leaves out Linux system dependencies and prevents the browser from launching.
② Missing if: always() — report not saved when tests fail
Without if: always() on the artifact upload step, the job stops the moment a test fails and the report never gets saved. Since reports are most valuable precisely when tests fail, always include this condition.
③ YAML indentation errors prevent the workflow from running
GitHub’s YAML parser is very strict about indentation. Mixing 2-space and 4-space indentation, or accidentally inserting tab characters, will result in an “Invalid workflow file” error. Run your YAML through yamllint.com before pushing to catch issues early.
④ Missing reports/ folder causes artifact upload to fail
If all tests are skipped or pytest exits before generating the report, the reports/ folder may not be created, causing the upload-artifact step to fail. The safest fix is to add a run: mkdir -p reports step before running pytest, or add if-no-files-found: warn to the artifact configuration.
⑤ Elements not found in headless mode (passes locally, fails in CI)
CI environments run in headless mode by default. Compared to headed mode, rendering timing can differ and page.locator() may fail to find elements. Resolve this by adding explicit waits with page.wait_for_selector() or expect(locator).to_be_visible().
⑥ Artifact name collision in matrix runs overwrites reports
If all parallel browser jobs use the same name: playwright-report, the last job to finish will overwrite the reports from earlier jobs. Use name: report-${{ matrix.browser }} to give each browser’s report a unique name.
⑦ Omitting action versions causes unexpected breakage
Using uses: actions/checkout@main means your workflow could silently break whenever that action is updated. Always pin to a major version like @v4 to ensure stable, predictable behavior.
⑧ Stale pip cache causes ModuleNotFoundError to persist
If you’re using cache: "pip" in actions/setup-python, updating requirements.txt may not immediately reflect in CI because the old cache is still being used. When you see ModuleNotFoundError in CI but not locally, stale cache is often the culprit.
# With caching enabled (cache is auto-invalidated when requirements.txt changes)
- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip" # cache is cleared automatically when requirements.txt hash changes
# Without caching (clean install every run)
- uses: actions/setup-python@v5
with:
python-version: "3.11"
# omitting cache means no caching (fresh install every time)If you hit an unexplained ModuleNotFoundError, the fastest fix is to go to “Actions → Caches” on GitHub and manually delete the cache, then re-run.
📖 Related Articles
📋 Summary
- GitHub Actions is a CI/CD service that works the moment you add a YAML file to
.github/workflows/— free for public repositories - playwright install chromium –with-deps is required in CI — the browser won’t launch without it
- if: always() on the artifact upload step ensures reports are saved even when tests fail
- schedule enables daily automated runs to continuously monitor service quality
- matrix makes it easy to run cross-browser parallel tests for comprehensive quality assurance
Integrating E2E tests into CI/CD means quality is guaranteed no matter who pushes code. Start with a simple setup — Chromium only, main branch only — and expand to scheduled runs and multi-browser testing as you get comfortable.

