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
- GitHub Actions
- GitLab CI
- Bitbucket Pipelines
- CircleCI, Jenkins, Azure DevOps, …
- Options
- Snapshot naming
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:
- 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.
-
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
| Option | Effect |
|---|---|
--no-wait | Return 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-changes | Pending review does not fail the pipeline |
--concurrency <n> | Parallel image uploads (default 4) |
--json | Final result as JSON on stdout (progress goes to stderr) |
--post-status | Post 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.png → login/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.