Skip to main content
When a CI run fails, re-running the entire job wastes time on tests that have already passed. This workflow runs only the failed tests on subsequent attempts.
ExampleYour test suite has 500 E2E tests.
  • A full run takes 1 hour
  • 50 tests fail and take about 6 minutes to run
Re-running the entire suite means executing 450 passing tests again. With this approach, only the 50 failed tests are re-run.Result: You save approximately 54 minutes per re-run.
ScenarioFull suite rerun timeFailed only rerun timeTime savedCost saved on LinuxCost saved on WindowsCost saved on macOS
Per rerun60 min6 min54 min$0.324$0.54$3.348
Per day (10 reruns)600 min60 min540 min (9 hrs)$3.24$5.40$33.48
Per 30-day month (10 reruns daily)18,000 min1,800 min16,200 min (270 hrs)$97.20$162.00$1,004.40
Note: Pricing is based on GitHub’s official Actions runner pricingfor GitHub-hosted runners:
  • Linux (2-core): $0.006 per minute
  • Windows (2-core): $0.010 per minute
  • macOS: $0.062 per minute
This workflow covers sharded execution, GitHub Actions reruns, failed-tests-only reruns, and full-suite fallback when no metadata exists.

Quick Reference

TaskCommand/ActionLink
Run full test suitenpx playwright test-
Cache failed test metadatanpx tdpw cacheSetting up cache
Get last failed testsnpx tdpw last-failedHow it works
Re-run detectionCheck github.run_attemptWorkflow logic
Upload HTML reportactions/upload-artifact@v4Quick start

How it works

Two CLI commands power this workflow:
  • npx tdpw cache stores failed test metadata after a run
  • npx tdpw last-failed returns Playwright arguments for the last failed tests
The workflow detects re-runs using github.run_attempt:
ConditionBehavior
run_attempt = 1Run full test suite
run_attempt > 1, flags foundRun failed tests only
run_attempt > 1, no flagsRun full test suite (fallback)
Playwright compatibility: Since @playwright/test@1.50+, Playwright has native --last-failed support. TestDino extends this with cross-runner caching, shard awareness, and workflow-level persistence.

Quick start

Add this logic to your Playwright step:
- name: Run Playwright with rerun logic
  env:
    TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }}
  run: |
    echo "Run attempt: ${{ github.run_attempt }}"
    if [[ "${{ github.run_attempt }}" -gt 1 ]]; then
      npx tdpw last-failed > flags.txt
      FLAGS="$(cat flags.txt)"
      if [[ -n "$FLAGS" ]]; then
        echo "Re-running only failed tests"
        eval "npx playwright test $FLAGS"
        exit 0
      fi
    fi
    echo "Running full test suite"
    npx playwright test

- name: Upload HTML report
  uses: actions/upload-artifact@v4
  with:
    name: Playwright Test Report
    path: ./playwright-report
    retention-days: 14

- name: Cache failed test metadata
  if: always()
  env:
    TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }}
  run: npx tdpw cache
The cache step runs with the if: always() condition, so failures are recorded even when tests fail.
Need to set up your API token? See Generate API Keys for instructions.

How to re-run failed tests in GitHub Actions?

1

Open the failed job

In the GitHub Actions run, find the failed job for Playwright:
  1. Go to your GitHub repository’s Actions tab.
  2. Select the failed workflow run.
  3. Under Jobs, open the job where Playwright tests get executed & fail.
This is the main job that executes Playwright and sharding.
Step 1: Open failed job
2

Check the run attempt

Inside the job, open the Playwright execution step. In the logs, you can see:
  • GitHub run attempt: <number>
  • Re-run detection output Step 2: Check run attempt
3

Identify the failed tests

Scroll to the end of the Playwright output. GitHub Actions show:
  • Total failed test count
  • Failed test titles Step 3: Identify failed tests
4

Confirm caching

Open the Cache failed test metadata step. Look for:
Cache data submitted successfully
This confirms TestDino stored the failure metadata.Step 4: Confirm caching
5

Confirm Re-run

Re-run the failed pipeline and confirm the execution of only the last failed tests.
  • The workflow detects run_attempt > 1
  • tdpw last-failed retrieves the failed tests
  • Only those tests are executed Step 5: Confirm re-run part 1 Step 5: Confirm re-run part 2
First run: 12 tests across 3 shards. Two shards fail.
ShardTestsResult
Shard 14 tests2 Passed, 2 Failed
Shard 24 testsPassed
Shard 34 tests3 Passed, 1 Failed
Re-run comparison:
├── Shard 1: runs 4 tests
├── Shard 3: runs 4 tests
└── Total: 12 tests
Re-runs all tests in failed shards, including tests that already passed.

Full workflow

For a complete workflow with sharding and report merging, see the example repository.
File name: .github/workflows/playwright.ymlRepository: github.com/testdino-hq/playwright-sample-tests-javascript
name: Run Playwright tests

on:
  push:
  pull_request:
  schedule:
    - cron: '30 3 * * 1-5' # 11:00 AM IST
  workflow_dispatch:

jobs:
  run-tests:
    name: Run Playwright tests ${{ matrix.shardIndex }}/3
    runs-on: ubuntu-latest

    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3]
        shardTotal: [3]

    steps:
      - uses: actions/checkout@v4

      # ✅ REQUIRED: Node 20
      - name: Setup Node.js 20.x
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Create .env file
        run: |
          echo "USERNAME=${{ secrets.USERNAME }}" >> .env
          echo "PASSWORD=${{ secrets.PASSWORD }}" >> .env
          echo "NEW_PASSWORD=${{ secrets.NEW_PASSWORD }}" >> .env
          echo "FIRST_NAME=${{ secrets.FIRST_NAME }}" >> .env
          echo "STREET_NAME=${{ secrets.STREET_NAME }}" >> .env
          echo "CITY=${{ secrets.CITY }}" >> .env
          echo "STATE=${{ secrets.STATE }}" >> .env
          echo "COUNTRY=${{ secrets.COUNTRY }}" >> .env
          echo "ZIP_CODE=${{ secrets.ZIP_CODE }}" >> .env

      - name: Cache npm dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install deps + browsers
        run: |
          npm ci
          npx playwright install --with-deps chromium firefox webkit

      # ✅ FULL + RERUN LOGIC
      - name: Run Playwright (rerun failed tests if applicable)
        env:
          TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }}
          SHARD_INDEX: ${{ matrix.shardIndex }}
          SHARD_TOTAL: ${{ matrix.shardTotal }}
        run: |
          echo "GitHub run attempt: ${{ github.run_attempt }}"

          if [[ "${{ github.run_attempt }}" -gt 1 ]]; then
            echo "Detected re-run. Checking failed test metadata from TestDino."

            npx tdpw last-failed \
              --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} \
              > last-failed-flags.txt

            EXTRA_PW_FLAGS="$(cat last-failed-flags.txt)"

            if [[ -n "$EXTRA_PW_FLAGS" ]]; then
              echo "Running only failed tests for this shard:"
              echo "$EXTRA_PW_FLAGS"

              # IMPORTANT: JSON + BLOB BOTH REQUIRED
              # Ensure playwright-report directory exists
              mkdir -p ./playwright-report
              eval "npx playwright test $EXTRA_PW_FLAGS"
              exit 0
            fi

            echo "No failed test metadata found. Falling back to full shard."
          fi

          # First run (full shard)
          # Ensure playwright-report directory exists
          mkdir -p ./playwright-report
          npx playwright test \
            --grep="@chromium|@firefox|@webkit|@android|@ios" \
            --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}

      - name: Upload blob report
        if: ${{ !cancelled() }}
        uses: actions/upload-artifact@v4
        with:
          name: blob-report-${{ matrix.shardIndex }}
          path: ./blob-report
          retention-days: 1

      # ✅ THIS WILL SHOW "Metadata cached successfully ✔" WHEN JSON EXISTS
      - name: Cache tdpw last failed metadata
        if: always()
        env:
          TESTDINO_TOKEN: ${{ secrets.TESTDINO_TOKEN }}
          SHARD_INDEX: ${{ matrix.shardIndex }}
          SHARD_TOTAL: ${{ matrix.shardTotal }}
        run: |
          npx tdpw cache --verbose

  merge-reports:
    name: Merge Reports
    needs: run-tests
    if: always()
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      # ✅ Node 20 here as well
      - name: Setup Node.js 20.x
        uses: actions/setup-node@v3
        with:
          node-version: '20'

      - name: Cache npm dependencies
        uses: actions/cache@v3
        with:
          path: ~/.npm
          key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-node-

      - name: Install deps + browsers
        run: |
          npm ci
          npx playwright install --with-deps

      - name: Download all blob reports
        uses: actions/download-artifact@v4
        with:
          path: ./all-blob-reports
          pattern: blob-report-*
          merge-multiple: true

      - name: Merge HTML & JSON reports
        run: npx playwright merge-reports --config=playwright.config.js ./all-blob-reports

      - name: Upload combined report
        uses: actions/upload-artifact@v4
        with:
          name: Playwright Test Report
          path: ./playwright-report
          retention-days: 14

      - name: Send TestDino report
        run: |
          npx --yes tdpw ./playwright-report \
            --token="${{ secrets.TESTDINO_TOKEN }}" \
            --upload-html \
            --upload-traces \
            --verbose

How the workflow logic works

The workflow uses a conditional check based on github.run_attempt:
if [[ "${{ github.run_attempt }}" -gt 1 ]]; then
  # Re-run: get failed tests from TestDino
else
  # First run: execute full shard
fi
  • On the first attempt (run_attempt == 1), the full shard is executed.
  • On subsequent attempts, the workflow attempts to re-run only previously failed tests.
The tdpw last-failed command outputs test filters formatted for Playwright:
-g "Verify that a New User Can Successfully Complete the Journey from Registration to a Single Order Placement @chromium|test @chromium"
These flags can be passed directly to npx playwright test.
The command uses eval to handle quoted arguments in the test filter:
eval "npx playwright test $EXTRA_PW_FLAGS"
This keeps the -g "pattern" quoting intact when passed to Playwright.

Edge cases

If a job fails during dependency installation or setup, no test metadata exists.On re-run:
  1. tdpw last-failed returns nothing
  2. The workflow detects the empty result
  3. The job runs the full shard
No manual action required. The workflow handles this case automatically:
if [[ -n "$EXTRA_PW_FLAGS" ]]; then
  # Failed tests found, run only those
  eval "npx playwright test $EXTRA_PW_FLAGS"
else
  echo "No failed test metadata found."
  echo "Running full shard instead."
fi
And then, it will run the normal execution:
mkdir -p ./playwright-report
npx playwright test \
  --grep="@chromium|@firefox|@webkit|@android|@ios" \
  --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
Playwright’s --max-failures option stops test execution after N failures. For example:
npx playwright test --max-failures=2
Playwright stops after 2 failures. Tests that did not run are not recorded.On re-run, only the failed tests execute. Skipped tests are not included as of now.
CI overview, GitHub Actions, status checks, and flaky tests.

CI Optimization Overview

CI optimization strategies for Playwright

GitHub Actions

Set up Playwright tests in GitHub Actions

GitHub Status Checks

Configure PR status checks

Flaky Tests

Detect and fix flaky tests