UniTrack CLI
unitrack-cli is a small Spring Boot command-line uploader that pushes JUnit, coverage and
performance results to a UniTrack server from any CI. It is the same engine behind the Docker
image, the GitHub Action, and (later) the Maven/Gradle plugins.
1. Getting it
The CLI ships as a runnable JAR and as a hermetic OCI image (JRE + the jar) published to GHCR on each version tag — so it runs on any CI with neither Java nor bash on the host.
# Runnable JAR (from a release, or `./mvnw -pl unitrack-cli -am package`)
java -jar unitrack-cli.jar --help
# Or the container image (no JDK needed on the host). The workspace is mounted at /work;
# pass the server URL and token via env (never baked into a layer).
docker run --rm -v "$PWD:/work" \
-e UNITRACK_URL=https://unitrack.example -e UNITRACK_TOKEN=ut_xxx \
ghcr.io/alexmond/unitrack-uploader upload --project app \
--junit 'target/surefire-reports/*.xml'
The image is built from deploy/cli.Containerfile and pushed by the publish-cli-image
workflow (semver + latest tags). An ingest-scoped token is the right credential here.
2. GitHub Action
A Docker-based action wraps the uploader image, so a GitHub workflow needs one step. URL and token come from a repo variable/secret; an ingest-scoped token is the right credential.
- uses: alexmond/unitrack/action@v1
with:
url: ${{ vars.UNITRACK_URL }}
token: ${{ secrets.UNITRACK_TOKEN }}
project: myapp
branch: ${{ github.ref_name }}
commit: ${{ github.sha }}
junit: 'target/surefire-reports/*.xml'
jacoco: 'target/site/jacoco/jacoco.xml'
gate: true # fail the build if the quality gate doesn't pass
junit/jacoco/perf accept one glob per line. The action lives at action/ in this repo
(action/action.yml); list it on the Marketplace from a dedicated repo when ready.
3. Uploading
java -jar unitrack-cli.jar upload \
--url https://unitrack.example \
--project myapp \
--commit "$GIT_SHA" \
--branch "$GIT_BRANCH" \
--build "$CI_JOB_URL" \
--junit "target/surefire-reports/*.xml" \
--jacoco "target/site/jacoco/jacoco.xml"
On a supported CI, the metadata is auto-detected, so the command collapses to just the reports:
java -jar unitrack-cli.jar upload --junit "target/surefire-reports/*.xml" --jacoco "target/site/jacoco/jacoco.xml"
-
--urldefaults toUNITRACK_URL(orhttp://localhost:8080);--tokentoUNITRACK_TOKEN(sent as a Bearer header). -
--junit,--jacoco,--perfaccept glob patterns and may be repeated. Coverage format is auto-detected (JaCoCo / Cobertura / LCOV / OpenCover). -
--run-keymerges sharded uploads into one run;--flagtags a component. -
--dry-runresolves files and prints what would be sent, without uploading. -
--allow-emptypermits an upload when no report files matched (otherwise that is an error). -
--verboseprints the resolved request (token redacted) before sending. -
--soft-failtreats an upload/transport failure as a warning (exit 0), so a flaky network never reddens a green build.
Transient failures (network errors, HTTP 429/502/503/504) are retried with exponential backoff,
using the org.springframework.core.retry support built into Spring Framework 7 (no extra
dependency). Uploads over the server’s size caps (25 MB/file, 100 MB/request) are rejected
before sending, with a clear message.
The project page in the UI shows a ready-to-paste command for each project ("Push results from CI").
4. CI auto-detection
When run inside a known CI, the uploader infers project, branch, commit, build URL, repo URL, run key and PR number from the environment — so you rarely pass any metadata flag. Explicit flags always override detection.
| CI | Detected from |
|---|---|
GitHub Actions |
|
GitLab CI |
|
Jenkins |
|
CircleCI |
|
Buildkite |
|
Azure Pipelines |
|
The run key is derived from the CI run id, so sharded/matrix jobs merge into one run with no configuration.
5. Gating a build
gate fails the build (exit 1) when the project’s latest run did not pass the quality gate —
works on any CI:
java -jar unitrack-cli.jar gate --project myapp --commit "$GIT_SHA"
6. Exit codes
| Code | Meaning |
|---|---|
0 |
Success (upload completed, or gate passed). |
1 |
The quality gate did not pass ( |
2 |
Usage / configuration error (bad flag, or no report files matched). |
3 |
Transport failure (network error, or a server error after retries). |
4 |
The server rejected the payload (e.g. too large, unprocessable). |
Upload and gate are separate steps with separate exit semantics: a flaky upload should not redden a green build, while the gate step is the one allowed to fail it.
7. Maven plugin
For Maven builds, the unitrack-maven-plugin is the idiomatic, lowest-config path — it reuses
the same CLI engine, auto-discovers Surefire/Failsafe/JaCoCo reports under the project, and reads
the project name from the POM. Bind upload to verify:
<plugin>
<groupId>org.alexmond</groupId>
<artifactId>unitrack-maven-plugin</artifactId>
<version>0.1.0-SNAPSHOT</version>
<executions>
<execution>
<goals><goal>upload</goal></goals>
</execution>
</executions>
</plugin>
url/token come from -Dunitrack.url/-Dunitrack.token or UNITRACK_URL/UNITRACK_TOKEN.
The gate goal fails the build on a red quality gate. (Published to Maven Central with a v1
line versioned against the ingest API — pending release.)
8. Gradle plugin
For Gradle builds, apply the org.alexmond.unitrack plugin — it registers unitrackUpload
and unitrackGate tasks that run the same CLI engine and default to Gradle’s report locations
(build/test-results, build/reports/jacoco):
plugins {
id("org.alexmond.unitrack") version "0.1.0-SNAPSHOT"
}
unitrack {
url = providers.environmentVariable("UNITRACK_URL").orNull
token = providers.environmentVariable("UNITRACK_TOKEN").orNull
}
Then ./gradlew test unitrackUpload. The plugin resolves the unitrack-cli executable jar and
runs it, so there’s one upload code path across the CLI, Maven and Gradle. (Published to the
Gradle Plugin Portal with a v1 line — pending release.)