@devant-net/junit-uploader
v0.6.1
Published
Generic JUnit XML uploader for devq-cloud — works with Vitest, Jest, Bun test, pytest, Go test, JUnit/Java, and anything else that emits standard JUnit XML.
Maintainers
Readme
@devant-net/junit-uploader
Generic JUnit XML uploader for Devant.
Works with any test runner that emits standard JUnit XML — Vitest, Jest,
Bun test, pytest, Go test, JUnit/Java, Mocha, Cucumber, Karma — anything
the JUnit-compatible ecosystem touches. Framework-specific reporters
(@devant-net/playwright-reporter, @devant-net/cypress-reporter,
@devant-net/maestro-reporter) handle richer per-step / per-attempt /
per-artifact data; this is the long-tail catch-all.
Install
bun add -D @devant-net/junit-uploader
# or
npm i -D @devant-net/junit-uploaderUse
After your test command emits a JUnit XML, pipe it to Devant:
# 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 mochaCI sharding writes one XML per shard — pass the directory and the uploader
parses every *.xml recursively, merging all <testsuite>s into one run:
devant-test upload --junit test-results/ --framework vitestConnecting 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
The uploader resolves each <testcase> against a Devant test_case row in
this order:
<property name="devq-key">DEF-AB12</property>— explicit override.<property name="testCaseId">DEF-AB12</property>orxray-test-key— common Xray / test-id conventions; only used if the value matches[A-Z]+-[A-Z0-9]+.@KEYtoken anywhere inname,classname, or property values.- Auto-create. The uploader prints the new key:
[devq] minted DEF-XYZ9 for "src/math.test.ts > add > adds positive numbers" — embed '@DEF-XYZ9' in the test name to bind it
Once a key is bound in source, future runs (and renames) reuse the same case.
Resolution happens server-side, inside the results batch — the uploader sends each row's name, suite chain and key rather than looking cases up one request at a time. That is the difference between ~12 requests and ~1,965 on a 2,000-test suite, and the requests are what the upload time is made of.
The suite chain matters for correctness, not just grouping. A case is unique per
(project, suite, name), so a row that arrives without its suite resolves in the
"no suite" bucket and creates a second case with the same name — no error, just
a duplicated list. Rehearse it before switching a project over:
devant-test upload --junit junit.xml --dry-runIt reads the project's existing cases, replays the same resolution order
locally, and reports what would be created without writing anything or creating
a run. 0 cases would be created on an unchanged suite means the upload will
land on the cases you already have.
Embedding the key in source
// Vitest / Jest / Bun test
it("adds positive numbers @DEF-XYZ9", () => { /* … */ });# pytest
def test_add_positive_numbers():
"""@DEF-XYZ9"""
...Or use a property if your runner supports it (pytest does via
pytest-junit markers; Jest does via 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.
