CI runner
Run exported scenarios and suites headless in CI with the command-line runner, the Docker image or the GitHub Action, with JSON, JUnit and HTML reports.
Last updated
The headless runner executes scenario and suite manifests exported from the desktop app, including recorded scenarios, and writes JSON, JUnit XML and HTML reports, from the command line, a Docker image or a GitHub Action.
Use the runner only on sites you own or are contracted to test. The full reference is in docs/CI-RUNNER.md.
Export a manifest
In the desktop app, open QA automation and use:
- Export for CI on a scenario. The manifest contains
scenario,profileand optionally amatrix(engines,devices,targets,concurrency,retries). - Export suite for CI on a suite. It includes the suite's scenarios, profiles and workspace environments.
- Export approved baselines for CI for visual comparisons, which writes a
.qavbpack. Approve baselines on the same operating system and browser versions the CI uses (for example in the Linux app with the same bundled engines); otherwise comparisons fail. How baselines are reviewed and kept per browser and device is described in approved baselines in CI.
Manifests are executable configuration. Never commit real form data or credentials in them; URLs with embedded usernames or passwords are rejected.
Command line
From a clone of the repository:
npm ci
npm run build
npm run browsers:install
npm run qa -- --config /path/to/scenario.json --output qa-results| Flag | Meaning |
|---|---|
--config <file> |
Scenario or suite manifest (required) |
--output <directory> |
Report directory (default qa-results) |
--environment <name> |
Environment exported in a suite manifest |
--baselines <pack> |
Approved screenshot pack (.qavb) |
--healing <mode> |
Override every scenario's self-healing mode: off, warn or fail |
Examples:
npm run qa -- --config suite.json --environment "Staging" --output qa-results
npm run qa -- --config scenario.json --baselines approved.qavb --output qa-resultsThe runner uses a temporary in-memory database and fresh browser contexts. It never reads the desktop app's credential vault, loads no .env file, and does not download browsers itself. Imported baselines are temporary copies; missing or changed comparisons are test failures. Baselines are never approved by the runner.
Outputs and exit codes
The output directory gets results.json, results.xml (JUnit), results.html and an artifacts/ directory with screenshots and diffs.
| Code | Meaning |
|---|---|
| 0 | All cases passed |
| 1 | At least one case failed (assertion, navigation, visual change, missing baseline) |
| 2 | Invalid configuration: manifest, flags, environment name or proxy variables rejected |
| 130 | Cancelled (SIGINT or SIGTERM); active work stops and browsers close |
Configuration errors name the failing field and the reason, for example QA configuration error: Invalid manifest: matrix.engines.0: Invalid option: …. Values from the manifest or the environment are never echoed.
Proxy credentials
Leave all proxy variables unset for a direct (unproxied) run. For profiles that use a built-in provider, pass credentials through environment variables, set from your CI secret store:
| Variable | Meaning |
|---|---|
QA_PROVIDER |
Provider ID, for example dataimpulse. Must match the provider named by the manifest's proxied profiles |
QA_PROVIDER_PRODUCT |
Product key, for example residential or mobile. Default: the product of the first proxied profile |
QA_PROVIDER_HOST, QA_PROVIDER_PORT |
Gateway; default: the provider's documented gateway |
QA_PROVIDER_USERNAME, QA_PROVIDER_PASSWORD |
Login (secret) |
QA_PROVIDER_EXTRA_<KEY> |
Provider-specific credential fields (secret ones are redacted like passwords) |
QA_PROXY_SERVER, QA_PROXY_USERNAME, QA_PROXY_PASSWORD |
One custom gateway for the whole run; overrides profile routing |
DATAIMPULSE_PROXY_HOST, DATAIMPULSE_PROXY_PORT, DATAIMPULSE_PROXY_USERNAME and DATAIMPULSE_PROXY_PASSWORD remain supported as an alias for QA_PROVIDER=dataimpulse; when any QA_PROVIDER* variable is set they are ignored with a warning. Incomplete variables, or credentials for another provider than the manifest uses, exit with code 2 naming the variables, never their values. Every credential variable is removed from the process environment before browsers launch.
Site access tokens are a desktop-only feature and are not available to the runner. Allowlist CI traffic on your site instead.
Docker image
The runner image ghcr.io/ubaidbinwaris/proxy-qa-runner is published for linux/amd64. Its only browsers are Chromium, Firefox and WebKit; it also contains Node 24, the runner and the MCP server. It is built on the official Playwright image, contains no credentials and runs as the non-root user pwuser. Installed vendor browsers (Chrome, Edge, Brave and others) are not included; use chromium, firefox or webkit.
docker run --rm --init --ipc=host \
--user "$(id -u):$(id -g)" -e HOME=/tmp \
-v "$PWD:/work" \
ghcr.io/ubaidbinwaris/proxy-qa-runner:1.5.2 \
--config /work/suite.json --environment Staging --output /work/qa-results--initreaps browser processes;--ipc=hostavoids Chromium running out of shared memory.--user "$(id -u):$(id -g)" -e HOME=/tmpmakes the reports belong to you.- Add
--network host(Linux) when the site under test runs on the host'slocalhost. - Paths in
results.jsonare container paths. Mount the directory at the same path on both sides (-v "$PWD:$PWD" -w "$PWD") to keep them valid on the host.
Pass proxy settings by name, so values never appear on the command line or in shell history:
export QA_PROVIDER_USERNAME=... QA_PROVIDER_PASSWORD=... # from your secret store
docker run --rm --init --ipc=host -e QA_PROVIDER -e QA_PROVIDER_HOST -e QA_PROVIDER_PORT \
-e QA_PROVIDER_USERNAME -e QA_PROVIDER_PASSWORD -v "$PWD:/work" \
ghcr.io/ubaidbinwaris/proxy-qa-runner:1.5.2 --config /work/suite.jsonPin the image to a release tag or digest. To build it locally, see docs/CI-RUNNER.md.
GitHub Action
To run browser tests in GitHub Actions, use the composite action UbaidBinWaris/proxy_browser/action. It runs the image on Linux runners with Docker (GitHub-hosted ubuntu-* runners qualify), uploads the report directory and fails the step when the exit code is not 0.
jobs:
qa:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: UbaidBinWaris/proxy_browser/action@v1.5.2
with:
config: qa/signup-suite.json
environment: Staging
env:
QA_PROVIDER_USERNAME: ${{ secrets.QA_PROVIDER_USERNAME }}
QA_PROVIDER_PASSWORD: ${{ secrets.QA_PROVIDER_PASSWORD }}| Input | Default | Description |
|---|---|---|
config |
(required) | Manifest path inside the workspace |
output |
qa-results |
Report directory inside the workspace |
environment |
none | Environment name from the manifest |
baselines |
none | .qavb pack inside the workspace |
healing |
none | off, warn or fail; passed only when set |
image |
ghcr.io/ubaidbinwaris/proxy-qa-runner:<release> |
Runner image; may be pinned by digest |
docker-network |
host |
Docker network; host lets scenarios reach services started earlier in the job |
upload-artifact |
true |
Upload the report directory |
artifact-name |
qa-results |
Make it unique if the action runs several times in one workflow run |
Outputs: exit-code (the runner's exit code; 125 means Docker could not start the container) and results-json (the absolute path of results.json). To decide in a later step, set continue-on-error: true and read steps.<id>.outputs.exit-code.
Credentials are never inputs. Put them in the step's env: from secrets.*. The action forwards every set variable in the QA_PROVIDER*, DATAIMPULSE_PROXY_* and QA_PROXY_* families by name and masks their values in the log. Paths outside the workspace are rejected.
A complete consumer workflow is in examples/ci/github-workflow.yml.
Security notes
- Secrets go only through environment variables. Never write them into manifests,
with:inputs, URLs or committed files. - Reports can contain screenshots of your forms and the test data you typed. Use synthetic data and keep artifact retention short.
- Do not run the action on
pull_request_targetwith untrusted code.
Try the sample
The repository's examples/ci/ folder contains a static form, a direct-connection scenario manifest and the consumer workflow:
python3 -m http.server 8080 --bind 127.0.0.1 --directory examples/ci/site &
docker run --rm --init --ipc=host --network host --user "$(id -u):$(id -g)" -e HOME=/tmp \
-v "$PWD:$PWD" -w "$PWD" ghcr.io/ubaidbinwaris/proxy-qa-runner:1.5.2 \
--config examples/ci/scenario.json --output qa-resultsExpected: passed: 1/1 cases, exit code 0, and qa-results/results.{json,xml,html}.
Common questions
How do I pass proxy credentials in CI?
Leave all proxy variables unset for a direct run. For profiles that use a built-in provider, pass the credentials as QA_PROVIDER* environment variables (or the older DATAIMPULSE_PROXY_* alias) from your CI secret store; QA_PROXY_* instead sets one custom gateway for the whole run. See Proxy credentials.