GitHub Actions × Playwright: Automate E2E Tests in CI/CD | Complete Setup Guide from Scratch

test-automation

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.

FeatureDetails
RunnersUbuntu / Windows / macOS available
Free tierPublic repos: unlimited Private repos: 2,000 min/month
Triggerspush, pull_request, schedule, manual dispatch, and more
Config file.github/workflows/*.yml
Playwright supportOfficial 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
playwright
💡 Note: To pin versions, write them as pytest==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: 30

Here’s what each step does:

StepWhat it does
① TriggerRuns automatically on push to main or when a PR is opened
② runs-onUses the latest Ubuntu as the execution environment
③ checkoutClones the repository code into the CI environment
④ setup-pythonInstalls Python 3.11 in the CI environment
⑤ pip installUpgrades pip first, then installs packages from requirements.txt
⑥ playwright installInstalls Chromium browser and its Linux system dependencies
⑦ mkdir -p reportsPre-creates the reports folder (artifact upload fails if it doesn’t exist)
⑧ pytestRuns the tests (job is marked as failed if any test fails)
⑨ upload-artifactSaves 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.

StepAction
① Open the Actions tabClick “Actions” in the top menu of your GitHub repository
② Select the workflowSelect “Playwright E2E Tests” to see the run history
③ Check logsClick any step to expand its execution logs
④ Download reportDownload 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/
💡 Pro Tip: Running 3 browsers in parallel barely increases total execution time while tripling your browser coverage. However, it also triples your free tier usage. A common real-world approach: run all 3 browsers on main branch pushes, but only Chromium on PRs.

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.

📋 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.

Copied title and URL