# CI: CircleCI, Azure DevOps, and Jenkins

> **When to use**: Running Playwright tests in CI platforms other than GitHub Actions or GitLab. Each section provides a production-ready config you can copy and adapt.

## Quick Reference

```bash
# Common across all CI platforms
npx playwright install --with-deps    # install browsers + OS deps
npx playwright test --shard=1/4       # shard for parallelism
npx playwright merge-reports ./blob-report  # merge shard results
npx playwright test --reporter=dot,html     # multiple reporters
```

## Patterns

### Pattern 1: CircleCI

**Use when**: Your project runs on CircleCI.

#### Basic Pipeline

```yaml
# .circleci/config.yml
version: 2.1

executors:
  playwright:
    docker:
      - image: mcr.microsoft.com/playwright:v1.52.0-noble
    working_directory: ~/project

jobs:
  install:
    executor: playwright
    steps:
      - checkout
      - restore_cache:
          keys:
            - npm-deps-{{ checksum "package-lock.json" }}
      - run: npm ci
      - save_cache:
          key: npm-deps-{{ checksum "package-lock.json" }}
          paths:
            - node_modules
      - persist_to_workspace:
          root: .
          paths:
            - node_modules

  test:
    executor: playwright
    parallelism: 4
    steps:
      - checkout
      - attach_workspace:
          at: .
      - run:
          name: Run Playwright tests
          command: |
            npx playwright test --shard=$((CIRCLE_NODE_INDEX + 1))/$CIRCLE_NODE_TOTAL
      - store_artifacts:
          path: playwright-report
          destination: playwright-report
      - store_artifacts:
          path: test-results
          destination: test-results
      - store_test_results:
          path: test-results/junit.xml

workflows:
  test:
    jobs:
      - install
      - test:
          requires:
            - install
```

**Config for CircleCI JUnit integration:**

```ts
// playwright.config.ts
import { defineConfig } from "@playwright/test"

export default defineConfig({
  reporter: process.env.CI
    ? [["dot"], ["html", { open: "never" }], ["junit", { outputFile: "test-results/junit.xml" }]]
    : [["html", { open: "on-failure" }]],
})
```

#### CircleCI with Orbs (Simplified)

```yaml
# .circleci/config.yml
version: 2.1

orbs:
  node: circleci/node@6.1

executors:
  playwright:
    docker:
      - image: mcr.microsoft.com/playwright:v1.52.0-noble

jobs:
  e2e:
    executor: playwright
    parallelism: 4
    steps:
      - checkout
      - node/install-packages
      - run:
          name: Run tests
          command: npx playwright test --shard=$((CIRCLE_NODE_INDEX + 1))/$CIRCLE_NODE_TOTAL
      - store_artifacts:
          path: playwright-report
      - store_test_results:
          path: test-results/junit.xml

workflows:
  main:
    jobs:
      - e2e
```

---

### Pattern 2: Azure DevOps

**Use when**: Your project runs on Azure DevOps Pipelines.

#### Basic Pipeline

```yaml
# azure-pipelines.yml
trigger:
  branches:
    include:
      - main

pr:
  branches:
    include:
      - main

pool:
  vmImage: "ubuntu-latest"

variables:
  CI: "true"
  npm_config_cache: $(Pipeline.Workspace)/.npm

steps:
  - task: NodeTool@0
    inputs:
      versionSpec: "20.x"
    displayName: "Install Node.js"

  - task: Cache@2
    inputs:
      key: 'npm | "$(Agent.OS)" | package-lock.json'
      restoreKeys: |
        npm | "$(Agent.OS)"
      path: $(npm_config_cache)
    displayName: "Cache npm"

  - script: npm ci
    displayName: "Install dependencies"

  - script: npx playwright install --with-deps
    displayName: "Install Playwright browsers"

  - script: npx playwright test
    displayName: "Run Playwright tests"

  - task: PublishTestResults@2
    condition: always()
    inputs:
      testResultsFormat: "JUnit"
      testResultsFiles: "test-results/junit.xml"
      mergeTestResults: true
      testRunTitle: "Playwright Tests"
    displayName: "Publish test results"

  - task: PublishPipelineArtifact@1
    condition: always()
    inputs:
      targetPath: playwright-report
      artifact: playwright-report
      publishLocation: "pipeline"
    displayName: "Upload report"
```

**Config for Azure DevOps JUnit integration:**

```ts
// playwright.config.ts
import { defineConfig } from "@playwright/test"

export default defineConfig({
  reporter: process.env.CI
    ? [["dot"], ["html", { open: "never" }], ["junit", { outputFile: "test-results/junit.xml" }]]
    : [["html", { open: "on-failure" }]],
})
```

#### Azure DevOps with Sharding

```yaml
# azure-pipelines.yml
trigger:
  branches:
    include:
      - main

pr:
  branches:
    include:
      - main

variables:
  CI: "true"

stages:
  - stage: Test
    jobs:
      - job: Playwright
        pool:
          vmImage: "ubuntu-latest"
        strategy:
          matrix:
            shard1:
              SHARD: "1/4"
            shard2:
              SHARD: "2/4"
            shard3:
              SHARD: "3/4"
            shard4:
              SHARD: "4/4"
        steps:
          - task: NodeTool@0
            inputs:
              versionSpec: "20.x"

          - script: npm ci
            displayName: "Install dependencies"

          - script: npx playwright install --with-deps
            displayName: "Install browsers"

          - script: npx playwright test --shard=$(SHARD)
            displayName: "Run tests (shard $(SHARD))"

          - task: PublishPipelineArtifact@1
            condition: always()
            inputs:
              targetPath: blob-report
              artifact: blob-report-$(System.JobPositionInPhase)
            displayName: "Upload blob report"

  - stage: Report
    dependsOn: Test
    condition: always()
    jobs:
      - job: MergeReports
        pool:
          vmImage: "ubuntu-latest"
        steps:
          - task: NodeTool@0
            inputs:
              versionSpec: "20.x"

          - script: npm ci
            displayName: "Install dependencies"

          - task: DownloadPipelineArtifact@2
            inputs:
              patterns: "blob-report-*/**"
              path: all-blob-reports
            displayName: "Download all blob reports"

          - script: npx playwright merge-reports --reporter=html ./all-blob-reports
            displayName: "Merge reports"

          - task: PublishPipelineArtifact@1
            inputs:
              targetPath: playwright-report
              artifact: playwright-report
            displayName: "Upload merged report"
```

---

### Pattern 3: Jenkins

**Use when**: Your project runs on Jenkins.

#### Jenkinsfile (Declarative Pipeline)

```groovy
// Jenkinsfile
pipeline {
    agent {
        docker {
            image 'mcr.microsoft.com/playwright:v1.52.0-noble'
            args '-u root'  // Playwright needs root in container
        }
    }

    environment {
        CI = 'true'
        HOME = '/root'
        npm_config_cache = "${WORKSPACE}/.npm"
    }

    options {
        timeout(time: 30, unit: 'MINUTES')
        disableConcurrentBuilds()
    }

    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Test') {
            steps {
                sh 'npx playwright test'
            }
            post {
                always {
                    // Publish JUnit results
                    junit allowEmptyResults: true,
                         testResults: 'test-results/junit.xml'

                    // Archive HTML report
                    archiveArtifacts artifacts: 'playwright-report/**',
                                     allowEmptyArchive: true

                    // Archive traces on failure
                    archiveArtifacts artifacts: 'test-results/**',
                                     allowEmptyArchive: true
                }
            }
        }
    }

    post {
        failure {
            // Notify on failure (Slack, email, etc.)
            echo 'Playwright tests failed!'
        }
        cleanup {
            cleanWs()
        }
    }
}
```

**Config for Jenkins JUnit integration:**

```ts
// playwright.config.ts
import { defineConfig } from "@playwright/test"

export default defineConfig({
  reporter: process.env.CI
    ? [["dot"], ["html", { open: "never" }], ["junit", { outputFile: "test-results/junit.xml" }]]
    : [["html", { open: "on-failure" }]],
})
```

#### Jenkins with Parallel Stages

```groovy
// Jenkinsfile (sharded)
pipeline {
    agent none

    environment {
        CI = 'true'
        HOME = '/root'
    }

    options {
        timeout(time: 30, unit: 'MINUTES')
    }

    stages {
        stage('Test') {
            parallel {
                stage('Shard 1') {
                    agent {
                        docker {
                            image 'mcr.microsoft.com/playwright:v1.52.0-noble'
                            args '-u root'
                        }
                    }
                    steps {
                        sh 'npm ci'
                        sh 'npx playwright test --shard=1/4'
                    }
                    post {
                        always {
                            archiveArtifacts artifacts: 'blob-report/**',
                                             allowEmptyArchive: true
                        }
                    }
                }
                stage('Shard 2') {
                    agent {
                        docker {
                            image 'mcr.microsoft.com/playwright:v1.52.0-noble'
                            args '-u root'
                        }
                    }
                    steps {
                        sh 'npm ci'
                        sh 'npx playwright test --shard=2/4'
                    }
                    post {
                        always {
                            archiveArtifacts artifacts: 'blob-report/**',
                                             allowEmptyArchive: true
                        }
                    }
                }
                stage('Shard 3') {
                    agent {
                        docker {
                            image 'mcr.microsoft.com/playwright:v1.52.0-noble'
                            args '-u root'
                        }
                    }
                    steps {
                        sh 'npm ci'
                        sh 'npx playwright test --shard=3/4'
                    }
                    post {
                        always {
                            archiveArtifacts artifacts: 'blob-report/**',
                                             allowEmptyArchive: true
                        }
                    }
                }
                stage('Shard 4') {
                    agent {
                        docker {
                            image 'mcr.microsoft.com/playwright:v1.52.0-noble'
                            args '-u root'
                        }
                    }
                    steps {
                        sh 'npm ci'
                        sh 'npx playwright test --shard=4/4'
                    }
                    post {
                        always {
                            archiveArtifacts artifacts: 'blob-report/**',
                                             allowEmptyArchive: true
                        }
                    }
                }
            }
        }
    }
}
```

## Decision Guide

| CI Platform  | Docker Image Support             | Native Parallelism                        | Artifact Browsing     | JUnit Integration      |
| ------------ | -------------------------------- | ----------------------------------------- | --------------------- | ---------------------- |
| CircleCI     | First-class (`docker:` executor) | `parallelism: N` with `CIRCLE_NODE_INDEX` | Via artifacts tab     | `store_test_results`   |
| Azure DevOps | Via `vmImage` or container jobs  | `strategy.matrix`                         | Pipeline Artifacts UI | `PublishTestResults@2` |
| Jenkins      | Docker Pipeline plugin           | `parallel` stages                         | Archived Artifacts    | `junit` step           |

| Scenario       | CircleCI                                        | Azure DevOps                         | Jenkins                            |
| -------------- | ----------------------------------------------- | ------------------------------------ | ---------------------------------- |
| Shard variable | `$((CIRCLE_NODE_INDEX + 1))/$CIRCLE_NODE_TOTAL` | Define in matrix: `SHARD: '1/4'`     | Hardcode per parallel stage        |
| Cache key      | `checksum "package-lock.json"`                  | `Cache@2` with key template          | `stash`/`unstash` or shared volume |
| Secrets        | Context + environment variables                 | Variable groups + pipeline variables | Credentials plugin                 |
| Report upload  | `store_artifacts`                               | `PublishPipelineArtifact@1`          | `archiveArtifacts`                 |

## Anti-Patterns

| Anti-Pattern                                                   | Problem                                             | Do This Instead                                                |
| -------------------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------- |
| Installing browsers on bare metal without `--with-deps`        | Missing OS libs cause launch failures               | Use Playwright Docker image or `--with-deps` flag              |
| No JUnit reporter                                              | CI platform can't display test results natively     | Add `['junit', { outputFile: 'test-results/junit.xml' }]`      |
| Unlimited job timeout                                          | Hung tests run indefinitely, consuming CI resources | Set explicit timeout (20-30 min)                               |
| No artifact upload on success                                  | Can't verify results when tests pass                | Always upload reports (`condition: always()` / `when: always`) |
| Running browsers as non-root in container without setup        | Permission errors on browser binaries               | Run as root or configure proper permissions                    |
| Hardcoding shard count in config instead of using CI variables | Must update two places when changing parallelism    | Use CI-native variables (`CI_NODE_TOTAL`, `CIRCLE_NODE_TOTAL`) |

## Troubleshooting

### CircleCI: "Error: browserType.launch: Executable doesn't exist"

**Cause**: Not using the Playwright Docker image, or image version doesn't match `@playwright/test` version.

**Fix**: Match image tag to your Playwright version:

```yaml
docker:
  - image: mcr.microsoft.com/playwright:v1.52.0-noble # match package.json version
```

### Azure DevOps: Test results not showing in Tests tab

**Cause**: JUnit reporter not configured, or `PublishTestResults@2` task missing.

**Fix**: Add both the reporter and the publish task:

```ts
// playwright.config.ts
reporter: [['junit', { outputFile: 'test-results/junit.xml' }]],
```

```yaml
- task: PublishTestResults@2
  condition: always()
  inputs:
    testResultsFormat: "JUnit"
    testResultsFiles: "test-results/junit.xml"
```

### Jenkins: "Browser closed unexpectedly" in Docker agent

**Cause**: Running as non-root user in container. Chromium's sandbox needs root or `--no-sandbox`.

**Fix**: Run as root in the Docker agent:

```groovy
agent {
    docker {
        image 'mcr.microsoft.com/playwright:v1.52.0-noble'
        args '-u root'
    }
}
environment {
    HOME = '/root'
}
```

### All platforms: Shard index off by one

**Cause**: CircleCI's `CIRCLE_NODE_INDEX` is 0-based, but Playwright's `--shard` is 1-based.

**Fix**: Add 1 to the index for CircleCI:

```yaml
# CircleCI
command: npx playwright test --shard=$((CIRCLE_NODE_INDEX + 1))/$CIRCLE_NODE_TOTAL

# GitLab (already 1-based)
command: npx playwright test --shard=$CI_NODE_INDEX/$CI_NODE_TOTAL
```

## Related

- [ci/ci-github-actions.md](ci-github-actions.md) -- GitHub Actions configuration
- [ci/ci-gitlab.md](ci-gitlab.md) -- GitLab CI configuration
- [ci/parallel-and-sharding.md](parallel-and-sharding.md) -- sharding strategies
- [ci/docker-and-containers.md](docker-and-containers.md) -- Docker image details
- [ci/reporting-and-artifacts.md](reporting-and-artifacts.md) -- reporter configuration for CI
