diff --git a/.gitea/workflows/sonar-analysis.yml b/.gitea/workflows/sonar-analysis.yml new file mode 100644 index 0000000..dcb3b31 --- /dev/null +++ b/.gitea/workflows/sonar-analysis.yml @@ -0,0 +1,145 @@ +name: SonarQube Analysis + +# Called through "uses:" only, never triggered here - there is nothing in this +# repository worth analysing, and a dispatch here would run against these files. +# +# The caller decides WHEN the analysis runs, through its own "on:". On the +# SonarQube Community Build only the main branch is analysed, so that is in +# practice a push to main, and the consequence belongs said out loud: a finding +# appears AFTER the merge. This block observes a state, it does not guard a +# gate - see the note on the missing quality-gate step at the bottom. +on: + workflow_call: + inputs: + # Both inputs are type: string, and that is not an oversight waiting to be + # cleaned up. On this Gitea version (measured on 1.27.0, issue #4) a + # boolean workflow_call input does not reach the called workflow at all - + # neither its default nor an explicitly passed value - while the run stays + # green. Every input of every block here is therefore a string, even one + # that is a switch in meaning. Whoever "fixes" that later brings the fault + # back silently. + args: + required: false + type: string + default: "" + description: >- + Additional arguments for the Sonar Scanner CLI, passed through to the + scan action's "args" input. Everything project-specific belongs in the + sonar-project.properties of the calling repository, not here; this is + for the rest, e.g. -Dsonar.verbose=true for one diagnostic run. + # The scan action's own default for projectBaseDir is ".", and this input + # repeats it instead of defaulting to the empty string: an empty value + # would be passed on and would override the action's default rather than + # leave it alone. + project-base-dir: + required: false + type: string + default: "." + description: >- + Directory the analysis starts from, i.e. sonar.projectBaseDir. The + sonar-project.properties is looked up relative to it. + secrets: + # required: true, and that is a real decision on this forge, not a + # formality: a missing secret that the called workflow declares as + # required fails JOB CREATION, not the job - the run then stands there + # with no steps at all and nothing in it names the cause. Measured in + # jlab-cluster/helm-charts, written up on the wiki page + # "Gemessenes-Verhalten". + # + # It is the right setting here all the same, because every caller needs + # the token: without it there is no analysis to run, so a run without + # steps is the understandable case - exactly this prerequisite is missing. + # (required: false would be right only if a part of the block worked + # without it.) + SONAR_TOKEN: + required: true +jobs: + sonar-analysis: + name: SonarQube Analysis + # ubuntu-latest is enough, and that is worth a line because the sibling + # block docker-build-push needs the ubuntu-docker runner: the scan action + # declares "runs: using: node24", so it is a JavaScript action and runs in + # the runner's Node, not in a container. No Docker socket is involved, and + # ubuntu-docker is the only runner here that has one. + # + # What it does need is node24 in the runner image. That is NOT verified; if + # it is absent the action fails, and it fails with a message about the + # action rather than about the image. + runs-on: ubuntu-latest + steps: + # Both settings belong to the CALLING repository, not to this file: a + # shared block does not carry one tenant's infrastructure address, and it + # certainly does not carry anyone's token. The price of that is two + # prerequisites that can be forgotten, and an unset variable arrives as an + # empty string. + # + # An empty SONAR_HOST_URL does not fail here - it fails one step later, + # inside the scan action, against an address nobody can see. So the guard + # comes first and names the setting, the place to set it and the shape of + # the value. The publish block needed the same pattern for + # OP_CONNECT_HOST. + # + # The token is checked in the same step even though a MISSING one never + # gets this far (see the secrets block above): what this catches is a + # secret that exists and is empty, and a caller that forgot + # "secrets: inherit". Both arrive as an empty string, both would otherwise + # surface as an unauthenticated request against the server. + # + # Both are checked before either exits, so a repository that has set up + # neither learns both in one run instead of in two. + - name: "Check the calling repository's prerequisites" + env: + SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + run: |- + missing=0 + if [ -z "$SONAR_HOST_URL" ]; then + echo "::error::the variable SONAR_HOST_URL is not set in the calling repository (Settings -> Actions -> Variables, or on the organisation). It must hold the address of the SonarQube server, e.g. https://sonar.example.com - no trailing path. This workflow deliberately does not hardcode a host." + missing=1 + fi + if [ -z "$SONAR_TOKEN" ]; then + echo "::error::the secret SONAR_TOKEN is empty in the calling repository (Settings -> Actions -> Secrets, or on the organisation), or the caller did not pass it on with 'secrets: inherit'. It must hold a SonarQube token permitted to run analyses - and to create the project, if the first run is supposed to create it." + missing=1 + fi + [ "$missing" -eq 0 ] || exit 1 + # fetch-depth: 0, and this is the one checkout option the analysis cannot + # do without. With the default shallow clone the scanner has no history to + # attribute lines to, so every finding is reported against the last commit + # instead of the change that introduced it: blame, the "new code" period + # and every date-based measure become unusable. The clone is slower, which + # is the whole price. + # + # Pinned to a commit, not to a tag or a branch: a moving reference pulls + # the newest state into every run of every caller, and the resulting + # failure shows up at a caller that changed nothing (issue #5). + - name: "Checkout Repository" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + with: + fetch-depth: 0 + # The action downloads the Sonar Scanner CLI from binaries.sonarsource.com + # at run time and verifies its GPG signature. That download is around + # 59 MiB, and it is the most likely place for a first failure on this + # infrastructure - the wiki page for this block says why. + - name: "SonarQube Scan" + uses: SonarSource/sonarqube-scan-action@7006c4492b2e0ee0f816d36501671557c97f5995 # v8.1.0 + env: + SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} + # A variable of the caller, not a secret: the address of the server is + # not a credential, and as a variable it stays readable in the log, + # which is what makes a wrong one diagnosable at all. + SONAR_HOST_URL: ${{ vars.SONAR_HOST_URL }} + with: + args: ${{ inputs.args }} + projectBaseDir: ${{ inputs.project-base-dir }} + # There is deliberately NO quality-gate step here, and it is missing on + # purpose rather than by oversight. + # + # With an after-the-merge trigger there is nothing left to block. A red + # gate would only colour the main branch red, and a permanent red is a + # thing people learn to ignore - at which point the gate has cost the + # credibility of every other check without having stopped anything. + # + # It becomes the right thing under one of two conditions: pull-request + # analysis is available (then the gate blocks a merge, which is a decision + # it can actually carry), or a deployment hangs off this workflow (then it + # guards something concrete). Until then, adding it is a regression. diff --git a/README.md b/README.md index a9b472d..43dec97 100644 --- a/README.md +++ b/README.md @@ -269,6 +269,91 @@ Der Registry-Zugang selbst liegt in 1Password (`GitHub-Actions/jlab-registry-access`: `username`, `password`, `charts-url`) und muss in Gitea nicht hinterlegt werden. +### `.gitea/workflows/sonar-analysis.yml` + +Schickt den Baum des aufrufenden Repositories durch eine **SonarQube-Analyse**. +Der Baustein checkt mit vollständiger Historie aus und ruft dann die offizielle +Scan-Action auf; alles Projektspezifische — Projektschlüssel, Ausschlüsse, +Quellpfade — steht in der `sonar-project.properties` des aufrufenden +Repositories, nicht hier. Läuft auf `ubuntu-latest`. + +**Die Analyse ist kein Torwächter.** Es gibt bewusst keinen +Quality-Gate-Schritt: Auf der SonarQube Community Build wird nur der Hauptzweig +analysiert, der Auslöser liegt damit **nach** dem Merge, und dort ist nichts +mehr zu blockieren. Ein rotes Gate färbte nur `main` rot, und einen Dauerzustand +lernt man zu ignorieren. Sinnvoll wird es erst, wenn PR-Analyse verfügbar ist +oder ein Deployment an diesem Workflow hängt — bis dahin ist ein Gate hier eine +Verschlechterung und keine vergessene Zeile. Ein Befund ist eine Beobachtung des +Zustands, und wer ihn sehen will, schaut in SonarQube. + +#### Eingaben + +| Eingabe | Pflicht | Vorgabe | Bedeutung | +| ------------------ | ------- | ------- | --------- | +| `args` | nein | — | Zusätzliche Argumente für den Sonar-Scanner-CLI, unverändert an die Scan-Action weitergegeben. Für einen einmaligen Diagnoselauf, etwa `-Dsonar.verbose=true` | +| `project-base-dir` | nein | `.` | Verzeichnis, in dem die Analyse beginnt (`sonar.projectBaseDir`); die `sonar-project.properties` wird relativ dazu gesucht | + +Beide Eingaben sind `type: string`. Das ist kein Versehen: Boolesche +`workflow_call`-Eingaben kommen auf dieser Gitea-Version nicht an — bei grünem +Lauf. Siehe +[Issue #4](https://git.jk-dev.de/workflow-util/pipelines/issues/4) und die +Begründung im Abschnitt zu `push` oben. + +Die Vorgabe von `project-base-dir` ist `.` und nicht leer, weil die Scan-Action +selbst `.` als Vorgabe hat: Ein leerer Wert würde durchgereicht und überschriebe +diese Vorgabe, statt sie stehen zu lassen. + +#### Aufrufbeispiel + +```yaml +name: SonarQube + +on: + push: + branches: [main] + +jobs: + sonar: + uses: workflow-util/pipelines/.gitea/workflows/sonar-analysis.yml@v4 + secrets: inherit +``` + +`secrets: inherit` ist notwendig — der Workflow braucht `SONAR_TOKEN` aus dem +aufrufenden Repository. + +Der Baustein löst sich nicht selbst aus. Wann analysiert wird, entscheidet die +Hülle im aufrufenden Repository über ihr eigenes `on:` — üblicherweise ein +`push` auf `main`, weil die Community Build ausschließlich den Hauptzweig +analysiert. + +#### Voraussetzungen im aufrufenden Repository + +| Name | Art | Zweck | +| ------------------------- | ------------------- | ----- | +| `SONAR_HOST_URL` | Variable | Adresse des SonarQube-Servers, etwa `https://sonar.example.com` — ohne Pfad dahinter | +| `SONAR_TOKEN` | Secret | Token mit dem Recht, Analysen auszuführen; soll das Projekt beim ersten Lauf selbst entstehen, zusätzlich das Recht zum Anlegen. Kommt über `secrets: inherit` | +| `sonar-project.properties` | Datei im Repository | Trägt mindestens `sonar.projectKey`. Der Schlüssel sollte den Eigentümer enthalten (`_`), weil die Instanz mehrere Eigentümer bedient und ein nackter Repository-Name beim zweiten kollidiert | + +Dass die Adresse eine **Variable** ist und kein Secret, ist Absicht: Sie ist +kein Zugangsdatum, und als Variable bleibt sie im Protokoll lesbar — erst das +macht eine falsche Adresse überhaupt diagnostizierbar. + +Beides gehört dem aufrufenden Repository, nicht diesem Baustein — ein geteilter +Baustein trägt weder die Infrastruktur-Adresse eines einzelnen Mandanten noch +dessen Token. Der Preis sind zwei Voraussetzungen, die man vergessen kann, und +eine nicht gesetzte Variable kommt als leere Zeichenkette an. Deshalb prüft der +erste Schritt beide und benennt beim Abbruch die Einstellung samt Ort; geprüft +wird vollständig, bevor abgebrochen wird, damit ein Repository ohne beide das in +einem Lauf erfährt und nicht in zwei. + +Für `SONAR_TOKEN` fängt der Wächter allerdings nur den Fall eines leeren oder +nicht durchgereichten Secrets. Ein **ganz fehlendes** kommt nie so weit: Der +Workflow deklariert es als `required`, und ein fehlendes Pflicht-Secret lässt auf +dieser Instanz die Job-Erzeugung scheitern — der Lauf steht dann ohne Schritte +da. Das ist hier der verständliche Fall, denn ohne Token gibt es keine Analyse; +nachzulesen im Wiki unter +[Gemessenes-Verhalten](https://git.jk-dev.de/workflow-util/pipelines/wiki/Gemessenes-Verhalten). + ## Auf einen Tag zeigen, nicht auf `@main` Aufrufe bitte immer an einen Tag binden: @@ -294,15 +379,17 @@ stehen; wer umstellt, wechselt auf `@v2` und schreibt `push: 'true'` bzw. ### Die Nummer bezeichnet den Zustand des Repositories Tags gelten hier **repository-weit**, nicht je Datei. `v3` ist der Stand, ab dem -es `publish-helm-chart.yml` gibt; unter `v1` und `v2` existiert die Datei nicht, -ein Aufruf auf `@v2` findet sie also nicht. +es `publish-helm-chart.yml` gibt, `v4` der Stand, ab dem es +`sonar-analysis.yml` gibt; unter `v1` und `v2` existiert keine der beiden +Dateien, ein Aufruf auf `@v2` findet sie also nicht. Das hat eine Folge, die man kennen muss, weil sie sonst als Fehler gelesen wird: -Wer künftig `docker-build-push.yml` ändert, erhöht auf `v4` — und ein Aufruf von +Wer künftig `docker-build-push.yml` ändert, erhöht auf `v5` — und ein Aufruf von `publish-helm-chart.yml@v3` bleibt gültig und richtig, obwohl sich an dieser Datei nichts geändert hat. Die Nummer ist keine Dateiversion. Umgekehrt zwingt eine brechende Änderung an einem Baustein die Aufrufer der anderen zu nichts: Sie bleiben auf ihrem Tag stehen, bis sie bewusst nachziehen. -`v3` selbst ist additiv. Der neue Baustein ändert an `docker-build-push.yml` -nichts, bestehende Aufrufer auf `@v2` bleiben unberührt. +`v3` und `v4` sind beide additiv. Sie nehmen je einen Baustein hinzu und ändern +an den vorhandenen nichts; bestehende Aufrufer auf `@v2` bzw. `@v3` bleiben +unberührt.