Level 4 · Test Lead Automation · Practice 03

Playwright on GitHub Actions (Free Tier)

Push a small Playwright repo to GitHub, add a workflow file, and watch your tests run on every push — on GitHub's runners, for free, with the HTML report downloadable as a build artifact.

1 Goal

Take the Playwright project you built in Practice 01 (or a fresh copy), push it to a public GitHub repository, and add a .github/workflows/test.yml file that runs npx playwright test on every push and pull request. The workflow should install Node, cache dependencies, install Playwright browsers, run the suite, and upload the HTML report as a downloadable artifact. By the end, a teammate clicking "Actions" on your repo should see green runs for good commits, red runs for bad ones, and be able to download the HTML report with the trace viewer attached — without ever cloning your code. That is the bar for "CI" in 2025.

2 Sign up for GitHub & understand the free tier

  1. Create a free GitHub account Go to github.com/signup. Pick a username, use any email, and verify. The free plan is permanent; no credit card required.
  2. Understand what you get on the free tier GitHub Actions is metered, but the headline numbers are generous for public repos.
    Repo typeActions minutes / monthStorageNotes
    PublicUnlimitedUnlimited (subject to fair-use)Use this for the exercise — no meter.
    Private (Free plan)2,000 / month500 MBLinux minutes count 1×, Windows 2×, macOS 10×.
    Bottom line: Make the exercise repo public. Playwright runs on Linux by default, which is the cheapest runner, and the public-repo tier has no minute cap. You can run the workflow a hundred times a day and pay nothing.
  3. Install Git locally If git --version already returns a version, skip this step. Otherwise download from git-scm.com/downloads.
    Install via winget (or use the MSI)
    winget install --id Git.Git -e
    git --version
    Terminal (Xcode Command Line Tools)
    xcode-select --install
    git --version
    Debian / Ubuntu
    sudo apt-get update && sudo apt-get install -y git
    git --version
  4. Configure your Git identity once
    All platforms
    git config --global user.name "Your Name"
    git config --global user.email "you@example.com"

3 Project setup

You can reuse the project from Practice 01 or scaffold a fresh one. The steps below assume a fresh repo so the exercise stands alone.

  1. Create and enter the project folder
    All platforms
    mkdir playwright-ci-lab
    cd playwright-ci-lab
    npm init -y
    npm install --save-dev @playwright/test@latest
    npx playwright install --with-deps
    The --with-deps flag installs system libraries on Linux; it's a no-op on Windows and macOS.
  2. Target this final layout
    Tree
    playwright-ci-lab/
      .github/
        workflows/
          test.yml
      tests/
        smoke.spec.ts
      playwright.config.ts
      package.json
      package-lock.json
      .gitignore
  3. Add a .gitignore
    .gitignore
    node_modules/
    playwright-report/
    test-results/
    blob-report/
    playwright/.cache/

4 Write the tests, config, and workflow

Four files total. Paste each one in full.

a) playwright.config.ts

playwright.config.ts
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  timeout: 30_000,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [['html', { open: 'never' }], ['list']],
  use: {
    actionTimeout: 10_000,
    trace: 'retain-on-failure',
    screenshot: 'only-on-failure',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
  ],
});

b) tests/smoke.spec.ts

tests/smoke.spec.ts
import { test, expect } from '@playwright/test';

test('example.com loads and has the expected heading', async ({ page }) => {
  await page.goto('https://example.com');
  await expect(page.getByRole('heading', { name: 'Example Domain' })).toBeVisible();
});

test('playwright.dev homepage renders', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await expect(page).toHaveTitle(/Playwright/);
});

test('saucedemo standard user can log in', async ({ page }) => {
  await page.goto('https://www.saucedemo.com/');
  await page.getByPlaceholder('Username').fill('standard_user');
  await page.getByPlaceholder('Password').fill('secret_sauce');
  await page.getByRole('button', { name: 'Login' }).click();
  await expect(page).toHaveURL(/inventory\.html/);
});

c) package.json — add a test script

Open package.json and replace the scripts block so it reads:

package.json (scripts section)
"scripts": {
  "test": "playwright test"
}

d) .github/workflows/test.yml — the CI file. Create the folders first:

PowerShell
New-Item -ItemType Directory -Force -Path .github/workflows | Out-Null
bash/zsh
mkdir -p .github/workflows

Then write .github/workflows/test.yml with this exact content:

.github/workflows/test.yml
name: Playwright Tests

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    name: Run Playwright suite
    timeout-minutes: 15
    runs-on: ubuntu-latest
    steps:
      - name: Check out the repository
        uses: actions/checkout@v4

      - name: Set up Node.js 20
        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()
        uses: actions/upload-artifact@v4
        with:
          name: playwright-report
          path: playwright-report/
          retention-days: 7
Why if: always() on the upload step? Without it, the upload only runs if the previous step succeeded. When tests fail, that is exactly when you most want the report. if: always() runs the step regardless, so red builds still publish their artifacts.
Pin your actions. Use @v4, not @main. Pinning to a major version is the balance between "I want security patches" and "I don't want my workflow to break overnight when someone merges a breaking change upstream."

5 Push the repo & verify the run

  1. Create a new public repo on GitHub Go to github.com/new. Name it playwright-ci-lab. Select Public. Do NOT tick "Initialize with README" — you'll push your existing files.
  2. Initialise git locally and push Replace YOUR-USERNAME with your GitHub handle.
    All platforms
    git init
    git add .
    git commit -m "Initial Playwright CI lab"
    git branch -M main
    git remote add origin https://github.com/YOUR-USERNAME/playwright-ci-lab.git
    git push -u origin main
    On the first push, GitHub prompts for credentials. Use a personal access token or the browser-based flow that the Git Credential Manager opens for you.
  3. Watch the workflow run Open https://github.com/YOUR-USERNAME/playwright-ci-lab/actions. You'll see a run called "Playwright Tests" with a yellow dot. Click into it to watch live logs. The full run on a public Ubuntu runner takes roughly two to three minutes: most of that is the browser download on first run, then about thirty seconds for the tests themselves.
  4. Expected result A green tick next to "Run Playwright Tests". If all three tests pass, the last log line in the Run Playwright tests step reads:
    Actions log (abridged)
    3 passed (18.4s)
    Scroll down to the Artifacts section at the bottom of the run summary — you'll see playwright-report listed.

6 Download the report & open the trace

  1. Click the artifact to download it The download is a zip. Extract it — you'll get a playwright-report/ folder with index.html and a data/ subfolder.
  2. Open the report locally You can't just double-click index.html; Playwright reports need to be served because they load JSON via fetch(). Run it through Playwright's own server:
    All platforms
    npx playwright show-report ./playwright-report
    A browser opens at http://localhost:9323 with the same report you'd see locally — traces, screenshots, videos, everything.
  3. Force a failure on purpose (optional) Change one assertion in smoke.spec.ts to fail (e.g. toHaveTitle(/NotReallyPlaywright/)), commit, push. Watch Actions run red, then download the artifact. The failing test's trace tab should contain the full recording. This is the same triage loop as Practice 01 — only now the failure happened on a server you don't own.
What you just built is production-grade. Every serious QA team runs a variation of this: actions/checkoutsetup-node with cache → npm ciplaywright install --with-deps → run → upload on always. Yours is missing two things a real team adds: sharding (Challenge 1) and a status badge in the README. That's it.

7 Troubleshooting

The workflow doesn't appear in the Actions tab after pushing

Most likely your file isn't at the exact path .github/workflows/test.yml. GitHub only discovers workflow files under that literal folder structure, and the file extension must be .yml or .yaml. Check in the GitHub web UI that the path renders as you expect. If it's there but "No workflow runs yet", the YAML has a syntax error — open the file in GitHub's web editor; a red banner near the top will show the parse error.

Error: Process completed with exit code 1 and no obvious test failure

Expand the Install Playwright browsers step. The most common cause is running npx playwright install without --with-deps on Linux, which leaves Chromium unable to launch. Always use npx playwright install --with-deps on GitHub-hosted Ubuntu runners.

The artifact uploads but show-report says "No data"

You uploaded the wrong folder. Playwright's HTML report lives in playwright-report/, not test-results/. The path in the workflow must match exactly: path: playwright-report/. If you changed the outputFolder in playwright.config.ts, update the workflow path to match.

Free-tier minutes panic — "Am I about to get billed?"

If the repo is public, minutes are unlimited — the meter doesn't apply. Double-check the repo visibility at Settings → General → Danger Zone. If it's private, you have 2,000 Linux minutes per month on the free plan; billing settings at Settings → Billing and plans will show your current usage. You cannot be charged by accident — GitHub will simply stop running jobs when the free allowance is exhausted unless you have added a payment method and lifted the spending cap.

8 Challenge

Make it look like a real team's CI

Challenge 1 — shard the test run across two jobs. Add a strategy: matrix: block with shardIndex: [1, 2] and shardTotal: [2], then pass --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} to npx playwright test. Each job uploads its own artifact with a name suffixed by the shard number. Sharding is how real projects keep CI under five minutes on a 300-test suite, and it is a standard interview talking point for lead-level roles.

Challenge 2 — add a status badge to the README. Create a README.md at the repo root with the line ![Playwright Tests](https://github.com/YOUR-USERNAME/playwright-ci-lab/actions/workflows/test.yml/badge.svg). Push it. The badge turns green on a passing run, red on failure. It is the single cheapest signal you can give reviewers that your repo actually tests itself — and it costs zero minutes to keep green.