Download JUnit XML from openQA pipeline for test reports

Downloads the JUnit XML artifacts from the downstream openQA jobs,
then names them according to their test flow, collecting them as a
test report so we can have a nice summary in merge requests.
This commit is contained in:
Thomas Duckworth
2026-07-26 16:12:12 +10:00
parent 93df0052c0
commit ab8a95f098
2 changed files with 115 additions and 0 deletions

View File

@@ -85,6 +85,28 @@ trigger-openqa:
SYSUPDATE_PUBKEY_B64: $SYSUPDATE_PUBKEY_B64
OPENQA_BRANCH: master
# Collect the JUnit files produced by the triggered openQA pipeline.
openqa-report:
stage: test
image: python:3.13-alpine
rules:
- if: $CI_PROJECT_PATH != 'kde-linux/kde-linux'
when: never
- when: always
needs:
- job: trigger-openqa
artifacts: false
script:
- python3 .gitlab-ci/scripts/download-openqa-results.py
artifacts:
when: always
expire_in: 4 weeks
reports:
junit:
- openqa-results/**/gitlab-artifacts/*-results.xml
paths:
- openqa-results/
# Publish the staged image in storage.
publish:
stage: publish

View File

@@ -0,0 +1,93 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
# SPDX-FileCopyrightText: 2026 Thomas Duckworth <tduck@filotimoproject.org>
import io
import json
import os
import urllib.parse
import urllib.request
import zipfile
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Any
def get(url: str, token: str, json_response: bool = True) -> Any:
request = urllib.request.Request(url, headers={"JOB-TOKEN": token})
with urllib.request.urlopen(request) as response:
return json.load(response) if json_response else response.read()
def label(xml: bytes, job: str) -> bytes:
root = ET.fromstring(xml)
for testcase in root.iter("testcase"):
testcase.set("classname", f"{job}.{testcase.get('classname')}")
return ET.tostring(root, encoding="utf-8", xml_declaration=True)
def main() -> None:
# Fetches JUnit results from the downstream openQA pipeline.
env = os.environ
api = env["CI_API_V4_URL"]
token = env["CI_JOB_TOKEN"]
project = urllib.parse.quote("kde-linux/os-autoinst-distri-kdelinux", safe="")
bridges = get(
f"{api}/projects/{env['CI_PROJECT_ID']}/pipelines/"
f"{env['CI_PIPELINE_ID']}/bridges",
token,
)
bridge = next(
(
bridge
for bridge in bridges
if bridge["name"] == "trigger-openqa"
),
None,
)
if bridge is None:
return
downstream = next(
(
bridge["downstream_pipeline"]["id"]
for bridge in [bridge]
if bridge["downstream_pipeline"] is not None
),
None,
)
if downstream is None:
return
jobs = get(f"{api}/projects/{project}/pipelines/{downstream}/jobs", token)
output = Path("openqa-results")
for job in jobs:
if not job.get("artifacts_file"):
continue
archive: bytes = get(
f"{api}/projects/{project}/jobs/{job['id']}/artifacts",
token,
False,
)
with zipfile.ZipFile(io.BytesIO(archive)) as zipped:
for name in zipped.namelist():
if name.startswith("gitlab-artifacts/") and name.endswith("-results.xml"):
xml = zipped.read(name)
if not xml.strip():
print(f"skipping empty XML file {name!r} from job {job['name']!r}")
continue
try:
labelled_xml = label(xml, str(job["name"]))
except ET.ParseError as error:
print(f"skipping {name!r}: {error}")
continue
result = output / str(job["id"]) / name
result.parent.mkdir(parents=True, exist_ok=True)
result.write_bytes(labelled_xml)
if __name__ == "__main__":
main()