CI Recipes (per ecosystem & per CI)

Copy-paste recipes to push results to UniTrack from any CI. Two steps everywhere:

  1. Make your tests emit JUnit XML + a coverage report (most tools need a non-default reporter — see the footguns below).

  2. Add one upload step (the unitrack-cli engine, run as a GitHub Action, a Docker image, or java -jar).

The uploader auto-detects project/branch/commit/build-url/PR/run-key from the CI environment, and defaults to conventional report globs — so most recipes are one line.

1. Step 1 — emit the reports (per ecosystem)

The silent footgun in every stack: the test runner does not write JUnit XML or coverage XML by default. Add the reporter, then point the uploader at its output.

Ecosystem JUnit XML Coverage (XML/LCOV)

JVM (Maven/Gradle)

Surefire writes target/surefire-reports/*.xml by default.

JaCoCo XML is opt-in: Maven <format>XML</format> on the report goal; Gradle jacocoTestReport { reports { xml.required = true } }**/jacoco.xml.

Python

pytest --junitxml=junit.xml (cleanest path — the flagship recipe).

coverage run -m pytest && coverage xml → Cobertura coverage.xml.

JS/TS

Add jest-junit (or vitest’s JUnit reporter) — not default → junit.xml.

Istanbul/--coveragecoverage/lcov.info.

.NET

dotnet test emits TRX, not JUnit — add the JunitXml.TestLogger NuGet pkg, then --logger "junit;LogFilePath=test-results/junit.xml".

Coverlet → **/coverage.cobertura.xml (in a nested GUID dir).

Go

Neither output is native: gotestsum --junitfile junit.xml.

go test -coverprofile=cover.out then gocover-cobertura < cover.out > coverage.xml.

2. Step 2 — upload (per CI)

2.1. GitHub Actions

- uses: alexmond/unitrack/action@v0
  with:
    url: ${{ vars.UNITRACK_URL }}
    token: ${{ secrets.UNITRACK_TOKEN }}
    # junit/jacoco default to conventional globs; override with `junit:` / `jacoco:` if needed
    # split-by-module: "true"   # multi-module: each module becomes its own coverage component

project/branch/commit/buildUrl/build number/PR number/run-key are auto-detected from GITHUB_*.

For a multi-module build, set split-by-module: "true". Each module (the directory before /target/) is uploaded as its own coverage flag/component — so the project shows per-module tests, coverage and quality gates under "Coverage by flag" — plus a merged rollup that stays the project’s headline. Without it, every module merges into one flat run.

2.2. GitLab CI

unitrack:
  stage: .post
  image: ghcr.io/alexmond/unitrack-uploader:0
  script:
    - upload --junit "**/junit.xml" --jacoco "**/coverage.xml"
  variables:
    UNITRACK_URL: $UNITRACK_URL
    UNITRACK_TOKEN: $UNITRACK_TOKEN

Branch/commit/MR-IID/pipeline-URL are auto-detected from CI_*.

2.3. Jenkins (declarative)

stage('UniTrack') {
  steps {
    sh '''
      docker run --rm -v "$PWD:/work" -w /work \
        -e UNITRACK_URL -e UNITRACK_TOKEN \
        ghcr.io/alexmond/unitrack-uploader:0 \
        upload --junit "**/surefire-reports/*.xml" --jacoco "**/jacoco.xml"
    '''
  }
}

2.4. CircleCI

- run:
    name: Upload to UniTrack
    command: |
      docker run --rm -v "$PWD:/work" -w /work \
        -e UNITRACK_URL -e UNITRACK_TOKEN \
        ghcr.io/alexmond/unitrack-uploader:0 \
        upload --junit "**/test-results/**/*.xml"

Anywhere a JDK is already present, the same command is java -jar unitrack-cli.jar upload ….

3. The quality gate

Fail the build on a red gate with a second step (exit 1 = gate failed):

# any CI, after the upload
docker run --rm -e UNITRACK_URL -e UNITRACK_TOKEN \
  ghcr.io/alexmond/unitrack-uploader:0 \
  gate --project myapp --commit "$GIT_SHA"

The GitHub Action exposes the same via a gate: true input. See Quality Gate.

4. Behind Cloudflare Access (or any proxy/WAF)

When the server is exposed publicly behind a blanket gate, keep the gate on the UI and let CI reach the API with a machine credential — the two are already separate paths (/api/** vs the dashboard at /), so a per-path policy covers it without moving anything.

With Cloudflare Access, create two Access applications on the host:

  • unitrack.example.org/api/* → a Service Token policy (machines).

  • unitrack.example.org (everything else) → interactive login (humans).

The uploader then sends the service-token headers via the action’s headers: input (or the CLI’s repeatable --header / -H):

- uses: alexmond/unitrack/action@v0
  with:
    url: ${{ vars.UNITRACK_URL }}
    token: ${{ secrets.UNITRACK_TOKEN }}        # app-level ingest token (authn at UniTrack)
    headers: |                                  # Cloudflare Access service token (authn at the edge)
      CF-Access-Client-Id: ${{ secrets.CF_ACCESS_CLIENT_ID }}
      CF-Access-Client-Secret: ${{ secrets.CF_ACCESS_CLIENT_SECRET }}

Run the server in closed mode so it enforces auth too (defense in depth): set unitrack.security.open-mode=false and unitrack.security.require-ingest-token=true. See Configuration and Accounts & API Tokens.

5. Fork-PR behaviour

On most CIs, secrets are not exposed to workflows triggered by forked pull requests, so the upload step has no UNITRACK_TOKEN and is skipped/fails. Options: guard the step to run only on same-repo events, or (open-mode servers) allow tokenless ingest. Don’t fail the build on a missing token for forks — wrap the upload in a conditional or use the uploader’s --soft-fail. See Accounts & API Tokens for token scopes.