Your local config and CI config should be different. Locally you want headed mode, fast feedback, maybe one browser. In CI you want headless, retries, multiple browsers, and specific reporters. Playwright makes this easy with process.env.CI.
import { defineConfig, devices } from '@playwright/test';
export default defineConfig({
testDir: './tests',
// CI: retry failed tests to handle flakiness
// Local: no retries, fix the test immediately
retries: process.env.CI ? 2 : 0,
// CI: use half the available CPUs
// Local: let Playwright decide
workers: process.env.CI ? '50%' : undefined,
// CI: forbid test.only -- fail the build if someone left it in
forbidOnly: !!process.env.CI,
// Reporter configuration
reporter: process.env.CI
? [
['html', { open: 'never' }],
['github'], // Annotates PR with failures
['list'], // Logs to CI console
]
: [['html', { open: 'on-failure' }]],
use: {
// Always headless in CI (default), headed locally for debugging
headless: process.env.CI ? true : false,
// Capture trace on first retry (CI) or retain on failure (local)
trace: process.env.CI ? 'on-first-retry' : 'retain-on-failure',
// Screenshot only on failure to save storage
screenshot: 'only-on-failure',
// Shorter timeout in CI -- fail fast
actionTimeout: process.env.CI ? 10_000 : 30_000,
// Video only in CI on first retry
video: process.env.CI ? 'on-first-retry' : 'off',
},
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
],
});| Setting | CI Value | Local Value | Why |
|---|---|---|---|
| retries | 2 | 0 | CI has more infra flakiness, retries prevent false failures |
| workers | 50% | undefined | Prevents CI runner from running out of memory |
| forbidOnly | true | false | Fails build if someone left test.only in code |
| trace | on-first-retry | retain-on-failure | Saves storage, captures only when needed |
| screenshot | only-on-failure | only-on-failure | Same -- only capture evidence of failures |
| reporter | html + github + list | html | GitHub reporter annotates PR, list logs to console |
| video | on-first-retry | off | Video is heavy -- only record during retry |
Do NOT set workers to 100% in CI. CI runners typically have 2 vCPUs. Running 100% means 2 workers, which is fine. But if your tests are memory-heavy, even 50% is better. GitHub Actions ubuntu-latest has 7 GB RAM. Exceeding it crashes the runner silently.
This one catches everyone at least once. You are debugging locally. You add test.only to focus on one test. You fix the bug. You commit. You push. CI runs only that one test. 200 other tests never run. A critical bug ships. forbidOnly: true in CI config prevents this entirely. The build fails immediately if test.only exists anywhere.
// Someone left this in and pushed it
test.only('should login with valid credentials', async ({ page }) => {
// This is the ONLY test that runs in CI
// 200 other tests are silently skipped
});The github reporter is special. It uses GitHub Actions workflow commands to annotate your pull request directly. When a test fails, you see the error right in the PR files tab. No need to download reports.
Combine reporters in CI. Use html for the full interactive report, github for PR annotations, and list for console output. They all work together. Each serves a different purpose.
Q: How do you configure Playwright differently for CI vs local development?
A: Use process.env.CI to branch configuration. In CI: set retries to 2 for flakiness, workers to 50% for memory safety, forbidOnly to true to catch test.only mistakes, trace to on-first-retry to save storage, and add the github reporter for PR annotations. Locally: no retries for fast feedback, headed mode for debugging, and html reporter that opens on failure. The key is one config file that adapts automatically based on the environment.
Key Point: Set forbidOnly: true in CI. It prevents the silent disaster of someone accidentally pushing test.only and skipping the entire test suite.
Key Point: One config file, two behaviors. Use process.env.CI to adapt retries, workers, reporters, and trace settings for CI vs local.