Tests only protect a product when they run automatically on every change. This guide sets up Playwright in GitHub Actions, Jenkins and Docker, publishes HTML reports and traces as artifacts, and speeds up large suites with sharding. For the Java version, see the Playwright Java framework.

Run Your Tests Automatically on Every Push (GitHub Actions)

Time: ~25 minutes. You will push your project to GitHub and set up CI so your Playwright tests run automatically on every push and pull request, with a downloadable report.

Goal

A green CI run visible on GitHub, producing a Playwright HTML report artifact.

Step 1 — Add a .gitignore (don’t commit junk or secrets)

Create .gitignore in the project root:

node_modules/
test-results/
playwright-report/
blob-report/
playwright/.auth/
.env

Step 2 — Create the workflow file

Create the folders and file .github/workflows/playwright.yml:

name: Playwright Testson:  push:    branches: [main]  pull_request:    branches: [main]jobs:  test:    timeout-minutes: 20    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - 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      - name: Upload HTML report        if: always()                      # keep the report even if tests fail        uses: actions/upload-artifact@v4        with:          name: playwright-report          path: playwright-report          retention-days: 7

Step 3 — Understand the two critical lines

  • npm ci — installs the EXACT locked dependencies (reproducible, fast in CI).
  • npx playwright install --with-deps — installs the browsers and the Linux libraries they need on a fresh CI machine. Skip this and browsers fail to launch — the #1 CI mistake.

Step 4 — Create a GitHub repo and push

On github.com, create a new empty repo (no README). Then, in your project folder:

git init
git add .
git commit -m "Playwright framework with CI"
git branch -M main
git remote add origin https://github.com/<your-username>/<your-repo>.git
git push -u origin main

✅ Your code is now on GitHub.

Step 5 — Watch CI run

On your repo page, click the Actions tab. ✅ You should see a “Playwright Tests” workflow running. Click it to watch the steps: checkout → setup-node → install → install browsers → run tests → upload report.

Step 6 — Download the report from CI

When the run finishes, scroll to Artifacts on the run’s summary page and download playwright-report. Unzip it and open index.html. ✅ You’re looking at the exact HTML report from the CI run — including traces if a test failed.

Step 7 — Prove the gate works (make CI fail, then fix)

Introduce a failing assertion in any test, commit, and push:

git add . && git commit -m "temporary failing test" && git push

✅ The Actions run turns red. Download the report artifact to see the failure and trace. Now fix the test, commit, and push again — the run turns green.

Step 8 — Make it a required check (optional, real-world)

In your repo: Settings → Branches → Add branch protection rule for main → require the “Playwright Tests” status check to pass before merging. Now broken code can’t be merged — your tests are a real quality gate.

What you just learned

  • How to write a GitHub Actions workflow for Playwright.
  • Why npm ci and --with-deps are essential in CI.
  • How to download the report artifact and debug CI failures.
  • How to make tests a merge gate with branch protection.

Next

Walkthrough 10 runs your tests across multiple browsers in parallel and inside Docker for perfect local/CI parity.

Advertisement

CI/CD Fundamentals

Level: L4.

What is it?

CI (Continuous Integration): every code change is automatically built and tested. CD (Continuous Delivery/Deployment): validated changes are automatically released. Your Playwright suite is the quality gate in this pipeline.

Why do we need it?

Manual testing can’t keep pace with frequent commits. CI runs your tests on every push/PR, catching regressions in minutes and preventing bad code from merging or shipping.

How does it work?

A CI server (Jenkins, GitHub Actions, GitLab CI) watches the repo. On a trigger (push/PR/schedule), it spins up a runner, installs dependencies + browsers, runs npx playwright test, and publishes reports/artifacts. Pass = merge/deploy allowed; fail = blocked.

The pipeline stages

1. Trigger → push / PR / nightly schedule
2. Checkout → pull the repo
3. Setup → Node, npm ci, npx playwright install --with-deps
4. Build → (app build if needed)
5. Test → npx playwright test (sharded/parallel)
6. Report → publish HTML/JUnit + traces as artifacts
7. Gate → block merge/deploy on failure
8. Deploy (CD) → release if green

Basic pipeline command block

npm cinpx playwright install --with-depsnpx playwright test --reporter=html,junit

Practical Example — where tests fit

PR opened → CI runs @smoke (fast) → must pass to merge
Merge to main → CI runs full @regression + deploy to staging
Nightly → full cross-browser matrix on staging

Line-by-Line Explanation

  • npm ci installs exact locked deps (reproducible, faster than npm install in CI).
  • playwright install --with-deps installs browsers and OS libraries the browsers need on a fresh runner.
  • Splitting smoke (PR) vs regression (merge/nightly) balances speed and coverage.

Common Mistakes

  • Forgetting --with-deps → browsers fail to launch on clean runners.
  • Running the full matrix on every PR (slow feedback).
  • No artifacts published → failures undebuggable.
  • Non-deterministic tests making the pipeline untrustworthy.

Best Practices

  • npm ci + playwright install --with-deps in every pipeline.
  • Smoke on PRs, full regression on merge/nightly.
  • Publish HTML/JUnit + traces as artifacts.
  • Fail the build on any test failure (real quality gate).
  • Cache npm and browser binaries to speed runs.

Interview Questions

  • Q: What is CI/CD? A: Automated build+test on every change (CI) and automated release of validated changes (CD).
  • Q: Where do E2E tests fit? A: As a quality gate — smoke on PRs, full regression before deploy.
  • Q: Why npm ci over npm install in CI? A: Deterministic installs from the lockfile, faster and reproducible.
  • Q: Why --with-deps? A: Installs the OS libraries browsers require on fresh CI machines.

Practice Exercise

Sketch a pipeline (stages above) for your framework: PR→smoke, merge→regression, nightly→cross-browser. Note where artifacts are published and where the gate blocks.

Real-World Scenario

Before CI, regressions were found days later by manual QA. After wiring Playwright into CI as a merge gate, broken changes were caught on the PR itself — mean time to detection dropped from days to minutes.

GitHub Actions

Level: L4.

What is it?

GitHub’s built-in CI/CD. Workflows are YAML files in .github/workflows/ that run on triggers (push/PR/schedule) and execute your Playwright suite on GitHub-hosted runners.

Why do we need it?

It’s the fastest way to add CI to a GitHub repo — no server to maintain. Extremely common in modern teams and open source.

How does it work?

A workflow defines jobs of steps. Steps check out code, set up Node, install deps + browsers, run tests, and upload the report as an artifact. Matrix/shards enable parallelism.

Complete working workflow

# .github/workflows/playwright.ymlname: Playwright Testson:  push:    branches: [main]  pull_request:    branches: [main]  schedule:    - cron: '0 2 * * *'   # nightly at 02:00 UTCjobs:  test:    timeout-minutes: 30    runs-on: ubuntu-latest    strategy:      fail-fast: false      matrix:        shard: [1, 2, 3]      # 3-way sharding    steps:      - uses: actions/checkout@v4      - 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        env:          CI: true          BASE_URL: ${{ vars.BASE_URL }}          TEST_USER: ${{ secrets.TEST_USER }}          TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}        run: npx playwright test --shard=${{ matrix.shard }}/3 --reporter=blob      - name: Upload blob report        if: always()        uses: actions/upload-artifact@v4        with:          name: blob-report-${{ matrix.shard }}          path: blob-report          retention-days: 7  merge-report:    if: always()    needs: [test]    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v4      - uses: actions/setup-node@v4        with: { node-version: 20, cache: 'npm' }      - run: npm ci      - name: Download blob reports        uses: actions/download-artifact@v4        with:          path: all-blob-reports          pattern: blob-report-*          merge-multiple: true      - name: Merge into HTML report        run: npx playwright merge-reports --reporter=html ./all-blob-reports      - name: Upload HTML report        uses: actions/upload-artifact@v4        with:          name: playwright-html-report          path: playwright-report          retention-days: 14

Secrets & variables setup

  • Repo → Settings → Secrets and variables → Actions.
  • Add secrets TEST_USER, TEST_PASSWORD; add a variable BASE_URL.
  • They’re injected as env vars in the run step (secrets are masked in logs).

Line-by-Line Explanation

  • on: triggers on push, PR, and a nightly cron.
  • matrix.shard: [1,2,3] runs three parallel jobs; each runs --shard=k/3.
  • setup-node with cache: 'npm' speeds installs; npm ci is deterministic.
  • playwright install --with-deps makes browsers launch on the clean runner.
  • Each shard uploads a blob report (if: always() so failures still upload).
  • The merge-report job downloads all blobs and merges them into one HTML report artifact.

Common Mistakes

  • Missing --with-deps → browser launch failures on the runner.
  • Not using if: always() on artifact upload → no report when tests fail.
  • Committing secrets to YAML instead of using secrets.
  • Forgetting needs/merge-multiple when merging sharded reports.

Best Practices

  • Cache npm; pin Node; npm ci + --with-deps.
  • Shard with a matrix; merge blob reports into one HTML artifact.
  • Secrets via secrets.*, config via vars.*.
  • Upload artifacts with if: always() and sensible retention.
  • Add branch protection requiring this workflow to pass before merge.

Interview Questions

  • Q: How do you run Playwright on GitHub Actions? A: A workflow: checkout, setup-node, npm ci, playwright install --with-deps, playwright test, upload report artifact.
  • Q: How to parallelize? A: A matrix of shards running --shard=k/n, then a merge job.
  • Q: How to handle credentials? A: Repo Actions secrets, injected as env vars (masked).
  • Q: Why if: always() on upload? A: To keep reports/traces even when tests fail (the case you most need them).

Practice Exercise

Add .github/workflows/playwright.yml (above), configure secrets/vars, push a PR, and confirm the sharded run + merged HTML report artifact. Add branch protection requiring the workflow.

Real-World Scenario

An open-source project runs this exact pattern: PRs trigger a 3-shard run; the merged HTML report (with traces) is downloadable from the run page, so maintainers debug contributor PR failures without cloning the branch.

Best-Practices Recap

npm ci → --with-deps → shard via matrix → blob reports → merge → upload with if: always() → secrets via secrets.* → gate merges on green.

Jenkins

Level: L4.

What is it?

Jenkins is a widely-used, self-hosted automation server. You define pipelines as code in a Jenkinsfile (declarative pipeline) that checks out, installs, runs Playwright, and publishes reports.

Why do we need it?

Many enterprises run Jenkins on-prem. As an SDET you’re expected to wire your suite into a Jenkins pipeline and read/fix its output.

How does it work?

A Jenkinsfile in the repo defines stages. Jenkins runs them on an agent (often a Docker image with browsers). Post-build steps publish JUnit and the HTML report.

Complete working Jenkinsfile (declarative)

pipeline {  agent {    docker {      // Official Playwright image: browsers + deps preinstalled      image 'mcr.microsoft.com/playwright:v1.55.0-jammy'      args '-u root:root'    }  }  options {    timeout(time: 30, unit: 'MINUTES')    disableConcurrentBuilds()    buildDiscarder(logRotator(numToKeepStr: '20'))  }  parameters {    string(name: 'BASE_URL', defaultValue: 'https://qa.myapp.com', description: 'Target env')    choice(name: 'SUITE', choices: ['smoke', 'regression'], description: 'Which tests')  }  environment {    CI = 'true'    BASE_URL = "${params.BASE_URL}"    // secrets pulled from Jenkins credentials, never hard-coded:    TEST_USER = credentials('qa-test-user')    TEST_PASSWORD = credentials('qa-test-password')  }  stages {    stage('Checkout') {      steps { checkout scm }    }    stage('Install') {      steps {        sh 'npm ci'        // image already has browsers; run install to be safe on version bumps:        sh 'npx playwright install --with-deps'      }    }    stage('Test') {      steps {        sh "npx playwright test --grep @${params.SUITE} --reporter=junit,html"      }    }  }  post {    always {      junit 'results.xml'      publishHTML(target: [        reportName: 'Playwright Report',        reportDir: 'playwright-report',        reportFiles: 'index.html',        keepAll: true, alwaysLinkToLastBuild: true, allowMissing: false      ])      archiveArtifacts artifacts: 'test-results/**', allowEmptyArchive: true    }    failure {      echo 'Tests failed — see the Playwright Report and archived traces.'    }    cleanup {      cleanWs()    }  }}

Config to emit JUnit (needed by the junit step)

// playwright.config.tsreporter: [['junit', { outputFile: 'results.xml' }], ['html', { open: 'never' }]],

Line-by-Line Explanation

  • agent { docker { image 'mcr.microsoft.com/playwright:...' } } runs on the official image with browsers/deps preinstalled — no browser-launch failures.
  • parameters let you pick environment/suite at build time.
  • environment injects secrets via credentials(...) — Jenkins masks them; nothing is hard-coded.
  • npm ci + playwright install --with-deps = reproducible, launch-ready.
  • --grep @${SUITE} runs smoke or regression based on the chosen parameter.
  • post { always { junit ...; publishHTML ...; archiveArtifacts ... } } publishes results and traces every run (even on failure).
  • cleanWs() keeps the agent clean between builds.

Multi-shard variation (parallel across agents)

stage("Test (sharded)") {  parallel {    stage("shard 1") { steps { sh 'npx playwright test --shard=1/2 --reporter=blob' } }    stage("shard 2") { steps { sh 'npx playwright test --shard=2/2 --reporter=blob' } }  }}stage("Merge reports") {  steps { sh 'npx playwright merge-reports --reporter=html ./blob-report' }}

Common Mistakes

  • Not using the Playwright Docker image and forgetting --with-deps → browser launch errors.
  • Hard-coding credentials instead of credentials(...).
  • Not publishing JUnit/HTML → failures invisible/undebuggable.
  • No timeout → hung builds pin an agent.

Best Practices

  • Use the official Playwright image (pin the version to your @playwright/test).
  • Secrets via Jenkins credentials.
  • Publish JUnit + HTML + archive test-results/ (traces) always.
  • Parameterize env/suite; shard for speed; set a timeout.

Interview Questions

  • Q: How do you run Playwright in Jenkins? A: A declarative Jenkinsfile on the Playwright Docker agent: npm ci, playwright install --with-deps, playwright test, then publish JUnit/HTML in post.
  • Q: How do you handle secrets? A: Jenkins credentials(...) injected as env vars, masked in logs.
  • Q: How to publish results? A: junit 'results.xml' + publishHTML + archiveArtifacts in post { always }.
  • Q: How to speed a large suite? A: parallel sharded stages, then merge-reports.

Practice Exercise

Add the Jenkinsfile above to your repo, configure qa-test-user/qa-test-password credentials, and run a parameterized build for smoke then regression. Open the published HTML report.

Real-World Scenario

An enterprise runs nightly regression in Jenkins on the Playwright image, sharded across two agents, with results feeding a dashboard via JUnit. On failure, archived traces let engineers debug the exact run without reproducing locally.

Docker

Level: L4.

What is it?

Packaging your test environment (Node, browsers, OS deps, your code) into a container so it runs identically everywhere — locally and in CI.

Why do we need it?

“Works on my machine” dies with Docker. The official Playwright image has the exact browsers + system libraries, eliminating environment drift and browser-launch issues in CI.

How does it work?

Base your image on mcr.microsoft.com/playwright, copy your project in, install deps, and run tests. Run the container locally or as the CI agent.

Dockerfile (working)

# Pin the version to match your @playwright/test in package.jsonFROM mcr.microsoft.com/playwright:v1.55.0-jammyWORKDIR /app# Install deps first (better layer caching)COPY package*.json ./RUN npm ci# Copy the rest of the projectCOPY . .# Default command runs the suiteCMD ["npx", "playwright", "test"]

Build & run

docker build -t pw-tests .docker run --rm \  -e BASE_URL=https://qa.myapp.com \  -e TEST_USER=std -e TEST_PASSWORD=secret \  -v "$(pwd)/playwright-report:/app/playwright-report" \  pw-tests

Practical Example — docker-compose (app + tests)

# docker-compose.ymlservices:  app:    image: myapp:latest    ports: ["3000:3000"]  tests:    build: .    depends_on: [app]    environment:      BASE_URL: http://app:3000    command: npx playwright test    volumes:      - ./playwright-report:/app/playwright-report
docker compose up --build --abort-on-container-exit

Line-by-Line Explanation

  • Pinning the image version to @playwright/test avoids “browser/driver version mismatch”.
  • Copying package*.json and running npm ci before copying the rest caches the dependency layer — rebuilds are fast when only tests change.
  • The compose file starts the app, then the tests target it at http://app:3000 over the internal network; the report is mounted out to the host.

Common Mistakes

  • Image version ≠ @playwright/test version → driver mismatch errors.
  • Installing browsers manually on a plain node image (heavy, error-prone) instead of the Playwright image.
  • Not mounting the report/artifacts out → results lost when the container exits.
  • Copying everything before npm ci → no dependency-layer caching.

Best Practices

  • Use the official Playwright image; pin its version to your dep.
  • Order Dockerfile for layer caching (deps before code).
  • Pass config via env vars; mount reports/traces to the host.
  • Keep images lean; use .dockerignore (node_modules, test-results).

Interview Questions

  • Q: Why run Playwright in Docker? A: Identical browsers/OS deps everywhere → no environment drift or launch failures.
  • Q: Which base image? A: mcr.microsoft.com/playwright:<version>-jammy, pinned to your @playwright/test.
  • Q: How do tests reach the app in compose? A: Via the service name on the internal network (e.g., http://app:3000).
  • Q: How to get the report out of the container? A: Bind-mount playwright-report (and test-results) to the host.

Practice Exercise

Write the Dockerfile above, build it, and run your suite in the container with env-injected BASE_URL. Mount the report out and open it on the host. Bonus: add the compose file with a dummy app.

Real-World Scenario

A team’s CI failed with browser-launch errors on a bare Node image. Switching to the pinned Playwright Docker image made local and CI runs byte-for-byte consistent, and the “works on my machine” failures disappeared overnight.

FAQs

How do you run Playwright in GitHub Actions?

Check out the code, install Node and dependencies, run npx playwright install --with-deps, run npx playwright test, and upload the playwright-report folder as an artifact even when tests fail.

Why use the official Playwright Docker image?

It contains the browsers and system dependencies matching a specific Playwright version, so tests and screenshots behave the same locally and in CI.