diff --git a/Home.md b/Home.md index 976ae34..b23aa28 100644 --- a/Home.md +++ b/Home.md @@ -10,6 +10,7 @@ Repositories über `uses:` aufgerufen (`on: workflow_call`). | --- | --- | | [docker-build-push](docker-build-push) | Baut ein Docker-Image und schiebt es als Multi-Arch-Manifest in eine Registry. Mit abgeschaltetem Push dient derselbe Baustein als Bau-Prüfung. | | [publish-helm-chart](publish-helm-chart) | Packt ein Helm-Chart und lädt es in eine klassische, indexbasierte Helm-Registry. | +| [sonar-analysis](sonar-analysis) | Schickt den Baum des aufrufenden Repositories durch eine SonarQube-Analyse. Eine Beobachtung des Zustands, bewusst kein Torwächter. | Dazu eine Seite quer zu beiden: [Gemessenes-Verhalten](Gemessenes-Verhalten) — die Eigenheiten dieser Forge, die hier Geld gekostet haben, mit dem Lauf oder diff --git a/sonar-analysis.md b/sonar-analysis.md new file mode 100644 index 0000000..954b158 --- /dev/null +++ b/sonar-analysis.md @@ -0,0 +1,174 @@ +# sonar-analysis + +`.gitea/workflows/sonar-analysis.yml` — schickt den Baum des aufrufenden +Repositories durch eine SonarQube-Analyse. Die Eingabe-Tabelle, das +Aufrufbeispiel und die Voraussetzungen stehen im README; diese Seite begründet +den Baustein und benennt, was an ihm **nicht** gemessen ist. + +Der Baustein ist bewusst klein: ein Wächter, ein Checkout, die offizielle +Scan-Action. Alles Projektspezifische — Projektschlüssel, Quellpfade, +Ausschlüsse — steht in der `sonar-project.properties` des aufrufenden +Repositories. Genau deshalb ist er teilbar: Der Workflow ist für jedes +Repository identisch, und ein zweiter Abnehmer kostet eine Datei mit drei +Zeilen. + +## Die Analyse ist eine Beobachtung, kein Torwächter + +Das ist die wichtigste Eigenschaft des Bausteins, und sie folgt aus dem +Auslöser. + +Die SonarQube Community Build analysiert **ausschließlich den Hauptzweig**. Eine +PR-Analyse gibt es dort nicht, der Aufruf hängt deshalb an einem Push auf +`main`, und damit erscheint ein Befund **nach** dem Merge. Zu diesem Zeitpunkt +ist nichts mehr zu blockieren. + +Daraus folgt das fehlende Quality-Gate, und es fehlt absichtlich. Ein rotes Gate +hinter dem Merge färbte nur den Hauptzweig rot, ohne irgendetwas verhindert zu +haben — und einen Dauerzustand lernt man zu ignorieren. Der Schaden wäre nicht +das Gate selbst, sondern dass es die Glaubwürdigkeit der übrigen Prüfungen +mitnimmt: Wer gewöhnt ist, dass ein rotes `main` nichts bedeutet, liest auch die +nächste echte Rotfärbung als Rauschen. + +Sinnvoll wird ein Gate unter genau einer von zwei Bedingungen, und beide ändern +die Lage wirklich: + +- **PR-Analyse ist verfügbar.** Dann blockiert das Gate einen Merge, und das ist + eine Entscheidung, die es tragen kann. +- **Ein Deployment hängt an diesem Workflow.** Dann bewacht es etwas Konkretes. + +Bis dahin ist das Hinzufügen eines Gates keine Nachbesserung, sondern eine +Verschlechterung. Im Workflow steht das als Kommentar an der Stelle, an der der +Schritt stünde — damit niemand ihn als Versehen nachträgt. + +## `fetch-depth: 0` ist nicht optional + +Die eine Checkout-Option, ohne die die Analyse ihren Zweck verliert. + +Ein flacher Klon — die Vorgabe — liefert dem Scanner keine Historie. Er kann +Befunde dann keiner Änderung zuordnen, sondern schreibt sie alle dem letzten +Commit zu. Was damit unbrauchbar wird, ist nicht eine Randfunktion, sondern das +Meiste, wofür man SonarQube überhaupt aufstellt: die Zuordnung zu Autor und +Änderung, der Zeitraum „neuer Code", jedes datumsbasierte Maß. Ein Bericht, in +dem alles gleich alt und von derselben Person ist, sagt nichts. + +Der Preis ist ein langsamerer Checkout. Das ist der ganze Preis. + +## Warum kein Docker-Runner nötig ist + +Der Nachbarbaustein [docker-build-push](docker-build-push) braucht den Runner +mit dem Label `ubuntu-docker`, weil er einen Docker-Socket benutzt. Dieser hier +braucht ihn nicht, und das ist nachgesehen und nicht vermutet: Die Scan-Action +deklariert `runs: using: node24`. Sie ist damit eine **JavaScript-Action**, läuft +im Node des Runners und nicht in einem Container. Kein Socket, kein Daemon, +`ubuntu-latest` genügt. + +Das hat eine Kehrseite, die man im Blick behalten muss. Eine JavaScript-Action +verlangt ihre Node-Hauptversion **im Runner-Image**. Ob diese dort vorhanden +ist, ist **nicht geprüft** — siehe unten. + +## Was der Scanner zur Laufzeit holt + +Die Action bringt den Sonar-Scanner-CLI nicht mit. Sie **lädt ihn bei jedem +Lauf** von `binaries.sonarsource.com` und prüft dessen GPG-Signatur. Das ist +nichts Kleines: Bei der Messung für diese Seite lag das Linux-Archiv bei rund +**59 MiB** (`Content-Length` 61 626 644). + +Damit ist das die wahrscheinlichste Stelle für den ersten Fehlschlag auf dieser +Infrastruktur, und zwar aus einem konkreten, bereits einmal bezahlten Grund. Im +README des ersten Abnehmers steht gemessen: Auf diesem Runner kam einmal eine +**17-MB-Datei nicht durch**, weil eine Docker-Brücke mit MTU 1500 gegen ein +Pod-Netz mit 1450 lief. Kleine Antworten passten in ein Paket, die große nicht — +und es sah aus wie ein gesperrter Host, nicht wie ein MTU-Problem. Behoben wurde +das auf der Knoten-Seite, nicht in einem Workflow. + +Der Scanner-CLI ist ein Mehrfaches dieser 17 MB. Wenn ein Lauf dieses Bausteins +beim Download hängt oder abbricht, ist das die erste Spur, die man verfolgt — +und man sucht sie auf der Knoten-Seite, nicht im YAML. + +Zwei weitere Folgen des Laufzeit-Downloads, nebenbei: Der Baustein hängt an der +Erreichbarkeit eines fremden Hosts, und er hängt an einer Signaturprüfung. Beide +können unabhängig von allem hier scheitern. + +## Die Adresse ist eine Variable, das Token ein Secret + +`SONAR_HOST_URL` ist eine **Variable** des aufrufenden Repositories, +`SONAR_TOKEN` ein **Secret**. Die Trennung ist keine Formsache: + +Die Adresse des Servers ist kein Zugangsdatum. Als Variable bleibt sie im +Protokoll lesbar — und erst das macht eine falsche Adresse überhaupt +diagnostizierbar. Als Secret wäre sie maskiert, und ein Fehler gegen eine +maskierte Adresse ist nicht zu deuten; genau diese Lage hat bei +[publish-helm-chart](publish-helm-chart) einen eigenen Maskierungsschritt +erzwungen. + +Beides gehört dem Aufrufer und nicht diesem Repository. Ein geteilter Baustein +trägt weder die Infrastruktur-Adresse eines einzelnen Mandanten noch dessen +Token — dieselbe Hausregel, aus der bei den anderen Bausteinen der +Eigentümer-Zwang folgt. + +## Der Wächter, und was er nicht fängt + +Der Preis der vorigen Entscheidung ist eine Voraussetzung, die man vergessen +kann, und eine nicht gesetzte Variable kommt als **leere Zeichenkette** an. Eine +leere Adresse scheitert nicht dort, wo sie gesetzt wird, sondern einen Schritt +später in der Scan-Action, gegen eine Adresse, die niemand sieht. Deshalb steht +der Wächter zuerst und benennt die Einstellung, den Ort und die erwartete Form. +Dasselbe Muster wie bei `OP_CONNECT_HOST` in +[publish-helm-chart](publish-helm-chart) — es ist inzwischen die Hausform. + +Der Wächter prüft **beide** Werte, bevor er abbricht. Ein Repository, das keinen +von beiden eingerichtet hat, erfährt das sonst in zwei Läufen statt in einem, +und der zweite kostet genauso viel Aufmerksamkeit wie der erste. + +Beim Token fängt er nur zwei der drei Fälle: ein vorhandenes, aber leeres +Secret, und einen Aufrufer, der `secrets: inherit` vergessen hat. Beide kommen +als leere Zeichenkette an. Ein **ganz fehlendes** Secret kommt nie so weit — der +Workflow deklariert es als `required`, und ein fehlendes Pflicht-Secret lässt auf +dieser Instanz die **Job-Erzeugung** scheitern, nicht den Job; der Lauf steht +dann ohne Schritte da. Nachzulesen unter +[Gemessenes-Verhalten](Gemessenes-Verhalten). + +Dass `required: true` hier trotzdem richtig ist, folgt der dort aufgeschriebenen +Linie: **Jeder** Aufrufer braucht das Token, ohne Token gibt es keine Analyse. +Ein Lauf ohne Schritte ist dann der verständliche Fall. `required: false` wäre +nur richtig, wenn ein Teil des Bausteins ohne das Secret arbeiten könnte — hier +tut keiner das. + +## Gepinnt auf Commits, nicht auf Zweige + +Beide Actions sind auf einen Commit gepinnt, nicht auf einen Zweig und nicht auf +eine Hauptversion. Begründung und der Fall, an dem sie hängt, stehen unter +[Gemessenes-Verhalten](Gemessenes-Verhalten), Abschnitt „Wandernde +Action-Verweise": Ein beweglicher Verweis holt bei jedem Lauf den neuesten Stand +und ändert das Verhalten aller Aufrufer, ohne dass hier etwas angefasst wurde. + +## Was an diesem Baustein nicht gemessen ist + +Diese Liste ist der eigentliche Grund für die Seite. Sie steht hier, damit +niemand sie zweimal zusammentragen muss, und sie gehört abgearbeitet, sobald der +erste Lauf da ist — die Ergebnisse dann auf +[Gemessenes-Verhalten](Gemessenes-Verhalten). + +1. **Ist `node24` im Runner-Image vorhanden?** Die Scan-Action verlangt es. Wenn + nicht, scheitert sie — und zwar mit einer Meldung über die Action, nicht über + das Image. Wer das ohne diese Zeile liest, sucht den Fehler in der Action. +2. **Kommt der Scanner-Download durch?** Siehe oben; die wahrscheinlichste + Stelle, und eine mit Vorgeschichte. +3. **Kommt `vars.SONAR_HOST_URL` in einem aus einem fremden Repository + aufgerufenen Workflow an?** Dass der Variablen-Kontext der des Aufrufers + bleibt, ist dokumentiert, auf dieser Forge-Fassung aber nicht gemessen — + dasselbe Offene wie bei `OP_CONNECT_HOST` in + [publish-helm-chart](publish-helm-chart), wo es ebenfalls noch unbelegt ist. + Wenn es nicht ankommt, ist das immerhin laut: Der Wächter greift und benennt + die Variable. Das Gegenstück dort hat sich für die andere Richtung + entschieden — der Eigentümer ist eine **Eingabe**, weil er im Aufrufer + ausgewertet und nicht auf eine Kontext-Semantik gesetzt wird. Käme `vars` + nicht an, wäre dieselbe Umstellung auch hier der Weg: die Adresse als + Eingabe. +4. **Was der erste Lauf überhaupt meldet**, insbesondere aus Helm-Templates. Der + Helm-Analysator trifft dort auf Dateien, die wegen `{{ }}` kein gültiges YAML + sind. Was er damit macht, bekommt man nur heraus, indem man ihn einmal + darauf laufen lässt — deshalb beginnt der erste Abnehmer **ohne Ausschlüsse**. + Das Ergebnis entscheidet, ob Ausschlüsse nachträglich nötig sind, und es + gehört in die `sonar-project.properties` des jeweiligen Repositories, nicht + in diesen Baustein.