---
title: "Run Playwright Tests on Pull Requests, Keep Merges Fast"
description: "A GitHub Actions design to run Playwright tests on pull requests: a fast selected tier on PRs, the full suite at merge, and one required check."
canonical: "https://currents.dev/posts/run-playwright-tests-on-pull-requests-keep-merges-fast"
updated: "2026-10-29"
---

# Run Playwright Tests on Pull Requests, Keep Merges Fast

Published 2026-10-29 by Walter Galvão.

Canonical URL: https://currents.dev/posts/run-playwright-tests-on-pull-requests-keep-merges-fast

A GitHub Actions design to run Playwright tests on pull requests: a fast selected tier on PRs, the full suite at merge, and one required check.

---


> **TL;DR**
>
> On pull requests, a preliminary `--only-changed` run gives a faster feedback loop and slightly lower CI consumption, as long as the full test suite still runs after it. In a merge queue, the `merge_group` trigger runs the full suite; without a queue, a push to `main` runs it.
>
> 1. Make the full suite, behind a summary job, the single required check, and add a concurrency group with `cancel-in-progress: true` so superseded PR runs that are still running get cancelled.
> 2. Add a preliminary run on pull requests with the base branch passed: `--only-changed=origin/$GITHUB_BASE_REF`.
> 3. Add shards last, sized to the budget, once the blob reports merge into one HTML report.

Running the whole Playwright suite on every push to a pull request looks like the safe default, until the suite grows. Every PR waits on the slowest job, every fixup commit restarts the clock, and the e2e check becomes the one reviewers rerun when it goes red and stop reading when it stays yellow.

A gate in that state stops protecting anything. Someone makes it optional for one urgent release, or switches it off after a week of red runs nobody could explain, and the regressions it was meant to catch merge unchecked. A slow check and a noisy check end the same way: people route around them.

The fix is to stop asking one run to answer two questions. The author needs to know quickly whether the change broke something obvious. The merge needs to know the whole suite passes. Answer them separately. Playwright's [CI docs](https://playwright.dev/docs/ci) say a preliminary run with `--only-changed` gives a faster feedback loop and slightly lower CI consumption while you work on pull requests, as long as the full test suite still runs after it.

## Splitting PR and merge runs

One workflow running one suite on every event is where the slowdown starts. The official Playwright workflow for GitHub Actions installs the dependencies and Playwright, then runs the tests on every push and pull request to `main` or `master` ([Playwright's CI guide](https://playwright.dev/docs/ci)). That's the right first version and the wrong long-term design, because a PR author and a merge need different answers.

Extend that workflow file instead of adding a second one next to it. Two workflows that both test pull requests are two things to keep in sync and two results to read.

<AntiPattern title="The full suite on every push" id="anti-pattern-the-full-suite-on-every-push">

The workflow triggers on `pull_request`, which by default runs it when a pull request is opened, synchronized or reopened. Each run calls `npx playwright test` with nothing selected, which runs all tests. Every push pays the full suite's runtime, and a PR that takes five fixup commits pays it five times. Nothing in the workflow stops the run for a commit that a newer push has already replaced, so that run keeps a runner busy for a result nobody will read.

<AntiPattern.Fix>

Split the triggers into two tiers. `pull_request` runs a selected subset. In a merge queue, the `merge_group` trigger runs the full suite. Without a queue, a push to `main` runs it. The PR tier is allowed to miss tests only because the full run exists, so never drop the second tier.

We'd also add a `concurrency` group keyed on the PR number, with `cancel-in-progress` on for pull requests only, so a new push replaces the run it supersedes and a merge-queue run is never cancelled halfway.

```yaml
name: Playwright Tests
on:
  pull_request:
    branches: [main]
  merge_group:
  push:
    branches: [main]

concurrency:
  group: playwright-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  test:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    env:
      # Read by playwright.config.ts to pick the flake policy
      PW_TIER: ${{ github.event_name == 'pull_request' && 'pr' || 'full' }}
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - name: Install dependencies
        run: npm ci
      - name: Type check
        run: npx tsc --noEmit
      - name: Install Playwright browsers
        run: npx playwright install --with-deps
      - name: Run the PR tier
        if: github.event_name == 'pull_request'
        run: npx playwright test --grep @smoke
      - name: Run the full suite
        if: github.event_name != 'pull_request'
        run: npx playwright test
```

`PW_TIER` tells the Playwright config which tier it is running in, and the flake policy further down reads it. The PR step here uses a tagged smoke set; `--only-changed` is another way to select tests.

Install only what the tier runs. If your PR tier uses one browser project, install that browser alone (we cover [a Chromium-only PR gate and what it saves](/posts/github-actions-cost-how-to-cut-playwright-ci-minutes-without-losing-coverage#chromium-only-for-the-pr-gate) separately). If you run inside the Playwright Docker image instead, pin the image tag to the same Playwright version your lockfile resolves.

</AntiPattern.Fix>

</AntiPattern>

## Selecting tests with --only-changed

`--only-changed` runs only the test files changed between `HEAD` and a ref you pass, and it only supports Git ([CLI reference](https://playwright.dev/docs/test-cli)). To detect the test files a changeset affects, `--only-changed` analyses the suite's dependency graph.

Without a ref, `--only-changed` defaults to running all uncommitted changes. Always pass the base branch: `--only-changed=origin/$GITHUB_BASE_REF`. Playwright's docs frame this as a preliminary run of the test files likely to fail first, for faster feedback and slightly lower CI use while you work on pull requests.

<AntiPattern title="Diffing against a shallow checkout" id="anti-pattern-diffing-against-a-shallow-checkout">

The flag gets added to the existing PR job, and the checkout step stays as it was. The docs' own fail-fast workflow forces a non-shallow checkout so that it can reference `$GITHUB_BASE_REF`. Leave the checkout shallow, and the base ref the flag names isn't there to compare against.

```yaml
# Anti-pattern: shallow checkout, so origin/$GITHUB_BASE_REF can't be referenced
name: Playwright PR tier
on:
  pull_request:
    branches: [main]
jobs:
  pr-tier:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test --only-changed=origin/$GITHUB_BASE_REF
```

<AntiPattern.Fix>

Check out with `fetch-depth: 0`, which fetches all history for all branches and tags, and run the selection on pull requests only.

```yaml
name: Playwright PR tier
on:
  pull_request:
    branches: [main]
jobs:
  pr-tier:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
        with:
          # Non-shallow checkout, so the job can reference $GITHUB_BASE_REF
          fetch-depth: 0
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx playwright install --with-deps
      - name: Run tests affected by this PR
        if: github.event_name == 'pull_request'
        run: npx playwright test --only-changed=origin/$GITHUB_BASE_REF
```

The `if` keeps the step to pull requests when you fold it into the two-tier workflow above.

</AntiPattern.Fix>

</AntiPattern>

### Where the full run goes

The docs call `--only-changed` a heuristic that might miss tests, and say the full suite should always run after the preliminary run. Put that full run in the merge tier. Chain it after the selected run in the same PR job and the full suite gives back every minute the selection saved.

### When it pays off

Selection follows the suite's dependency graph. In a monorepo, give each app its own Playwright config, so each config's graph stays narrow. Expect a change to a widely shared module to select most of the suite. That's the graph doing its job, and on that PR the tier is slow.

## A tagged smoke tier

`--only-changed` gives you a PR tier whose size follows the diff. A tagged smoke set gives you one whose size you choose, which is much easier to fit into a time budget.

Tag a test with a details object when you declare it, or with an `@` token in its title; either way, every tag must start with `@` ([annotations docs](https://playwright.dev/docs/test-annotations)). The `--grep` command-line option then runs only the tests that have a given tag.

<AntiPattern title="Grepping for a bare word" id="anti-pattern-grepping-for-a-bare-word">

Grepping for the bare word can match more than the tag: Playwright's `grep` option tests its regular expression against one string made of the project name, the test file name, the describe name (if any), the test name and the tags, separated by spaces. In the docs' example, that string reads `chromium my-test.spec.ts my-suite my-test`, so a bare word matches any project, file, describe or test name that contains it, as well as the tag.

```bash
# Anti-pattern: also matches file names, describe names and test titles
npx playwright test --grep smoke
```

<AntiPattern.Fix>

Tag the tests on purpose and grep for the tag, `@` included. Both tagging styles work:

```ts
// Paths like '/login' and '/checkout' assume use: { baseURL } in playwright.config.ts
import { test, expect } from '@playwright/test';

// Tag in the details object
test('user can sign in', { tag: '@smoke' }, async ({ page }) => {
  await page.goto('/login');
  await page.getByLabel('Email').fill('user@example.com');
  await page.getByLabel('Password').fill(process.env.TEST_USER_PASSWORD!);
  await page.getByRole('button', { name: 'Sign in' }).click();
  await expect(page).toHaveURL(/\/dashboard/);
});

// Or tag with an @ token in the title
test('checkout completes with a saved card @smoke', async ({ page }) => {
  await page.goto('/checkout');
  await page.getByRole('button', { name: 'Pay with saved card' }).click();
  await expect(page.getByText('Order confirmed')).toBeVisible();
});
```

```bash
npx playwright test --grep @smoke
```

</AntiPattern.Fix>

</AntiPattern>

### Choosing the smoke set

Pick smoke tests by what hurts users when it breaks: sign-in, checkout, payment. A folder is a bad proxy, because folders follow how the code is organised, not what users depend on.

Use `@smoke` when you need a predictable PR runtime. Use `--only-changed` when changes tend to stay inside one feature and you'd rather let the diff decide.

## Sizing shards to a budget

Shard counts picked by feel end up too low to help or so high that setup eats the gain. Start from a time budget instead. Multiply the PRs your team opens per day by the minutes each one waits on the check: that's the gate's daily cost in engineer time, and the number worth arguing about. Choose the wait you'll accept, divide the full suite's single-job runtime by it, and round up. That's your starting shard count.

Playwright shards with `--shard=x/y`; in the docs' example, four shards each run a quarter of the tests ([sharding guide](https://playwright.dev/docs/test-sharding)). On GitHub Actions, `jobs.<job_id>.strategy.matrix` spreads the shards across jobs, and the matrix runs a separate job for every combination of its values. Add a browser dimension and the job count multiplies: three browser projects times four shards is twelve jobs.

```yaml
jobs:
  playwright-tests:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npx playwright test --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
```

<AntiPattern title="Shards without fullyParallel" id="anti-pattern-shards-without-fullyparallel">

If your test files are not evenly sized, certain shards may end up running significantly more tests, while others may run fewer or even none. Playwright can only shard tests that can run in parallel, and by default that means it shards whole test files. Inside a file, tests run in order in the same worker process. When test files are unevenly sized, some shards may end up running far more tests than others, while others may run fewer or even none. So without `fullyParallel`, a whole file, however large, goes to one shard and runs there in order, in one worker process.

<AntiPattern.Fix>

Turn on `fullyParallel`. Playwright recommends it for balanced distribution across shards, especially in CI; without it, you may have to organise test files by hand to avoid imbalances.

Playwright also recommends setting workers to 1 in CI, for stability and reproducibility. Keep that, and buy PR speed with more shards rather than more workers per runner. If you're weighing the two, we compare [when workers are enough and when to add shards](/posts/optimizing-test-runtime-playwright-sharding-vs-workers#choosing-workers-over-sharding) in more depth.

```ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Recommended for balanced distribution across shards
  fullyParallel: true,
  // One worker per CI runner; scale out with shards instead
  workers: process.env.CI ? 1 : undefined,
});
```

</AntiPattern.Fix>

</AntiPattern>

## Merging shard reports

Four shards produce four partial results, and a reviewer looking at a red check shouldn't have to open four job logs to find one failure. Have each shard write a blob report instead. A blob report holds every test that ran, its results and all its attachments, such as traces and screenshot diffs, and blob reports can be merged and converted into any other Playwright report ([sharding guide](https://playwright.dev/docs/test-sharding)). To merge them, put the blob files in one directory. Their names contain the shard number, so they don't clash.

Switch the reporter to `blob` on CI and keep HTML locally:

```ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  fullyParallel: true,
  workers: process.env.CI ? 1 : undefined,
  // Each CI shard writes a blob report for the merge job
  reporter: process.env.CI ? 'blob' : 'html',
});
```

Here is the full two-tier workflow with shards. Each shard uploads its blob report with `if: ${{ !cancelled() }}`, because the shards you most need a report from are the failing ones. The `merge-reports` job declares `needs: [playwright-tests]`, as in the docs' GitHub Actions example, so it runs after the sharded job, downloads the blobs into one directory and builds one HTML report.

```yaml
name: Playwright Tests
on:
  pull_request:
    branches: [main]
  merge_group:
  push:
    branches: [main]

concurrency:
  group: playwright-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
  playwright-tests:
    timeout-minutes: 60
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        shardIndex: [1, 2, 3, 4]
        shardTotal: [4]
    env:
      PW_TIER: ${{ github.event_name == 'pull_request' && 'pr' || 'full' }}
      SHARD: ${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - run: npm ci
      - run: npx tsc --noEmit
      - run: npx playwright install --with-deps
      - name: Run the PR tier
        if: github.event_name == 'pull_request'
        run: npx playwright test --grep @smoke --shard=$SHARD
      - name: Run the full suite
        if: github.event_name != 'pull_request'
        run: npx playwright test --shard=$SHARD
      - uses: actions/upload-artifact@v5
        if: ${{ !cancelled() }}
        with:
          name: blob-report-${{ matrix.shardIndex }}
          path: blob-report
          retention-days: 1

  merge-reports:
    if: ${{ !cancelled() }}
    needs: [playwright-tests]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - uses: actions/setup-node@v6
        with:
          node-version: lts/*
      - run: npm ci
      - uses: actions/download-artifact@v5
        with:
          path: all-blob-reports
          pattern: blob-report-*
          merge-multiple: true
      - run: npx playwright merge-reports --reporter html ./all-blob-reports
      - uses: actions/upload-artifact@v5
        with:
          name: playwright-report
          path: playwright-report
          retention-days: 14
```

Reviewers download one `playwright-report` artifact and get a combined report showing all the test results from all the shards, in one place instead of four sets of logs in the Actions tab. Artifact naming and storage across many shards get their own treatment in [the blob reporter pattern at scale](/posts/playwright-ci-at-scale-github-gitlab#artifact-handling-and-the-blob-reporter-pattern).

<AntiPattern title="Annotations from every shard" id="anti-pattern-annotations-from-every-shard">

The built-in `github` reporter adds automatic failure annotations when tests run in GitHub Actions. Turning it on in the sharded job looks like the obvious way to put failures into the PR. Playwright's reporter docs advise against these annotations with a matrix strategy, because the stack-trace failures multiply and obscure the GitHub file view.

<AntiPattern.Fix>

Keep the sharded job on the blob reporter and send reviewers to the merged report. If you want the result in the PR itself, report it once for the whole run, which is what the single required check below does.

</AntiPattern.Fix>

</AntiPattern>

## CI guards in the config

Some PR failures have nothing to do with the change: a run that hangs, or a focused test someone forgot to unfocus. Each takes a few lines of config to guard against.

<AntiPattern title="Relying on timeout-minutes alone" id="anti-pattern-relying-on-timeout-minutes-alone">

The job gets a `timeout-minutes` and the Playwright config gets nothing. By default a Playwright run has no upper bound, so a suite that hangs, or slowly grows past the CI job limit, is killed by the runner mid-run and doesn't produce its report ([Playwright's CI guide](https://playwright.dev/docs/ci)). GitHub cancels the job, and the run produces no test report.

<AntiPattern.Fix>

The docs say to always set a global timeout in CI. Set `globalTimeout` on CI and keep the job's `timeout-minutes` above it, so the job limit stays the backstop rather than the thing that ends the run.

```ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Exit with an error if test.only or test.describe.only reaches CI
  forbidOnly: !!process.env.CI,
  // 45 minutes, below the workflow's timeout-minutes: 60
  globalTimeout: process.env.CI ? 45 * 60 * 1000 : undefined,
});
```

</AntiPattern.Fix>

</AntiPattern>

### Keep the job limit

Keep `timeout-minutes` on the job anyway, as the outer limit. Hangs don't always start in your tests: one report describes a GitHub Actions pull_request run, checked out without usable origin credentials, that hung before any test started ([#42630](https://github.com/microsoft/playwright/issues/42630)). That issue was closed as completed on 2026-09-09.

### Cheap checks first

`forbidOnly` makes the run exit with an error when any test or group is marked `test.only()` or `test.describe.only()`. With a focused test or suite in the code, Playwright runs the focused tests and suites and nothing else.

The workflows above also run `npx tsc --noEmit` before installing browsers. A type error then fails the job before it spends any time on browser setup.

## Flaky tests in the gate

Retries decide whether a flaky test blocks a merge or slips through it. By default, Playwright doesn't retry failing tests. With retries enabled, a failing test is retried until it passes or reaches the maximum number of retries, and a test that failed on the first run but passed on a retry is labelled flaky ([retries docs](https://playwright.dev/docs/test-retries)).

<AntiPattern title="Blanket retries, nothing fails" id="anti-pattern-blanket-retries-nothing-fails">

Retries go on for every CI run, and nothing reads the flaky label. An intermittent regression fails once, passes on the second attempt, and the check goes green. The merge goes through with the bug in it.

<AntiPattern.Fix>

Keep retries, and fail the PR tier on flaky results with `failOnFlakyTests`, which makes the run exit with an error if any test is marked flaky. Leave it off in the merge tier.

```ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  // Retry on CI so intermittent failures show up as flaky
  retries: process.env.CI ? 2 : 0,
  // PW_TIER is set by the workflow: fail on flaky results on PRs only
  failOnFlakyTests: process.env.PW_TIER === 'pr',
});
```

The split is deliberate. On the PR tier, a flaky result in tests selected for this change most likely comes from the change. In the merge queue, a failed required check removes the PR from the queue and makes the queue rebuild the groups behind it, so let a flake pass there and track it.

</AntiPattern.Fix>

</AntiPattern>

### Labels, not thresholds

Playwright gives you a per-run label and an exit code. A rule like "block the merge when the pass rate drops below a threshold" or "flag tests whose flake rate rose against last month" needs results across many runs, which one run's output doesn't hold. That takes tooling outside Playwright, or [keeping every run's report and computing a flake rate](/posts/playwright-flaky-tests-detect-them-in-ci-with-retries#keep-every-runs-report-and-compute-a-flake-rate) yourself.

Retries only surface flakiness. Fixing it is test design: tests isolated from each other, third-party services mocked, user-facing locators, and web-first assertions instead of fixed waits.

## One required check

A sharded workflow doesn't produce one result you can require. The matrix runs a separate job for every combination of its values, so four shards are four jobs.

<AntiPattern title="Requiring each shard" id="anti-pattern-requiring-each-shard">

Each `playwright-tests (1, 4)` job gets marked as required, one by one. The day the shard count changes to six, branch protection still asks for the old job names and knows nothing about the new ones. The required list now names checks the workflow no longer runs.

<AntiPattern.Fix>

Add one summary job that uses `always()` with `needs` on the sharded job, as [GitHub's required-checks guide](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/collaborating-on-repositories-with-code-quality-features/troubleshooting-required-status-checks) recommends for required checks that depend on other jobs, and mark only that job as required. Its name stays the same when the shard count changes.

```yaml
jobs:
  e2e-gate:
    if: always()
    needs: [playwright-tests]
    runs-on: ubuntu-latest
    steps:
      - name: Fail if any shard did not pass
        if: needs.playwright-tests.result != 'success'
        run: exit 1
      - name: Report success
        run: echo "All Playwright shards passed"
```

Add `e2e-gate` to the `jobs` of the workflow above, next to `merge-reports`. The gate is only as strict as the Playwright run behind it. The gate reads the sharded job's result from `needs.<job_id>.result`, whose possible values are `success`, `failure`, `cancelled`, or `skipped`.

If you'd rather see the cause in the PR than in the checks list, the [Currents GitHub App](https://docs.currents.dev/resources/integrations/github/github-app) can update the commit status once per run event, whatever the number of groups, and it posts one PR comment that it edits as its groups finish.

</AntiPattern.Fix>

</AntiPattern>

## Where the PR tier stops helping

Tiers add moving parts, and some suites don't need them. If the full suite already fits your time budget, run it on every PR behind the summary job and skip the tiers. A selected tier would only add a second, weaker gate.

Whenever you select, keep the merge tier mandatory. A selected tier can miss tests, so a PR that passed it can still fail in the queue. That failure is the design working, not noise.

Tiers also need upkeep. To tag a test, you either provide an additional details object when declaring the test, or add an `@` token to the test title. Review the tagged set on a schedule, the same way you review who owns the required check.

## Rolling it out

Change one thing at a time, in this order, so each step is something the team trusts before the next one lands.

1. Make the full suite, behind a summary job, the single required check, and add a concurrency group with `cancel-in-progress: true` so superseded PR runs that are still running get cancelled.
2. Introduce the selected PR tier, `--only-changed` or `@smoke`, and move the full suite to the merge queue or to pushes to `main`.
3. Add shards last, sized to the budget, once the blob reports merge into one HTML report.

Only the wiring here is specific to GitHub Actions. Sharding is a Playwright command-line flag, and Playwright's CI guide runs the same `--shard` flag on CircleCI by adding 1 to `CIRCLE_NODE_INDEX`.

Step one alone fixes the trust problem: one required check that goes red only when the run does. Step three's shards split that check to speed up its runtime, and step two's `--only-changed` run gives PRs a faster feedback loop. Because `--only-changed` might miss tests, keep the full suite as the required check in the merge queue.

## Frequently asked questions


### How do I run Playwright tests on pull requests without slowing merges?

Split the triggers into two tiers: `pull_request` runs a selected subset. In a merge queue, the `merge_group` trigger runs the full suite; without a queue, a push to `main` runs it. The PR tier can skip tests only because the full run exists, so never drop the second tier, as [splitting PR and merge runs](#splitting-pr-and-merge-runs) shows.


### How do I use --only-changed on a GitHub Actions pull request?

Always pass the base branch, `--only-changed=origin/$GITHUB_BASE_REF`, and check out with `fetch-depth: 0`.


### Is --only-changed enough to gate a merge?

No. `--only-changed` is a heuristic that might miss tests, and the docs say to always run the full test suite after the preliminary run. Put that full run in the merge tier, as [where the full run goes](#where-the-full-run-goes) explains.


### How do I tag Playwright tests for a smoke run on PRs?

Tag a test with a details object when you declare it, or with an `@` token in its title, and run `npx playwright test --grep @smoke`. Every tag must start with `@`. Grep for the tag with the `@` included, because grepping for the bare word can also match project, file, describe and test names, as [the bare word anti-pattern](#anti-pattern-grepping-for-a-bare-word) shows.


### How do I merge Playwright shard reports in GitHub Actions?

Have each shard write a blob report, then merge the blobs into one HTML report in a separate job. The `merge-reports` job declares `needs: [playwright-tests]`, so it runs after the sharded job, downloads the blobs into one directory and builds one HTML report. The full workflow is in [merging shard reports](#merging-shard-reports).


### How many shards should I use for Playwright tests?

Start from a time budget: choose the wait you'll accept, divide the full suite's single-job runtime by it, and round up. That's your starting shard count, as [sizing shards to a budget](#sizing-shards-to-a-budget) describes.


### Which job should be the required check for sharded Playwright tests?

Add one summary job that uses `always()` with `needs` on the sharded job, and mark only that job as required.


### How should I handle flaky tests in a Playwright PR gate?

Keep retries, and fail the PR tier on flaky results with `failOnFlakyTests`, which makes the run exit with an error if any test is marked flaky. Leave it off in the merge tier. In the merge queue, a failed required check removes the PR from the queue and makes the queue rebuild the groups behind it, as [flaky tests in the gate](#flaky-tests-in-the-gate) explains.




---

## About the author

**Walter Galvão**, Software Engineer. Walter Galvão is a software engineer at Currents. He writes about Playwright in practice: debugging flaky tests, structuring suites for CI, and the engineering workflows that keep large test suites reliable.

LinkedIn: https://www.linkedin.com/in/waltergalvao/
