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"
  • --url defaults to UNITRACK_URL (or http://localhost:8080); --token to UNITRACK_TOKEN (sent as a Bearer header).

  • --junit, --jacoco, --perf accept glob patterns and may be repeated. Coverage format is auto-detected (JaCoCo / Cobertura / LCOV / OpenCover).

  • --run-key merges sharded uploads into one run; --flag tags a component.

  • --dry-run resolves files and prints what would be sent, without uploading.

  • --allow-empty permits an upload when no report files matched (otherwise that is an error).

  • --verbose prints the resolved request (token redacted) before sending.

  • --soft-fail treats 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

GITHUB_* (PR head SHA + branch + number read from the event payload, not the synthetic merge SHA)

GitLab CI

CI_*

Jenkins

GIT_COMMIT, GIT_BRANCH, BUILD_URL, JOB_NAME, BUILD_TAG

CircleCI

CIRCLE_*

Buildkite

BUILDKITE_*

Azure Pipelines

BUILD_*

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 (gate).

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.)