Skip to main content
RunBook Academy

Git, CI/CD & GitOpsXLIV · ArtifactsReports

Reports and junit — test results, coverage, scan results as structured artifacts

Intermediate⏱ ~20 mingit

What you'll learn

  • Identify the common report formats: JUnit XML, Cobertura, LCOV, SARIF, JSON
  • Upload a test report as an artifact and surface pass/fail summaries in the PR UI
  • Recognise the difference between a report as an artifact (durable, downloadable) and a report as a check (live, branch-protected)
  • Apply the if-no-files-found input to surface a missing report as a failure rather than a silent skip

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

Structured reports are the most common artifact category in modern CI. Every tool that produces machine-readable output — test runners, coverage analysers, SAST scanners, dependency auditors, infrastructure linters — produces a report in a specific format. The report is the artifact: the artifact is the durable store, the format is the contract between the producer and the consumer, and the PR UI surfaces the summary the reviewer needs to make an approval decision.

The common report formats

The formats the production pipeline will encounter:

  • JUnit XML. The de-facto standard for test results. A <testsuite> root with <testcase> children; each test case records name, class, time, and (on failure) a <failure> or <error> element. Most test runners (pytest with --junit-xml, Go with -output=junit.xml, Maven Surefire, Jest with jest-junit) can emit JUnit XML.
  • Cobertura XML. Coverage report format. A <coverage> root with <package> and <class> children; each line and branch carries a hits / misses count.
  • LCOV. Coverage report format. A line-oriented text format with SF: (source file), DA: (line data), BRDA: (branch data). Produced by gcov, llvm-cov, and many language-specific tools.
  • SARIF. Static Analysis Results Interchange Format. A JSON object with runs[].results[]; each result records a rule, a level (error/warning/note), a message, and a location. Produced by CodeQL, Semgrep, Trivy, many SAST/SCA scanners.
  • CycloneDX / SPDX. SBOM formats. JSON or XML describing the components and dependencies of the build.
  • JSON. Generic structured output for custom tools (infrastructure inventory, drift reports, configuration audits).
flowchart LR
    subgraph PRODUCERS["Report producers"]
        P1["pytest"]
        P2["go test"]
        P3["maven surefire"]
        P4["go coverage"]
        P5["codeql"]
        P6["trivy"]
        P7["terraform validate"]
    end
    subgraph FORMATS["Report formats"]
        F1["JUnit XML"]
        F2["Cobertura XML"]
        F3["LCOV"]
        F4["SARIF"]
        F5["CycloneDX JSON"]
        F6["JSON"]
    end
    subgraph CONSUMERS["Artifact consumers"]
        C1["PR UI summary"]
        C2["Code-scanning alerts"]
        C3["Coverage badge"]
        C4["Audit pipeline"]
        C5["Long-term archive"]
    end
    P1 --> F1
    P2 --> F1
    P3 --> F1
    P4 --> F2
    P4 --> F3
    P5 --> F4
    P6 --> F4
    P7 --> F6
    F1 --> C1
    F2 --> C3
    F3 --> C3
    F4 --> C2
    F5 --> C4
    F6 --> C4
    F1 --> C5
    F4 --> C5

Uploading a report

The upload pattern is identical to any other artifact; the format is the difference. A JUnit upload:

- name: Run tests
  run: pytest --junitxml=junit.xml
- name: Upload test report
  if: always()
  uses: actions/upload-artifact@v4
  with:
    name: junit
    path: junit.xml
    if-no-files-found: error
    retention-days: 90

The if: always() ensures the upload runs even when the tests fail; without it, a failing test skips the upload and the PR loses the failure details. The if-no-files-found: error makes a missing report a workflow failure rather than a silent skip; this catches the case where the test runner’s CLI flag changed and the report is no longer produced.

Reports as artifacts versus reports as checks

A report-as-artifact is a durable blob in the artifact store; the consumer downloads it when needed. A report-as-check is a live API call that the workflow makes to the GitHub Checks API (or the GitHub Actions UI) — the data is rendered in the PR without a download step. The two are not interchangeable:

  • Artifact — durable, downloadable, retention-bounded. The consumer is a downstream pipeline, an audit pipeline, or a human who wants the raw bytes.
  • Check — live, branch-protected, fails the merge if the check fails. The consumer is the PR UI and the branch protection rule.

Most test runners produce both: the runner writes JUnit XML (the artifact) and the actions/tooling annotates the PR with pass/fail counts (the check). The artifact is the permanent record; the check is the gate.

flowchart TB
    A["pytest --junitxml=junit.xml"] --> B["junit.xml"]
    A --> C["GitHub Actions test reporter"]
    B --> D["upload-artifact junit"]
    C --> E["PR UI: pass/fail counts"]
    D --> F["Artifact store"]
    E --> G["Required check on branch protection"]
    F --> H["Audit pipeline downloads junit"]
    F --> I["Long-term archive"]

Production discipline

  1. Use if: always() on test report uploads. A failing test must produce a downloadable report.
  2. Use if-no-files-found: error on report uploads. A missing report is a failure, not a skip.
  3. Set retention-days to the audit window. A typical value is 90 days; compliance-bound teams may need longer.
  4. Match the report format to the consumer. JUnit for test UIs, SARIF for code-scanning, Cobertura or LCOV for coverage badges, JSON for custom audit pipelines.
  5. Pair the artifact with the live check. The artifact is the durable record; the check is the gate.

Cross-course references

  • Git, CI/CD & GitOps — Part XLIV-01 (What an artifact is) covers the artifact model.
  • Git, CI/CD & GitOps — Part XXXVIII-06 (Artifacts, caches, and outputs) covers the three-mechanism model.
  • Linux for Production Sysadmins — Part XXXIV (ConfigMgmt) applies the same report-as-artifact pattern to package build reports.

Quiz

Knowledge check · 4 questions

  1. Q1. A test runner produces a JUnit XML report. The upload-artifact step uses `if-no-files-found: warn`. What is the failure mode?

  2. Q2. A test report upload without `if: always()` skips when the tests fail, hiding the failure details from the PR UI.

  3. Q3. Name four common report formats used as CI artifacts and state which consumer each format is paired with.

  4. Q4. An audit discovers that 30% of test reports are missing from the artifact store. Diagnose the configuration error and prescribe the fix.

    Team T's pipeline runs pytest on every PR. The upload step uses `actions/upload-artifact@v4` with `name: junit`, `path: junit.xml`, no `if-no-files-found`, and no `if:` clause. The default `if:` is `success()`. A failing test fails the upload step, so the report is not uploaded. An audit three months later finds that 30% of PR runs have no junit artifact. The team cannot tell whether the tests ran or were silently skipped.

Passing score: 75%. Answers are checked in this browser.