When to use: Running Playwright tests in GitLab pipelines on merge requests, merges to main, or scheduled pipelines.
# Key commands used in GitLab pipelines
npx playwright install --with-deps # install browsers + OS deps
npx playwright test --shard=1/4 # run 1 of 4 parallel shards
npx playwright merge-reports ./blob-report # merge shard results
npx playwright test --reporter=dot # minimal output for CI logsUse when: Any GitLab project with Playwright tests. This is the complete, recommended configuration.
# .gitlab-ci.yml
image: mcr.microsoft.com/playwright:v1.52.0-noble
stages:
- install
- test
- report
variables:
CI: "true"
npm_config_cache: "$CI_PROJECT_DIR/.npm"
# Cache node_modules and npm cache across pipelines
cache:
key:
files:
- package-lock.json
paths:
- .npm/
- node_modules/
install:
stage: install
script:
- npm ci
artifacts:
paths:
- node_modules/
expire_in: 1 hour
test:
stage: test
needs: [install]
script:
- npx playwright test
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 14 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHUse when: Test suite exceeds 10 minutes. Use GitLab's parallel keyword to split across jobs automatically.
Avoid when: Suite runs under 5 minutes.
# .gitlab-ci.yml
image: mcr.microsoft.com/playwright:v1.52.0-noble
stages:
- install
- test
- report
variables:
CI: "true"
npm_config_cache: "$CI_PROJECT_DIR/.npm"
cache:
key:
files:
- package-lock.json
paths:
- .npm/
- node_modules/
install:
stage: install
script:
- npm ci
artifacts:
paths:
- node_modules/
expire_in: 1 hour
test:
stage: test
needs: [install]
parallel: 4
script:
- npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
artifacts:
when: always
paths:
- blob-report/
expire_in: 1 hour
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
merge-report:
stage: report
needs: [test]
when: always
script:
- npx playwright merge-reports --reporter=html ./blob-report
artifacts:
when: always
paths:
- playwright-report/
expire_in: 14 daysConfig for sharded pipelines:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: process.env.CI
? [['blob'], ['dot']]
: [['html', { open: 'on-failure' }]],
});Use when: Tests need secrets (API keys, passwords) and should only run on merge requests or the default branch. Avoid when: Tests are fully self-contained with no external dependencies.
# .gitlab-ci.yml
image: mcr.microsoft.com/playwright:v1.52.0-noble
stages:
- test
variables:
CI: "true"
test:e2e:
stage: test
variables:
BASE_URL: $STAGING_URL
TEST_PASSWORD: $TEST_PASSWORD
API_KEY: $API_KEY
before_script:
- npm ci
script:
- npx playwright test
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 14 days
rules:
# Run on merge requests
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# Run on default branch pushes
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# Allow manual trigger
- when: manual
allow_failure: trueSetting variables in GitLab: Navigate to Settings > CI/CD > Variables and add:
STAGING_URL-- not masked, not protectedTEST_PASSWORD-- masked, protectedAPI_KEY-- masked, protected
Use when: Running Chromium on MRs and all browsers on the default branch. Avoid when: You only test one browser.
# .gitlab-ci.yml
image: mcr.microsoft.com/playwright:v1.52.0-noble
stages:
- install
- test
variables:
CI: "true"
install:
stage: install
script:
- npm ci
artifacts:
paths:
- node_modules/
expire_in: 1 hour
# Chromium only on merge requests (fast feedback)
test:chromium:
stage: test
needs: [install]
script:
- npx playwright test --project=chromium
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 14 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
# All browsers on default branch
test:all-browsers:
stage: test
needs: [install]
parallel:
matrix:
- PROJECT: [chromium, firefox, webkit]
script:
- npx playwright test --project=$PROJECT
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 14 days
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHUse when: Tests need the application running alongside Playwright, or you need custom system dependencies.
Avoid when: The official Playwright image plus webServer in config handles your use case.
# .gitlab-ci.yml
stages:
- test
test:e2e:
stage: test
image: mcr.microsoft.com/playwright:v1.52.0-noble
services:
- name: postgres:16-alpine
alias: db
- name: redis:7-alpine
alias: cache
variables:
CI: "true"
DATABASE_URL: "postgresql://postgres:postgres@db:5432/test"
REDIS_URL: "redis://cache:6379"
POSTGRES_PASSWORD: "postgres"
POSTGRES_DB: "test"
before_script:
- npm ci
- npx prisma db push
- npx prisma db seed
script:
- npx playwright test
artifacts:
when: always
paths:
- playwright-report/
- test-results/
expire_in: 14 days
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHUse when: Full regression is too slow for every MR. Run it on a schedule.
# .gitlab-ci.yml (add to existing config)
test:nightly:
stage: test
image: mcr.microsoft.com/playwright:v1.52.0-noble
before_script:
- npm ci
script:
- npx playwright test --grep @regression
artifacts:
when: always
paths:
- playwright-report/
expire_in: 30 days
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"Set up the schedule in CI/CD > Schedules: 0 3 * * 1-5 (3 AM UTC, weekdays).
| Scenario | Approach | Why |
|---|---|---|
| Simple project, < 5 min suite | Single test job using Playwright Docker image |
No sharding overhead; artifacts capture report |
| Suite > 10 min | parallel: N with --shard |
GitLab auto-assigns CI_NODE_INDEX/CI_NODE_TOTAL |
| Merge request fast feedback | Chromium only on MRs; all browsers on main | 3x fewer pipeline minutes on MRs |
| External services needed (DB, Redis) | services: keyword with Postgres/Redis images |
GitLab manages service lifecycle |
| Secrets for staging environment | GitLab CI/CD Variables (masked + protected) | Never hardcode secrets in .gitlab-ci.yml |
| Full nightly regression | Pipeline schedule (CI_PIPELINE_SOURCE == "schedule") |
Avoids blocking MR pipelines |
| Report browsing | artifacts: with paths: [playwright-report/] |
Browse directly in GitLab job artifacts UI |
| Anti-Pattern | Problem | Do This Instead |
|---|---|---|
| Not using the Playwright Docker image | Installing browsers every run adds 1-2 minutes | Use mcr.microsoft.com/playwright:v1.52.0-noble as base image |
artifacts: when: on_failure only |
No report when tests pass; can't verify results | Use when: always to capture reports regardless |
No expire_in on artifacts |
Artifacts accumulate and consume storage | Set expire_in: 14 days for reports, 1 hour for intermediate artifacts |
parallel: without fail-fast: false equivalent |
GitLab does not cancel siblings by default (good), but allow_failure: false means the pipeline fails fast |
Acceptable default behavior; no change needed |
Hardcoding CI_NODE_TOTAL in shard flag |
Breaks when you change parallel: value |
Use --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL |
Skipping needs: between stages |
Jobs wait for all previous stage jobs, not just their dependencies | Use needs: for precise dependency graphs |
Large cache: including node_modules/ without key |
Stale cache causes version conflicts | Key cache on package-lock.json hash |
Cause: Not using the Playwright Docker image, or using a version that doesn't match your @playwright/test version.
Fix: Match the Docker image tag to your Playwright version:
# Check your version
# npm ls @playwright/test -> @playwright/test@1.52.0
image: mcr.microsoft.com/playwright:v1.52.0-nobleCause: GitLab shared runners may have limited resources. Default timeouts too tight.
Fix: Reduce workers and increase timeouts:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
workers: process.env.CI ? 2 : undefined,
use: {
navigationTimeout: process.env.CI ? 30_000 : 15_000,
},
});Cause: Missing rules: configuration. Default GitLab behavior runs on every push.
Fix: Add explicit rules:
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCHCause: Using localhost instead of the service alias.
Fix: Use the service alias as hostname:
services:
- name: postgres:16-alpine
alias: db # <-- use "db" as hostname
variables:
DATABASE_URL: "postgresql://postgres:postgres@db:5432/test" # not localhostCause: Each shard job needs the blob reporter, not html. The merge step creates the HTML report.
Fix: Configure blob reporter for CI:
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
reporter: process.env.CI
? [['blob'], ['dot']]
: [['html', { open: 'on-failure' }]],
});- ci/ci-github-actions.md -- GitHub Actions equivalent
- ci/ci-other.md -- CircleCI, Azure DevOps, Jenkins
- ci/parallel-and-sharding.md -- sharding strategies
- ci/docker-and-containers.md -- Docker image details
- ci/reporting-and-artifacts.md -- reporter configuration