Upload test results to Qualflare from any CI pipeline or local machine. Supports 24 testing frameworks with automatic format detection.
Homebrew (macOS / Linux — recommended)
brew install qualflare/tap/qf
# Upgrade to the latest version
brew upgrade qualflare/tap/qfnpm (any OS, Node ≥ 16) — downloads the platform-native binary and puts qf on your PATH:
npm install -g @qualflare/cliDocker — run without installing anything, ideal for CI. Multi-arch (amd64 + arm64), published to both GitHub Container Registry and Docker Hub:
# GitHub Container Registry (no login needed to pull)
docker run --rm ghcr.io/qualflare/qf version
# Also on Docker Hub — pin a version for reproducible CI
docker run --rm docker.io/qualflare/qf:0.1.10 versionTo collect results from a pipeline, mount your workspace and pass your token — see CI/CD for the full flow.
Binary download — grab the latest release for your platform from the Releases page and place qf on your PATH.
Build from source — requires Go 1.25+: git clone then make build. The binary lands in build/qf.
1. Get your API token
Open Project → Settings → Access Tokens in the Qualflare dashboard (/project/<slug>/settings/access-tokens/) and create a new token.
2. Log in
Save your token under a short local identifier (the project slug shown in the dashboard works well):
qf login myapp qf_your_token_hereThis writes credentials to ~/.config/qualflare/config.toml (0600). Run qf login --help for options.
3. Upload your first report
qf myapp collect path/to/results.xmlThe framework is detected automatically from the filename and file content. You're done.
# Auto-detect framework from filename / content
qf myapp collect results.xml
# Multiple files (glob or space-separated)
qf myapp collect test-results/*.xml
# Specify the framework explicitly
qf myapp collect report.json --format playwright
# Parse and preview locally without sending (no auth required)
qf myapp collect results.xml --dry-run --output json
# Upload videos and Playwright traces too (opt-in — see below)
qf myapp collect ./qualflare-results --upload-artifacts=video,traceRun qf myapp collect --help for the full flag list (--branch, --commit, --environment, --timeout, etc.).
collect <dir> uploads every report in the directory, which is what lets N sharded jobs write into
one place and merge into a single Launch. Reports carry a runId that every shard of one run shares
and different runs do not, so a file left over from an earlier run is recognised rather than merged:
$ qf myproj collect ./qualflare-results
ignored 1 file(s) from 1 earlier run(s) (--allow-mixed-runs to include them)
Processing 2 test result file(s)...
OK Test results collected successfully
The run that just finished is uploaded; older files stay on disk untouched. There is nothing to
clear between runs. Pass --allow-mixed-runs to merge every run in the directory into one Launch
instead.
Reports written by a reporter too old to stamp a runId carry no run identity, so they are always
included — a mixed-version directory never loses live results.
The report itself always uploads, and so do screenshots. A video or Playwright trace only uploads when you ask for it:
| flag | --upload-artifacts=video, =trace, or =video,trace |
| env | QF_UPLOAD_ARTIFACTS=video,trace |
| default | image — screenshots upload, video and traces do not |
| decline everything | --upload-artifacts=none, screenshots included |
Named kinds are added to the default, not swapped for it: --upload-artifacts=video uploads
video and screenshots. Turning screenshots off is what none is for, and none cannot be
combined with another kind — asking for both is rejected rather than silently resolved one way.
Screenshots differ from video for a reason. They are small, and they were always uploaded back when inlining into the request body was their only route, so making them opt-in would have silently stopped delivering them for everyone who never passed the flag.
A video is the largest thing in a report by an order of magnitude, and before v0.1.20 every one a
reporter wrote was uploaded with no way to decline. Nothing is dropped quietly: collect reports
what it skipped and the exact flag to include it.
skipped 2 video, 1 trace attachment(s): upload them with --upload-artifacts=trace,video
A skipped artifact is left out of the launch entirely rather than uploaded as an empty row, so a gated run shows no unopenable placeholders. An artifact that was asked for but failed to upload is different — it stays, so the failure is visible rather than hidden.
Traces additionally need an api-service that accepts application/zip on the attachment-upload
endpoint.
Validate that qf can parse your files — useful as a pre-upload gate:
qf myapp validate results.xmlAll read commands print JSON, so they pipe naturally to jq and work well with AI agents:
qf myapp suites list
qf myapp cases list --suite 5
qf myapp plans list
qf myapp launches list --milestone 3
qf myapp defects list --severity critical,highRun qf myapp <command> --help for pagination, filtering, and sorting options.
Add a single step after your test run. Store your token in a repository secret named QF_TOKEN:
- name: Upload test results
run: |
qf login ci "$QF_TOKEN" --force
qf ci collect test-results/*.xml
env:
QF_TOKEN: ${{ secrets.QF_TOKEN }}
QF_BRANCH: ${{ github.ref_name }}
QF_COMMIT: ${{ github.sha }}qf is a single static binary with no runtime dependencies. If you're using GitLab CI, CircleCI, Jenkins, or another platform, download the binary from Releases and run the same two commands. See the documentation for platform-specific snippets.
| Command | Description |
|---|---|
qf login <id> <token> |
Save credentials for a project identifier |
qf logout <id> |
Remove saved credentials |
qf projects |
List all saved identifiers |
qf list-formats |
List all supported test frameworks |
qf version |
Print version information |
qf <id> collect [files...] |
Parse and upload test results |
qf <id> validate [files...] |
Parse and validate without uploading |
qf <id> suites|cases|plans|launches|defects|clusters|milestones ... |
Read project data |
Run qf <command> --help for flags and examples.
| Category | Frameworks |
|---|---|
| Generic (JUnit-compatible) | JUnit, CTRF, qualflare-json (the Collect JSON written by every @qualflare/* reporter — playwright, cypress, cucumberjs, vitest, jest — for merging sharded-CI output) |
| Unit Testing | pytest, Go testing, Jest/Vitest, Mocha, RSpec, PHPUnit, TestNG |
| BDD | Cucumber, Karate |
| UI / E2E / Mobile | Playwright, Cypress, Selenium, TestCafe, Maestro, XCTest, Espresso, Detox, WebdriverIO, Appium, Flutter |
| API Testing | Newman (Postman), k6 |
| Security | OWASP ZAP, Trivy, Snyk, SonarQube |
Format is auto-detected from the filename and file content. Pass --format <name> to override. Run qf list-formats for all valid format names.
Any framework that emits standard JUnit XML — NUnit, MSTest, xUnit.net, Robot Framework, and others — can be uploaded using --format junit. Because all JUnit emitters share the same XML schema, use --format <name> (or a recognizable filename like maestro-results.xml) to disambiguate Maestro, XCTest, Espresso, and TestNG outputs from generic JUnit.
Flutter's test runner has no pluggable reporter, so qf reads its built-in JSON stream:
flutter test --file-reporter json:flutter-results.json
qf myapp collect flutter-results.json
# integration_test on a device: the stream never names the device
flutter test integration_test/ -d <device> --file-reporter json:flutter-results.json
qf myapp collect flutter-results.json --platform android # or iosWidget, unit and integration_test results are uploaded with their failure messages, per-attempt retries and print output.
Some frameworks write screenshots, videos and logs to a directory rather than
into the results file. --artifacts-dir attaches those to the matching cases:
# Detox: the artifacts root, or one <configuration>.<timestamp> run inside it
qf myapp collect jest-results.json --artifacts-dir ./artifactsScreenshots upload by default. Videos and device logs do not — pass
--upload-artifacts=video,trace for those. A video is the largest thing in a
report by an order of magnitude, and a device log from a real app is the
artifact most likely to carry customer data, so neither is a surprise by
default.
The directory is never guessed: without the flag, nothing is scanned.
Credentials are stored per identifier in $XDG_CONFIG_HOME/qualflare/config.toml (permissions 0600). Treat this file like an SSH key.
Key environment variables:
| Variable | Description |
|---|---|
QF_VERBOSE |
Set to true for debug request/response logging |
QF_BRANCH / QF_COMMIT |
Git context (auto-detected from common CI vars if unset) |
QF_ENVIRONMENT |
Environment uid to upload into. Overrides the environment recorded in a qualflare-json report; --environment overrides both. Falls back to development. |
Run qf --help for the full list of flags and environment variables.
Contributions are welcome — see CONTRIBUTING.md for dev setup, commit conventions, and the PR checklist.
Licensed under the Apache License 2.0.