@devant-net/cli-uploader
v0.7.2
Published
Universal results uploader for Devant. JUnit XML from any test runner, SARIF from any code scanner.
Maintainers
Readme
@devant-net/cli-uploader
Universal results uploader for Devant. JUnit XML from any test runner, SARIF from any code scanner.
Works with any runner that writes standard JUnit XML: Vitest, Jest, Bun test, pytest, Go test, JUnit/Java, Mocha, Cucumber, Karma.
Playwright, Cypress and Maestro have their own reporters. Use those instead. They record per-step and per-attempt detail that JUnit XML cannot carry.
Install
bun add -D @devant-net/cli-uploader
# or
npm i -D @devant-net/cli-uploaderUse
Run your tests, then upload the XML.
# Vitest
vitest run --reporter=junit --outputFile=junit.xml
devant-test upload --junit junit.xml --framework vitest
# Bun test
bun test --reporter=junit --reporter-outfile=junit.xml
devant-test upload --junit junit.xml --framework bun-test
# Jest
jest --reporters=jest-junit
devant-test upload --junit junit.xml --framework jest
# pytest
pytest --junit-xml=junit.xml
devant-test upload --junit junit.xml --framework pytest
# Go (via gotestsum)
gotestsum --junitfile=junit.xml -- ./...
devant-test upload --junit junit.xml --framework go-test
# Mocha (via mocha-junit-reporter)
mocha --reporter mocha-junit-reporter
devant-test upload --junit test-results.xml --framework mochaSharded CI writes one XML per shard. Pass the directory instead. Every *.xml
under it is parsed and merged into one run:
devant-test upload --junit test-results/ --framework vitestScan results (SARIF)
Use --sarif instead of --junit. Most scanners write SARIF with one flag:
# Semgrep
semgrep --config auto --sarif -o scan.sarif
devant-test upload --sarif scan.sarif --framework semgrep
# Bandit (Python). Needs the extra. Without it bandit hides sarif from -f
# instead of telling you what is missing.
pip install "bandit[sarif]"
bandit -r . -f sarif -o scan.sarif
devant-test upload --sarif scan.sarif --framework bandit
# ruff (Python)
ruff check --output-format sarif > scan.sarif
devant-test upload --sarif scan.sarif --framework ruff
# gitleaks (secrets)
gitleaks detect --report-format sarif --report-path scan.sarif
devant-test upload --sarif scan.sarif --framework gitleaks
# gosec (Go)
gosec -fmt sarif -out scan.sarif ./...
devant-test upload --sarif scan.sarif --framework gosec
# ...and anything else that writes SARIF: CodeQL, Trivy, Checkov, Snyk, Grype.Use SARIF, not a scanner's JUnit export. JUnit has no field for severity, rule id or CWE tag, so the scanner drops them and your findings report comes out empty.
A scan with no findings prints no findings and still records the run, so a
clean build keeps its place on the trend line.
GitHub Actions
- uses: devant-net/upload-results-action@v1
if: always()
with:
junit: junit.xml
framework: vitest
api-url: ${{ vars.DEVANT_API_URL }}
token: ${{ secrets.DEVANT_TOKEN }}
project-id: ${{ vars.DEVANT_PROJECT_ID }}Keep the if: always(). Without it the step is skipped when tests fail, so the
failing run never reaches Devant.
Scans work the same way with sarif: instead of junit::
- run: semgrep --config auto --sarif -o scan.sarif
- uses: devant-net/upload-results-action@v1
if: always()
with:
sarif: scan.sarif
framework: semgrep
api-url: ${{ vars.DEVANT_API_URL }}
token: ${{ secrets.DEVANT_TOKEN }}
project-id: ${{ vars.DEVANT_PROJECT_ID }}The action outputs run-url and run-id, and writes the run link into the job
summary.
Connecting to Devant
Same env-var contract as the other devant-net reporters:
| Var | Default | Notes |
|-------------------|--------------------------|------------------------------------------------|
| DEVANT_API_URL | required | Your tenant's URL. |
| DEVANT_TOKEN | required | CI token (dc_…) from Settings → CI/CD. |
| DEVANT_PROJECT_ID | required | Numeric project id. |
| DEVANT_RUN_NAME | PR title on CI, else <framework> — <ISO date> | Display name on the run. |
| DEVANT_RUN_ID | unset | If set, skip create/complete (orchestration). |
…or pass them explicitly via --api-url, --api-token, --project-id.
How tests bind to test cases
Each <testcase> resolves to a Devant test case in this order:
- A
devq-keyproperty, which overrides everything else. - A
testCaseIdorxray-test-keyproperty, the names other tools write. Used only when the value looks likeABC-123. - An
@KEYtoken in the name, classname, or any property value. - Otherwise a new case is created and given a key.
Put the key in the test source and later runs reuse the same case, including after a rename.
Suites are part of a case's identity, since a case is unique per project, suite and name. A row that arrives without its suite lands in the "no suite" bucket and creates a second case under the same name. You get a duplicated list rather than an error, so check before switching a project over:
devant-test upload --junit junit.xml --dry-runThat replays the same resolution against your existing cases and reports what it would create, without writing anything or starting a run.
[devant] 0 cases would be created. Safe to upload.Embedding the key in source
// Vitest / Jest / Bun test
it("adds positive numbers @DEF-XYZ9", () => { /* ... */ });# pytest
def test_add_positive_numbers():
"""@DEF-XYZ9"""
...Or a property, if your runner supports one: pytest-junit markers, or
jest-junit properties.
Status mapping
Standard JUnit semantics:
| In XML | Devant status |
|-----------------------|---------------|
| <skipped/> | skipped |
| <failure> or <error> | fail |
| (none of the above) | pass |
Non-standard status="..." attributes (e.g. Maestro's
SUCCESS/WARNING/ERROR enum) are ignored here. Use
@devant-net/maestro-reporter if you need that mapping.
Artifact attachment
Pure JUnit has no per-test artifact convention, but most CI setups dump
screenshots / logs / coverage HTML into a sibling directory. Pass it with
--artifacts-dir and the uploader best-effort matches each file's
basename against the test's name / classname:
devant-test upload \
--junit junit.xml \
--artifacts-dir test-results/screenshots \
--framework jestFiles are attached to the matching test's first attempt. Mime types are
auto-detected from the extension (.png, .mp4, .json, .log,
.html, .xml, ...). Path-safety: the uploader refuses any file whose
realpath escapes --artifacts-dir.
Code coverage
Pass --coverage <lcov.info> to attach a line/branch/function summary to
the run. LCOV is what Go (gcov2lcov), jest, nyc, c8 and coverage.py
all emit:
devant-test upload \
--junit junit.xml \
--coverage coverage/lcov.info \
--framework jestLCOV carries no statement metric, so statements_pct is left null. A
missing or empty file logs a warning and is skipped. It never fails an
otherwise-successful upload.
What gets sent
| JUnit element | Devant call |
|---------------------|----------------------------------------|
| <testsuite> | (informational, folded into one run) |
| <testcase> | POST /v1/runs/:id/results + 1 attempt |
| <failure> / <error> | error_message + error_stack |
| <skipped/> | result status = skipped |
| <system-out> / <system-err> | stdout / stderr on the attempt |
| <property> | scanned for case-key bindings |
| <testcase time> | duration_ms on the result + attempt |
| <testcase timestamp> | started_at on the attempt + run window |
Run-level started_at and duration_ms are computed from the JUnit
timestamps when present, else by walking back from "now" using the sum
of <testcase time> values, so the dashboard always shows a coherent
run window even when the framework's XML omits per-test timestamps.
Limitations
- No retries. Standard JUnit emits one
<testcase>per logical test (most runners merge retries before writing the XML). If yours doesn't, the duplicates each get their owntest_resultrow. - No step trees. JUnit XML doesn't carry sub-step data; the result has one attempt and zero steps. Frameworks that expose richer data through their reporter API have native devant-net packages.
- Approximate artifact match.
--artifacts-dirmatches by filename-substring. False positives are possible if test names share short common substrings; prefer to put framework-specific reporters in front of the JUnit uploader when artifact accuracy matters.
