Currents Team
Currents Team

Playwright's New --last-failed-file Flag: Re-Run Only Failed Tests in GitHub Actions

Playwright v1.61 adds --last-failed-file, a flag that makes caching --last-failed in CI simpler and more reliable. Learn how to set it up in sharded GitHub Actions workflows.

Playwright's New --last-failed-file Flag: Re-Run Only Failed Tests in GitHub Actions

Playwright v1.61 shipped a small flag that makes caching --last-failed in CI significantly cleaner: --last-failed-file. If you use sharded Playwright tests in GitHub Actions and have ever wanted to wire up "Re-run failed jobs" without reaching into Playwright's internals, this is for you.

When This Is the Right Tool

Playwright's retries config already handles the common case: flaky tests that need a couple of attempts to pass. If a test is occasionally unreliable, retries are the better fit — they handle it automatically without human intervention.

--last-failed solves a different problem. Think about a release that gets blocked because a CI environment variable wasn't set correctly, or a feature flag wasn't enabled in the environment under test. The tests that failed aren't broken — the environment was. Once you've fixed the config, you want to re-run only the affected tests immediately, without touching test code or waiting for a full shard replay across your entire suite.

That's the scenario where "Re-run failed jobs" with --last-failed is the fastest path to unblocking a release.

Why --last-failed Was Awkward in CI

Playwright has had --last-failed for a while. Locally, it works great. Run your tests, some fail, re-run with --last-failed, and only the failures execute. Simple.

In CI, wiring this up required some internal knowledge. The .last-run.json file that powers --last-failed lived inside the first project's outputDir. Since that path is defined in your Playwright config, your CI workflow had to hardcode it — leaking project internals into your pipeline just to cache a file.

Without that setup, clicking "Re-run failed jobs" in GitHub Actions meant Playwright re-ran every test assigned to that shard. Not just the ones that failed. All of them. Minutes wasted, CI budget burned on work that already passed.

Improving It at the Source

At Currents, we've been building tooling around --last-failed in CI for a while. Our GitHub Action worked around the hardcoded file path by reaching into Playwright's internal output directory structure. It worked, but it was fragile. Every time Playwright changed internals, we had to adapt.

We wanted to clean this up on the Playwright side, so we opened a PR, which the Playwright team helped us get landed in time for the v1.61 release.

The New Flag

The result is a new flag in Playwright v1.61: --last-failed-file <path>. You can also set it via the PLAYWRIGHT_LAST_RUN_OUTPUT_FILE environment variable.

It does exactly what the name suggests: tells Playwright where to write and read the .last-run.json file. You can place it anywhere. Outside outputDir, outside test-results/, in a dedicated cache directory. The file no longer gets wiped on test start, and you don't need to know which project's output directory Playwright chose internally.

One flag. But it removes the need to know Playwright's internal directory layout, making the caching setup straightforward.

Using It with GitHub Actions Retries

The idea is straightforward: cache the .last-run.json file per shard, so that when you retry a failed job, Playwright can read it and skip the tests that already passed.

Cache Key Design

The cache key needs three properties:

  1. It must be scoped to the current workflow run (run_id), so stale caches from previous runs don't interfere.
  2. It must include the shard index, so each shard caches its own file.
  3. It must include run_attempt, because GitHub Actions caches are immutable. Once a key is written, it can't be overwritten. Each retry needs to write a new entry.

The pattern:

key: last-run-${{ github.run_id }}-${{ matrix.shard }}-${{ github.run_attempt }}
restore-keys: |
  last-run-${{ github.run_id }}-${{ matrix.shard }}

The restore-keys prefix (without run_attempt) lets a retry find and restore the cache from the previous attempt. On a fresh run, nothing matches, so all tests execute normally.

Workflow Steps

You need to split cache restore and save into separate steps. The combined actions/cache action only saves on job success — but the whole point is to cache the failure results so the retry can skip them. Use actions/cache/restore and actions/cache/save with if: always() on the save:

- name: Restore last-run file
  uses: actions/cache/restore@v5
  with:
    path: .last-run.json
    key: last-run-${{ github.run_id }}-${{ matrix.shard }}-${{ github.run_attempt }}
    restore-keys: |
      last-run-${{ github.run_id }}-${{ matrix.shard }}-

- name: Run Playwright tests
  run: |
    EXTRA_FLAGS=""
    if [ -f .last-run.json ]; then
      EXTRA_FLAGS="--last-failed"
    fi
    npx playwright test --shard=${{ matrix.shard }}/${{ strategy.job-total }} $EXTRA_FLAGS --last-failed-file .last-run.json

- name: Save last-run file
  if: always()
  uses: actions/cache/save@v5
  with:
    path: .last-run.json
    key: last-run-${{ github.run_id }}-${{ matrix.shard }}-${{ github.run_attempt }}

The key points:

  • actions/cache/save with if: always() is mandatory. If you use the combined actions/cache action instead, it skips the save when the job fails — exactly the case where you need it most.
  • --last-failed-file .last-run.json is always passed so Playwright writes to a known location, not inside test-results/ where it would be wiped.
  • --last-failed is only added when the file already exists, meaning this is a retry run.
  • On first run, no cache exists, so all tests execute and the file is written and saved (even if tests fail).
  • On retry, the cache restores the file, and only the failures re-run.

run_attempt is in the save key so each attempt writes a new cache entry (GitHub cache keys are immutable). The restore-keys prefix without run_attempt lets each retry find the previous attempt's entry.

Gotchas and Limitations

A few things to watch out for:

The file is shard-scoped. The .last-run.json records only the tests that ran in that specific shard. No cross-shard contamination. This is exactly what you want.

outputDir gets wiped. This is why --last-failed-file exists. If you point it somewhere inside test-results/, it will get deleted before tests start. Put it in the project root or a dedicated cache directory.

GitHub cache immutability. GitHub Actions caches are write-once per key. The run_attempt strategy above handles this. Alternatively, you can use actions/cache/save and actions/cache/restore as separate steps for more control.

Test file changes between attempts. If someone pushes a commit that restructures test files between the initial run and the retry, --last-failed might not match tests correctly. In practice this rarely happens because retries are immediate.

Use "Re-run failed jobs", not "Re-run all jobs". If you click "Re-run all jobs", every shard restarts — including the ones that passed. Those shards have no .last-run.json from a failed run, so they'll execute their full test suite again rather than skipping anything. You'd be re-running tests that already passed, which defeats the purpose. A more advanced setup can detect whether the cached file represents a failed or passed run and skip the --last-failed flag accordingly, but that's outside the scope of this example.

Cache must save on failure. Don't use the combined actions/cache action — it skips the save when the job fails. Use actions/cache/restore to restore and actions/cache/save with if: always() to save. Without this, a failed shard will never write its cache entry and the retry will re-run every test.

Skip the Caching Setup

The native approach works. But the cache key design, the conditional logic, and the edge cases add up. If you'd rather not manage it yourself, our GitHub Action handles shard-aware caching automatically:

- name: Playwright Last Failed
  id: last-failed
  uses: currents-dev/playwright-last-failed@v2
  with:
    pw-output-dir: test-results
    matrix-index: ${{ matrix.shard }}
    matrix-total: ${{ strategy.job-total }}

- name: Run Playwright
  run: npx playwright test ${{ steps.last-failed.outputs.extra-pw-flags }}

It takes care of the cache keys, shard indexing, and conditional flags. Full setup docs are here.

Wrapping Up

A small flag, but it meaningfully reduces the friction of setting up --last-failed in CI. If you're sharding Playwright in GitHub Actions, it's the cheapest improvement you'll make this quarter. No new infrastructure, no third-party dependencies (unless you want them), just a flag and a cache step.


Scale your Playwright tests with confidence.
Join hundreds of teams using Currents.
Learn More

Trademarks and logos mentioned in this text belong to their respective owners.

Related Posts