@lubed/test-uploader
v0.6.0
Published
Upload test results (JUnit XML, Vitest/Jest/Go/Rust/Python/Ruby JSON) to Merged.
Readme
@lubed/test-uploader
Warning This package is not ready for public use yet. The package is public, but you will not be able to finish setting up test result uploads until the main app is available. We will do a proper launch soon.
Upload test results from any framework that emits JUnit XML or a native JSON reporter (Vitest, Jest, Go, Rust, Python, Ruby) to Merged. The CLI captures git and CI context, attaches it to the run, and posts the raw report to the API. Parsing happens server-side.
Install
npm install -D @lubed/test-uploader
# or
pnpm add -D @lubed/test-uploader
# or
yarn add -D @lubed/test-uploaderFor one-off use without installing:
npx -y @lubed/test-uploader upload --app <id>Non-JS users can skip the CLI and curl against the same endpoint directly (see the per-framework recipes in the platform docs).
Authentication
Set MERGED_API_KEY in your environment. Create the key in the dashboard under Settings → API keys. The key needs the testRun.create permission.
| Env var | Required | Default | Description |
| ---------------- | -------- | ----------------------- | -------------------------------------------------- |
| MERGED_API_KEY | yes | — | Bearer token used for the upload. |
| MERGED_API_URL | no | https://api.lube.work | API base URL. Override for staging or self-hosted. |
CI context is auto-detected from the standard env vars (CI, GITHUB_ACTIONS, BUILDKITE, CIRCLECI, GITLAB_CI, TRAVIS). Commit SHA and branch are pulled from GITHUB_SHA / GITHUB_REF_NAME / BUILDKITE_COMMIT / BUILDKITE_BRANCH / CI_COMMIT_SHA / CI_COMMIT_REF_NAME when present, falling back to git rev-parse.
Usage
merged-tests upload --app <applicationId> [--file <path>] [--format <format>] [--env <label>] [--api-url <url>]Flags
| Flag | Required | Default | Description |
| ------------------- | -------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| --app <id> | yes | — | Application ID to tag the run to. |
| --file <path> | no | auto-detected | Path to the report file. |
| --format <format> | no | auto-detected from filename | One of: junit_xml, vitest_json, jest_json, gotest_json, gotestsum_json, nextest_json, pytest_json, rspec_json. |
| --env <label> | no | none | Optional environment label for filtering. |
| --service-id <id> | no | none | Stable id of the contributing service within the application. Preferred over --service-name when known. |
| --service-name <n>| no | none | Contributing service name (e.g. api-primary-v2). Matched or auto-created within the application. |
| --api-url <url> | no | $MERGED_API_URL or https://api.lube.work | API base URL. |
| --json | no | off | Emit the result as a single JSON object on stdout ({ runId, format, totals, dashboardUrl }) for CI consumption. |
Auto-detection
When --file is omitted, the CLI probes the current working directory in this order. The first match wins.
| Probe order | File / Directory | Format |
| ----------- | ------------------------------- | ---------------- |
| 1 | vitest-results.json | vitest_json |
| 2 | jest-results.json | jest_json |
| 3 | gotestsum.json | gotestsum_json |
| 4 | junit.xml | junit_xml |
| 5 | test-results/*.xml | junit_xml |
| 6 | target/surefire-reports/*.xml | junit_xml |
| 7 | target/test-results/*.xml | junit_xml |
| 8 | build/test-results/*.xml | junit_xml |
When --file is provided without --format, the format is inferred from the filename extension (.xml → junit_xml, known JSON filenames → their matching format). Pass --format explicitly to override either lookup.
Per-framework recipes
Vitest
pnpm exec vitest run --reporter=json --outputFile=vitest-results.json
merged-tests upload --app $MERGED_TEST_APP_IDFor JUnit XML output instead:
pnpm exec vitest run --reporter=junit --outputFile=junit.xml
merged-tests upload --app $MERGED_TEST_APP_ID --file junit.xml --format junit_xmlJest
jest --json --outputFile=jest-results.json
merged-tests upload --app $MERGED_TEST_APP_ID --format jest_jsonPlaywright
npx playwright test --reporter=json > playwright-results.json
merged-tests upload --app $MERGED_TEST_APP_ID --file playwright-results.json --format jest_jsonGo (go test -json)
Non-JS users typically curl directly. The CLI also works if you have Node available:
go test ./... -json | tee gotest.ndjson
merged-tests upload --app $MERGED_TEST_APP_ID --file gotest.ndjson --format gotest_jsonGo (gotestsum, preferred in CI)
gotestsum --jsonfile gotestsum.json -- ./...
merged-tests upload --app $MERGED_TEST_APP_IDRust (cargo nextest, preferred)
cargo nextest run --message-format libtest-json-plus > nextest.ndjson
merged-tests upload --app $MERGED_TEST_APP_ID --file nextest.ndjson --format nextest_jsonPython (pytest with pytest-json-report)
pip install pytest-json-report
pytest --json-report --json-report-file=pytest.json
merged-tests upload --app $MERGED_TEST_APP_ID --file pytest.json --format pytest_jsonRuby (RSpec)
rspec --format json --out rspec.json
merged-tests upload --app $MERGED_TEST_APP_ID --file rspec.json --format rspec_jsonJava / Kotlin (Maven Surefire)
mvn test
# Auto-detection finds target/surefire-reports/*.xml
merged-tests upload --app $MERGED_TEST_APP_IDJava / Kotlin (Gradle)
./gradlew test
merged-tests upload --app $MERGED_TEST_APP_ID --file build/test-results/test/TEST-*.xml --format junit_xml.NET
dotnet test --logger "junit;LogFilePath=TestResults/junit.xml"
merged-tests upload --app $MERGED_TEST_APP_ID --file TestResults/junit.xml --format junit_xmlAny other framework (JUnit XML fallback)
Any test runner that can emit JUnit XML works. Examples: PHPUnit (--log-junit), ExUnit (junit_formatter), Mocha (mocha-junit-reporter), Karma (karma-junit-reporter).
<your-test-runner> --junit-output=junit.xml
merged-tests upload --app $MERGED_TEST_APP_ID --file junit.xml --format junit_xmlCI recipes
GitHub Actions
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v6
with: { node-version: "lts/*", cache: pnpm }
- run: pnpm install
- run: pnpm vitest run --reporter=json --outputFile=vitest-results.json
- name: Upload test results
if: always()
env:
MERGED_API_KEY: ${{ secrets.MERGED_API_KEY }}
MERGED_TEST_APP_ID: ${{ secrets.MERGED_TEST_APP_ID }}
run: npx -y @lubed/test-uploader upload --app $MERGED_TEST_APP_IDif: always() ensures failing runs still upload. Add the same upload step to GitLab CI, CircleCI, or Buildkite using their secret-injection conventions.
Add --json to capture the result as a single JSON object on stdout ({ runId, format, totals, dashboardUrl }) when you want to wire the run id or dashboard URL into later CI steps.
Programmatic usage
import { uploadTestRun, captureContext, detectReportFile } from "@lubed/test-uploader"
const { file, format } = detectReportFile({ cwd: process.cwd() })
const context = captureContext({ environmentLabel: "staging" })
const result = await uploadTestRun({
apiUrl: process.env.MERGED_API_URL ?? "https://api.lube.work",
apiKey: process.env.MERGED_API_KEY,
applicationId: "app_abc123",
file,
format,
context,
})
console.log(`Uploaded run ${result.runId}: ${result.totals.passed} passed, ${result.totals.failed} failed`)Exit codes
| Code | Meaning | | ---- | ----------------------------------------------- | | 0 | Upload succeeded. | | 1 | Upload failed (any error). Stderr explains why. |
The CLI surfaces server errors verbatim. Common cases:
| Error | Cause | Fix |
| ---------------------------------------------------------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------- |
| MERGED_API_KEY env var is required. | Env var not set. | Export it or pass it in the CI step. |
| Could not find a test report file. Tried: ... | Auto-detection found none of the conventional paths. | Pass --file <path> explicitly. |
| Upload failed with status 404: Application ... not found. | Wrong app ID or API key belongs to a different org. | Confirm the app ID in the dashboard URL and the API key's organization. |
| Upload failed with status 413: ... | Report file or result count exceeded the limit (10MB / 50,000 results). | Split the run by package or test suite. |
| Upload failed with status 400: Unsupported test report format ... | Format key typo. | Use one of the eight valid keys listed above. |
| Upload failed with status 422: Failed to parse report with ...Parser | Report file is malformed for the chosen format. | Confirm --format matches the file; re-emit the report. |
Supported formats
| Format key | Description |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| junit_xml | JUnit XML. Universal fallback. Works for Java, Kotlin, .NET, pytest, RSpec, Rust (cargo2junit), Go (gotestsum --junitfile), Vitest/Jest/Playwright JUnit reporters, and anything else that emits JUnit. |
| vitest_json | Vitest JSON reporter. Includes retry metadata. |
| jest_json | Jest JSON reporter. Includes invocations for retries. |
| gotest_json | NDJSON event stream from go test -json. |
| gotestsum_json | gotestsum --jsonfile output (same shape as go test -json). |
| nextest_json | cargo nextest libtest-json-plus format. |
| pytest_json | pytest-json-report plugin output. |
| rspec_json | RSpec --format json output. |
