Difora in your CI pipeline

The difora CLI works on any CI system: it uploads a directory of PNG screenshots, waits for the comparison and exits 0 (passed or approved) or 1 (changes need review, rejected, error). Install nothing — npx difora is enough.

How it fits in

Branch and commit are detected automatically on the providers below; pass --branch/--commit or set DIFORA_BRANCH / DIFORA_COMMIT anywhere else. npx difora doctor prints what is detected and checks the connection.

Two ways to get the result onto the pull or merge request:

  1. Server-side (project settings → Repository status): Difora publishes a GitHub check run (via the Difora GitHub App), a GitLab commit status or a Bitbucket build status itself. GitLab and Bitbucket need an access token, which is stored encrypted.
  2. From the pipeline (difora upload … --post-status): the CLI posts the status with the pipeline's own credentials. Use this for self-hosted GitLab or Bitbucket servers the Difora service cannot reach, or when you would rather not hand Difora a token.

Store the project token from Settings → CI tokens as a secret named DIFORA_TOKEN.

GitHub Actions

jobs:
  visual:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      statuses: write   # only for --post-status
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npm ci && npx playwright test   # writes ./screenshots/*.png
      - run: npx difora upload ./screenshots --post-status
        env:
          DIFORA_TOKEN: ${{ secrets.DIFORA_TOKEN }}

On pull_request events the CLI reports the PR's head commit (not the merge commit), so the status lands on the PR. With the Difora GitHub App installed you can omit --post-status.

GitLab CI

visual:
  image: mcr.microsoft.com/playwright:v1.50.0-noble
  script:
    - npm ci && npx playwright test
    - npx difora upload ./screenshots --post-status
  variables:
    DIFORA_TOKEN: $DIFORA_TOKEN               # masked CI/CD variable
    DIFORA_STATUS_TOKEN: $DIFORA_STATUS_TOKEN # project access token, scope "api" (for --post-status)

Merge-request pipelines report CI_MERGE_REQUEST_SOURCE_BRANCH_NAME / …_SHA; branch pipelines CI_COMMIT_REF_NAME / CI_COMMIT_SHA. Server-side statuses: Repository status → GitLab, repository group/project, base URL for self-hosted instances (https, public host).

Bitbucket Pipelines

pipelines:
  pull-requests:
    '**':
      - step:
          image: mcr.microsoft.com/playwright:v1.50.0-noble
          script:
            - npm ci && npx playwright test
            - npx difora upload ./screenshots --post-status

Set DIFORA_TOKEN and, for --post-status, DIFORA_STATUS_TOKEN (repository access token with repository:write) as secured repository variables. Server-side: Repository status → Bitbucket Cloud, repository workspace/slug, the same token.

CircleCI, Jenkins, Azure DevOps, Buildkite, Drone, Travis

Detection works out of the box (CIRCLE_*, CHANGE_BRANCH/BRANCH_NAME/GIT_BRANCH, SYSTEM_PULLREQUEST_SOURCEBRANCH/BUILD_SOURCEBRANCH, BUILDKITE_*, DRONE_*, TRAVIS_*). These hosts have no status integration yet; the exit code gates the pipeline. Use --exit-zero-on-changes if the visual step should be advisory.

// Jenkinsfile
withCredentials([string(credentialsId: 'difora-token', variable: 'DIFORA_TOKEN')]) {
  sh 'npx difora upload ./screenshots'
}

Options worth knowing

OptionEffect
--no-waitReturn right after the upload (exit 0); the review link is printed
--timeout <s>How long to wait for the comparison (default 300; exit 3 when exceeded — the build continues on the server)
--exit-zero-on-changesPending review does not fail the pipeline
--concurrency <n>Parallel image uploads (default 4)
--jsonFinal result as JSON on stdout (progress goes to stderr)
--post-statusPost pending/final commit status with the pipeline's token

Exit codes: 0 passed or approved · 1 changes need review, rejected or error · 2 usage or upload error · 3 timed out waiting. Network errors, HTTP 429 and 5xx are retried up to five times with exponential back-off, honouring Retry-After. Uploads run with a 120 s timeout per image.

Snapshot naming

Snapshot names are the PNG paths relative to the uploaded directory without the extension: login/desktop.pnglogin/desktop. Names are case-sensitive. Put viewport or browser into the file name (home@1280.png, home-firefox.png) — they are not part of the identity otherwise. Symbolic links are skipped. Images above 30 million pixels are refused.