Code-Qualität kontinuierlich messen und sukzessive erhöhen
AUTOR OLIVER DASSDORF
„If it hurts, do it more frequently, and bring the pain forward“
Diesen Spruch hat Jez Humble bereits vor mehreren Jahren geprägt. Ziel ist es, Rückmeldungen bei Code-Integrationen möglichst früh und nah am Verursacher zu geben. Dafür werden Prozessschritte automatisiert, deren Durchlaufzeiten optimiert und durch sinnvolle Trigger (z.B. commit, erfolgreicher Build, Zeitabhängigkeit) ausgelöst. Diesbezüglich haben sich Continuous Integration (CI) Systeme, wie GitLab CI oder Jenkins, etabliert, die dabei unterstützen den gesamten Ablauf kontinuierlich stattfinden zu lassen. Durch frühes Feedback können Erkenntnisse schneller gewonnen werden, wodurch robustere Prozesse mit geringerer Fehleranfälligkeit resultieren können. Doch, wie lassen sich diese Systeme nutzen, um die Code-Qualität messen und verbessern zu können?
Dies möchte ich mit einer bestehenden .NET Applikation testen. Dabei werde ich meine Fancy_App mit Hilfe der GitLab CI in einem Docker-Container bauen sowie testen.
Zusätzlich möchte ich eine statische Code-Analyse bei einer externen Plattform von meiner GitLab CI aus starten. Dafür wird die nicht-kommerzielle Version von SonarQube eingesetzt.
Selbstverständlich sollen alle Ergebnisse anschließend schön aufbereitet werden, sodass eine komfortable Fehlersuche möglich wird.
Die Aufgaben bestehen wesentlich aus den Schritten:
-
Anbindung der .NET Applikation an GitLab CI
-
Anbindung der GitLab CI an SonarQube
-
Visualisierung von Testergebnissen in GitLab CI und SonarQube
Anbindung der .NET Applikation an die GitLab CI
Hierfür muss zunächst die GitLab CI Umgebung konfiguriert werden. Da die Applikation in einem Docker-Container ausgeführt werden soll, wird ein Docker Executor benötigt. Des Weiteren muss eine .gitlab-ci.yml
Datei im Root-Verzeichnis der Applikation angelegt werden. In dieser Datei werden die CI-Jobs der Anwendung definiert. Dies funktioniert mit Hilfe von selbst definierenden Stages.
Bauen
Zunächst einmal muss der Code gebaut werden. Dies wird in der Stage build
realisiert. Durch das image
Keyword geben wir das Docker-Image an, welches für die Durchführung der Prozessschritte verwendet wird. Da es sich um eine .NET Applikation handelt, ist für das Kompilieren die .NET CLI notwendig. Diese befindet sich im .NET SDK 3.1 Image von Microsoft. Durch die Verwendung können sämtliche Kommandos, die für das Bauen der Applikation notwendig sind, durchgeführt werden. Für die Shell-Kommandos, welche der GitLab-Runner ausführt, wird das script
Keyword verwendet.
build:
stage: build
image: 'mcr.microsoft.com/dotnet/sdk:3.1'
script:
- 'dotnet restore Fancy_App.NET.sln'
- 'dotnet build Fancy_App.NET.sln'
- 'dotnet publish Fancy_App.NET.sln --output output_dir'
Testen
Getreu dem Motto: "It compiles, let's sell it!" könnten wir hier nun aufhören.
Jedoch existieren noch weitere empfehlenswerte Mechanismen, um Code-Qualität feststellen zu können. Unsere Fancy_App soll Unit-Tests unterzogen werden sowie einer Analyse der Test-Coverage. Dies soll in einer eigenen Stage geschehen, die wir test
nennen.
test:
stage: test
image: 'mcr.microsoft.com/dotnet/sdk:3.1'
script:
- 'dotnet test Fancy_App.NET.sln --results-directory result /p:CollectCoverage=true
artifacts:
when: always
reports: # to visualize in GitLab
junit: result/test_result.xml
In der Ausgabe erhalten wir etwa folgende Aussage für das Ergebnis der 22 Unittests:
Test Run Successful.
Total tests: 22
Passed: 22
Total time: 3,9445 Seconds
Alle erfolgreich! Sehr gut!
Auch die GitLab CI kann diesen Wert in der GUI visualisieren, vorausgesetzt wir archivieren die Artefakte im jUnit
-Format. Sollte in Zukunft ein Test fehlschlagen, schlägt auch der CI-Job test
fehl.
Doch, wie sieht es mit der Test-Coverage aus?
Hierfür erhalten wir in der Kommandozeile lediglich folgende Ausgabe zum Ergebnis:
+---------+--------+--------+--------+
| | Line | Branch | Method |
+---------+--------+--------+--------+
| Total | 14,28% | 11,26% | 12,68% |
+---------+--------+--------+--------+
| Average | 14,28% | 11,26% | 12,68% |
+---------+--------+--------+--------+
Damit die Coverage Spalte in der GitLab CI GUI nicht, wie in der folgenden Abbildung dargestellt, leer bleibt, besteht die Möglichkeit eine Regular Expression anzugeben, die im generierten Log geparsed wird.
Unter Settings
-> CI/CD
-> General pipelines (Expand)
-> Test coverage parsing
kann dann folgende Regular Expression eingetragen werden: Total.*?(\d+(?:\.\d+)?)%
Doch Moment mal... Nur 14% Test-Coverage?
Das sollte schleunigst geändert werden. Jedoch gibt es keine weiteren Informationen darüber, welche Zeilen durchlaufen bzw. nicht durchlaufen werden. Diesbezüglich besteht aber die Möglichkeit sich in XML
- oder JSON
-Form genauere Informationen als Artefakt generieren zu lassen. Aber Fehlersuche in einem XML
- bzw. JSON
-Dokument mit mehreren 1000 Zeilen? Das muss doch komfortabler funktionieren. Wie schön, dass dafür ein NuGet-Paket existiert, welches leicht installiert werden kann. Mit Hilfe des dotnet tool install
Befehls, erhält man den dotnet-reportgenerator
. Dadurch können Test-Coverage Ergebnisse in ein für Menschen angenehm lesbares Format in HTML
-Form abgebildet werden. Dem Reportgenerator muss man lediglich den Pfad zur generierten XML
- bzw. JSON
-Datei mit den Test-Coverage Ergebnissen als Parameter übergeben. Ansonsten werden Informationen bzgl. Ausgabeformat und Zielverzeichnis angegeben. Wir erweitern also unsere vorherige CI-Stage und schon kann man sich das generierte HTML
-Dokument mit jedem CI-Durchlauf als Artefakt archivieren lassen, um gegebenenfalls nach Fehlern zu suchen.
test:
stage: test
image: 'mcr.microsoft.com/dotnet/sdk:3.1'
script:
- 'dotnet tool install --global dotnet-reportgenerator-globaltool'
- 'dotnet test Fancy_App.NET.sln --results-directory result /p:CollectCoverage=true /p:CoverletOutputFormat=<COVERAGE_FORMAT> /p:CoverletOutput=<COVERAGE_RESULT_FILE>'
- 'reportgenerator "-reports:<COVERAGE_RESULT_FILE>" "-targetdir:<PATH_TO_COVERAGE_REPORT>" -reporttypes:Html -title:Coverage'
artifacts:
when: always
paths:
- '<PATH_TO_COVERAGE_REPORT>' # coverage HTML reports as artificts (for download)
reports: # to visualize in GitLab
junit: result/test_result.xml
cobertura: <COVERAGE_RESULT_FILE>
Das reicht uns noch nicht!
Bisher messen wir unsere Code-Qualität nur durch die Unittests und der Test-Coverage. Allerdings existieren noch viele weitere Metriken, mit denen Code-Qualität gemessen werden kann. Eine populäre Metrik ist die statische Code-Analyse, bei der der Quellcode der Anwendung analysiert wird, ohne diesen tatsächlich auszuführen. Die Einstellmöglichkeiten reichen von Style-Metriken, wie "Tabs statt Leerzeichen" bis hin zu Komplexitätsmetriken, wie "Anzahl der verschachtelten if
-Anweisungen". Aber auch Programmierfehler, wie fehlende Exceptions können erkannt werden. Zur Ausführung einer statischen Code-Analyse existieren viele Tools. Wir werden die statische Code-Analyse SonarQube von SonarSource aus der GitLab CI starten und die Ergebnisse möglichst komfortabel auswerten. Dafür müssen wir die beiden Plattformen zunächst miteinander verknüpfen.
Anbindung der GitLab CI an SonarQube
Folgt man der ausführlichen Dokumentation von SonarQube und GitLab, besteht die Einrichtung aus den folgenden Schritten:
- Konfiguration für die GitLab CI
- SonarQube Token in GitLab CI erstellen
- Umgebungsvariablen in GitLab CI setzen
- Unter
Einstellungen
->→CI/CD
Variables
kann der SonarQube key als Umgebungsvariable bereitgestellt werden
- Unter
Durchführung der statischen Code-Analyse aus der CI-Stage
Nur wenn das Bauen und Testen der Anwendung erfolgreich waren, macht es Sinn, eine statische Code-Analyse bei SonarQube anzustoßen. Dafür soll eine eigene CI-Stage verantwortlich sein. Im Vorfeld sind jedoch noch einige Konfigurationen notwendig. Um eine statische Code-Analyse mit Hilfe von dotnet
Befehlen durchführen zu können, ist der .NET SonarScanner zu installieren. Dieser kann, wie der dotnet-reportgenerator
, über dotnet tool install
installiert werden. Dieser benötigt für die korrekte Ausführung zusätzlich ein OpenJDK. Als Alternative zur Installation der notwendigen Pakete während des Ausführung des CI-Jobs, kann auch ein eigenes Docker-Image gebaut werden. Über Befehle in einem Dockerfile
, die in diesem Blog-Artikel nicht weiter erläutert werden sollen, könnten dann im Vorfeld alle notwendigen Tools und Pakete installiert werden.
Bei der Ausführung der statischen Code-Analyse mit dem .NET SonarScanner wird es etwas knifflig. Dieser muss zunächst gestartet werden, woraufhin er auf die Ausführung der Kompilier- und Test-Prozesse wartet bis er anschließend wieder beendet werden muss. Daraus folgt, dass die Artefakte aus den vorherigen CI-Stages build
und test
nicht verwendet werden können und wir ein zweites Mal kompilieren und testen müssen. Schade, aber ein Übel mit dem man klar kommen muss!
Während des Kompiliervorgangs werden Dateien generiert, die für die statische Code-Analyse irrelevant sind und das Ergebnis verfälschen würden. Diese generierten Dateien können beim Starten des .NET SonarScanners mit Hilfe des folgenden Parameters ignoriert werden./d:sonar.scm.exclusions.disabled=true
Bei einer kontinuierlichen Durchführung der statischen Code-Analyse ist eine Versionierung der Analyse von Vorteil. Hierfür bietet die GitLab CI bereits die vordefinierte Variable CI_JOB_ID
. Dabei handelt es sich um eine eindeutige ID des CI-Jobs. Dadurch ist eine Zuordnung zwischen dem CI-Job und der extern ausgeführten statischen Code-Analyse, möglich. Die ID kann beim Starten des .NET SonarScanners mit /version:$CI_JOB_ID
angegeben werden.
Selbstverständlich sollte die statische Code-Analyse bei Verschlechterung der Code-Qualität die Entwickler entsprechend in Kenntnis setzen. Dies kann mit Hilfe von Quality Gates beim Starten des .NET SonarScanners durch /d:sonar.qualitygate.wait=true
aktiviert werden. Dadurch wartet die GitLab CI darauf, bis SonarQube die Ergebnisse liefert. Sollte die statische Code-Analyse von SonarQube fehlschlagen, schlägt nun der gesamte CI-Job fehl.
sonarqube-analysis:
stage: sonarqube-analysis
image: 'mcr.microsoft.com/dotnet/sdk:3.1'
script:
- 'dotnet tool install -g dotnet-sonarscanner'
- 'dotnet-sonarscanner begin /key:"SONAR_KEY" /d:sonar.host.url=$SONAR_HOST_URL /d:sonar.login=$SONAR_TOKEN /d:sonar.scm.exclusions.disabled=true /d:sonar.cs.opencover.reportsPaths="test_coverage.opencover.xml" /d:sonar.cs.xunit.reportsPaths="**/TestResults.xml" /d:sonar.qualitygate.wait=true /version:$CI_JOB_ID'
- 'dotnet build Fancy_App.NET.sln'
- 'dotnet test Fancy_App.NET.sln --results-directory result /p:CollectCoverage=true /p:CoverletOutputFormat=<COVERAGE_FORMAT> /p:CoverletOutput=<COVERAGE_RESULT_FILE>'
- 'dotnet-sonarscanner end /d:sonar.login=$SONAR_TOKEN'
Langsam aber sicher...
Nachdem die gesamte CI-Pipeline nun durchgelaufen ist, bemerkt man, dass die Verknüpfung zu SonarQube erfolgreich ist. Die statische Code-Analyse wird angestoßen und die Issues werden visualisiert. SonarQube bietet zusätzlich die Möglichkeit Unittest-Testergebnisse und Test-Coverage-Ergebnisse zu visualisieren. Warum dies also nicht unterstützend verwenden? Und damit kommen wir wohl zum unangenehmsten Teil der ganzen Arbeit. Denn was bringen die besten Tests und Code-Analysen, wenn diese nicht in einem optisch schönen Format darstellbar sind und somit die Fehlersuche erschwert wird.
Visualisierung von Testergebnissen in GitLab CI und SonarQube
Wir haben bereits gelernt, dass der .NET SonarScanner zunächst gestartet werden muss. Anschließend müssen die Kompilier- und Test-Prozesse ausgeführt werden und abschließend muss der .NET SonarScanner beendet werden. So weit so gut. Die aus den Kompilier- und Testprozessen entstandenen Artefakte werden dann vom .NET SonarScanner analysiert. Korrekt funktioniert dies aktuell jedoch nur für die statische Code-Analyse aber nicht für unsere Test-Artefakte (Unittest- und Test-Coverage-Artefakte). Das liegt daran, dass SonarQube das erstellte Dateiformat zur Visualisierung der Ergebnisse nicht interpretieren kann. Auch die GitLab CI kann nur bestimmte Dateiformate lesen. Daher muss eine Gegenüberstellung der gültigen Dateiformate für die entsprechenden Plattformen (GitLab CI und SonarQube) her. Wichtig für das weitere Verständnis ist die Abgrenzung zwischen der Visualisierung von Unittest-Testergebnissen und Test-Coverage-Testergebnissen. Beide besitzen unterschiedliche Ausgabeformate zwischen denen man sich entscheiden muss.
Unittest-Ausgabeformate für GitLab CI und SonarQube bei .NET-Projekten
dotnet test
kann für Unittest-Testergebnisse mehrere Formate in XML
-Form generieren. Darunter das native Visual Studio Testergebnisse-Format .trx
. Des Weiteren können Testergebnisse als jUnit-
oder xUnit-
Format generiert werden. Es sollte das Format ausgesucht werden, welches idealerweise sowohl von der GitLab CI als auch SonarQube visualisiert werden kann:
jUnit
- Einziges Format, welches von der GitLab CI zur Visualisierung von Testergebnissen unterstützt wird
- Wird nicht von SonarQube unterstützt
.trx
(Visual Studio Testergebnis Format)- Wird nicht von GitLab CI unterstützt (Eine Konvertierung in
jUnit
müsste nachträglich durchgeführt werden) - Das Formatierungstool könnte beispielsweise über ein
Dockerfile
geklont und entsprechend ausgeführt werden
- Wird nicht von GitLab CI unterstützt (Eine Konvertierung in
xUnit
- Wird nicht von GitLab CI zur Visualisierung von Testergebnissen unterstützt
- Kann zwar von SonarQube interpretiert werden, jedoch ist es nicht möglich Resultate auf der SonarQube Plattform anzuzeigen
- Dadurch ist keine Visualisierung möglich, wie viele Tests bestanden haben bzw. durchgefallen sind
- Um diese Hürde zu lösen, müsste eine Konvertierung stattfinden. Dadurch ist es möglich ein
xUnit
-Testergebnis Format in das von SonarQube interpretierbareGeneric Data Format
zu konvertieren. Dieses Tool könnte beispielsweise als .nupkg ins Dockerfile gepackt werden - Aufgrund der Tatsache, dass beim Fehlschlagen der vorgelagerten Unittests keine statische Code-Analyse durchgeführt wird, wird dieser Aufwand nicht betrieben
Somit bleibt uns eigentlich nichts anderes übrig, als zwei Formate zu generieren. In der CI-Stage test
das Artefakt im jUnit
-Format und in der CI-Stage für die SonarQube Analyse xUnit
.
Coverage-Reportformate für GitLab CI und SonarQube bei .NET-Projekten
dotnet test
kann coverlet
und opencover
als Coverage-Report generieren. Beide sind anschließend vom dotnet-reportgenerator
für die zusätzliche Berichtgenerierung zur Archivierung interpretierbar
opencover
- Wird von GitLab CI unterstützt
- Wird von SonarQube unterstützt
coverlet
- Wird von GitLab CI unterstützt
- Wird nicht von SonarQube unterstützt
Hier wird uns die Entscheidung dadurch abgenommen, da nur das opencover
-Format von beiden Plattformen unterstützt wird.
Die angepasste und vollständige .gitlab-ci.yml
Datei findet ihr am Ende dieses Blog-Artikels.
Was haben wir nun erreicht?
Wir haben es geschafft, die Artefakte der Unittest-Testergebnisse und Test-Coverage-Ergebnisse in korrekter Form für die GitLab CI und SonarQube zu generieren. Dadurch liefert SonarQube neben den Issues der statischen Code-Analyse auch eine detaillierte Ansicht zur Test-Coverage. Jetzt wird die Fehlersuche deutlich erleichtert. Die Kopplung zwischen der GitLab CI und SonarQube ist somit erfolgreich eingerichtet. Dadurch wird jetzt mit jedem Durchlauf der CI-Pipeline die Applikation gebaut, getestet sowie eine statische Code-Analyse durchgeführt. Die Unittest-Testergebnisse werden in der GitLab CI visualisiert und verarbeitet. Die Coverage-Resultate werden auf beiden Plattformen dargestellt. Einen detaillierteren Coverage-Report, um zu sehen, welche Zeilen tatsächlich nicht durchlaufen werden, kann über die SonarQube Plattform sowie über ein, durch die GitLab CI archivertes, HTML-Dokument betrachtet werden.
Fazit
Die Anbindung der .NET Applikation an die GitLab CI zum Bauen und Testen stellt sich sehr leicht dar. Hier liefert der .NET SDK 3.1 Docker-Container fast alles mit, was benötigt wird und bietet durch den Long Term Support auch noch zukünftige Kompatibilität.
Für die genauere Analyse von Testergebnissen können mit Hilfe des Reportgenerators ausführlichere HTML-Dokumente generiert werden. Diese können mit jeder CI-Pipeline als Artefakt archiviert und im Falle eines Fehlschlags studiert werden.
Interessant wird es beim Zusammenspiel zwischen der GitLab CI und SonarQube. Die Ausführung der statischen Code-Analyse aus der GitLab-CI Pipeline ist zwar möglich. Eine echte Integration funktioniert leider nicht. Die genauere Analyse der Issues ist aus der GitLab CI nicht möglich, weshalb immer auf die SonarQube Oberfläche gewechselt werden muss. Weiterhin werden in der nicht-kommerziellen Version von SonarQube keine Branches unterstützt. Die statische Code-Analyse sollte daher nur auf einem festen Entwicklungsbranch laufen. Ansonsten kommen Bugs hinzu oder verschwinden, weil die Stände der unterschiedlichen Branches miteinander verglichen werden. Ist ein merge-Workflow für das eigene Projekt notwendig, wird die kommerzielle Version benötigt. Auch ist es nicht möglich, die Analyse und Test-Coverage auf mehrere Schritte zu verteilen, um sie später visualisieren zu können. Dadurch ist für die statische Code-Analyse in der GitLab CI ein zusätzliches Bauen und Testen der Applikation notwendig.
Leider koorperieren GitLab und SonarQube auch nicht bei den Ausgabeformaten der Testergebnisse miteinander. Dadurch sind für die einzelnen Systeme unterschiedliche Protokollformate zu generieren. Aufgrund der oben verfassten Gegenüberstellung der Ausgabeformate hinsichtlich der Unterstützung von der GitLab CI und SonarQube werden folgende Formate empfohlen:
- Unittest-Testformat für GitLab CI:
jUnit
- Unitest-Testformat für SonarQube:
xUnit
- Coverage-Format für GitLab CI und SonarQube:
opencover
Diese Empfehlung beruht auf der Kompatibilität zwischen jUnit
und xUnit
. Des Weiteren ist opencover
das einzige Coverage-Format, welches von beiden Systemen unterstützt wird.
Unabhängig von den genannten Einschränkungen, existiert nun ein System, welches es den Entwicklern erleichtert eine messbar höhere Code-Qualität zu erzeugen und auszuliefern. Für eine echte Integration reicht die Interaktion zwischen der GitLab CI und SonarQube nicht aus. Durch das Feedback von SonarQube, mit dem man in der GitLab CI durch Quality-Gates reagieren kann, profitieren dennoch alle Beteiligten von einer höheren Code-Qualität, wie Entwickler, Projektleiter sowie der Endnutzer.
stages:
- build
- test
build:
stage: build
image: 'mcr.microsoft.com/dotnet/sdk:3.1'
script:
- 'dotnet restore Fancy_App.NET.sln'
- 'dotnet build Fancy_App.NET.sln'
- 'dotnet publish Fancy_App.NET.sln --output output_dir'
artifacts:
when: on_success
paths:
- 'output_dir'
test:
stage: test
image: 'mcr.microsoft.com/dotnet/sdk:3.1'
script:
- 'dotnet tool install --global dotnet-reportgenerator-globaltool'
- 'dotnet test Fancy_App.NET.sln --logger "jUnit;LogFileName=test_result.xml;MethodFormat=Class;FailureBodyFormat=Verbose" --results-directory result /p:CollectCoverage=true /p:CoverletOutputFormat=<COVERAGE_FORMAT> /p:CoverletOutput=<COVERAGE_RESULT_FILE>'
- 'reportgenerator "-reports:<COVERAGE_RESULT_FILE>" "-targetdir:<PATH_TO_COVERAGE_REPORT>" -reporttypes:Html -title:Coverage'
artifacts:
when: always
paths:
- '<PATH_TO_COVERAGE_REPORT>' # coverage HTML reports as artificts (for download)
reports: # to visualize in GitLab
junit: result/test_result.xml
cobertura: <COVERAGE_RESULT_FILE>
sonarqube-analysis:
stage: sonarqube-analysis
image: 'mcr.microsoft.com/dotnet/sdk:3.1'
script:
- 'dotnet tool install -g dotnet-sonarscanner'
- 'dotnet-sonarscanner begin /key:"SONAR_KEY" /d:sonar.host.url=$SONAR_HOST_URL /d:sonar.login=$SONAR_TOKEN /d:sonar.scm.exclusions.disabled=true /d:sonar.cs.opencover.reportsPaths="test_coverage.opencover.xml" /d:sonar.cs.xunit.reportsPaths="**/TestResults.xml" /d:sonar.qualitygate.wait=true /version:$CI_JOB_ID'
- 'dotnet build Fancy_App.NET.sln'
- 'dotnet test Fancy_App.NET.sln --logger:xUnit --results-directory result /p:CollectCoverage=true /p:CoverletOutputFormat=<COVERAGE_FORMAT> /p:CoverletOutput=<COVERAGE_RESULT_FILE>'
- 'dotnet-sonarscanner end /d:sonar.login=$SONAR_TOKEN'allow_failure: true
only:
refs:
- develop
variables:
- $CI_PROJECT_NAMESPACE == <FANCY_APP>
Über den Autor
Oliver Daßdorf arbeitet als Software-Entwickler im Embedded-Bereich der MATHEMA GmbH. Wenn er nicht gerade versucht den Entwicklungsprozess durch Einsatz moderner Methoden zu optimieren, beschäftigt Oliver sich mit Kryptowährungen oder anderen technischen Neuheiten.
Links
- https://docs.gitlab.com/runner/executors/docker.html
- https://github.com/microsoft/containerregistry
- https://docs.sonarqube.org/latest/analysis/gitlab-integration/
- https://docs.sonarqube.org/latest/user-guide/user-token/
- https://docs.gitlab.com/ee/ci/variables/#creating-a-custom-environment-variable
- https://docs.sonarqube.org/latest/analysis/scan/sonarscanner-for-msbuild/
- https://docs.gitlab.com/ee/ci/unit_test_reports.html
- https://docs.sonarqube.org/latest/analysis/coverage/
- https://gitlab.com/gitlab-org/gitlab/-/issues/28798
- https://github.com/gfoidl/trx2junit
- https://github.com/yzhoholiev/trxtosonar/releases/tag/1.0.0
- https://docs.gitlab.com/ee/user/project/merge_requests/test_coverage_visualization.html
- https://community.sonarsource.com/t/is-it-possible-to-split-analysis-and-test-coverage-into-two-separate-independet-ci-build-steps/10445/2