4 Commits
Author SHA1 Message Date
jlab-renovate-bot 6afd9523b1 Add renovate.json 2026-10-06 09:04:19 +00:00
jklein 0f7cd04c70 SonarQube-Analyse als geteilten Baustein aufnehmen
Neuer Baustein sonar-analysis.yml, on: workflow_call. Ein Waechter fuer die
Voraussetzungen des Aufrufers, Checkout mit vollstaendiger Historie, dann die
offizielle Scan-Action. Beide Actions auf einen Commit gepinnt.

Projektspezifisches steht in der sonar-project.properties des aufrufenden
Repositories, nicht hier - damit ist der Workflow fuer jedes Repository
identisch und ein zweiter Abnehmer kostet eine Datei mit drei Zeilen.

Kein Quality-Gate-Schritt, und das absichtlich: Die Community Build analysiert
nur den Hauptzweig, der Ausloeser liegt damit nach dem Merge, und dort ist
nichts mehr zu blockieren. Ein rotes main ohne Wirkung nimmt die
Glaubwuerdigkeit der uebrigen Pruefungen mit. Die zwei Bedingungen, unter denen
ein Gate richtig waere, stehen im README und als Kommentar an der Stelle, an
der der Schritt stuende.

SONAR_HOST_URL ist eine Variable des Aufrufers, SONAR_TOKEN sein Secret. Die
Adresse bleibt als Variable im Protokoll lesbar - erst das macht eine falsche
Adresse diagnostizierbar. Der Waechter prueft beide, bevor er abbricht, damit
ein Repository ohne beide das in einem Lauf erfaehrt.

Beide Eingaben sind type: string, auch project-base-dir: Boolesche
workflow_call-Eingaben kommen auf dieser Gitea-Version nicht an (Issue #4), und
die Regel gilt fuer jede Eingabe hier, damit sie nicht je Datei neu entschieden
wird.

Die Langform - warum fetch-depth: 0 nicht optional ist, warum kein
Docker-Runner noetig ist, was der Scanner zur Laufzeit holt und was an diesem
Baustein noch nicht gemessen ist - steht im Wiki unter sonar-analysis.

Der Baustein ist ab v4 verfuegbar; die Tag-Nummer bezeichnet wie bisher den
Zustand des Repositories und nicht die Version einer Datei.
2026-10-01 23:33:05 +00:00
jklein 6123f469f6 README auf das Wiki verweisen
Das Wiki dieses Repositories ist jetzt angelegt und traegt die Langform zu
beiden Bausteinen. Ohne einen Verweis von hier aus erfaehrt davon niemand, der
nur das README liest.

Die Aufteilung steht dabei gleich mit: Diese Datei bleibt die Kurzreferenz fuer
Aufrufer und wird nicht gekuerzt, das Wiki begruendet sie.

Part of jlab-cluster/helm-charts#66
2026-10-01 22:44:52 +00:00
jklein 7d0c1b9c22 Helm-Chart-Publish als geteilten Baustein aufnehmen
Der Workflow kommt aus jlab-cluster/helm-charts, wo er 26 duenne Huellen
bedient. Er laedt ein gepacktes Chart in eine klassische, indexbasierte
Helm-Registry: ein einzelnes HTTP-POST, kein OCI-helm-push.

Zwei Dinge sind beim Umzug anders als in der Vorlage.

Der Eigentuemer ist eine Pflichteingabe registry-owner vom Typ string, ohne
Vorgabe. Bisher wurde er aus github.repository_owner abgeleitet; als Eingabe
wird er im aufrufenden Repository ausgewertet, wo der Wert gemessen ist. Das
folgt der Hausregel, dass ein Eigentuemer-Segment nie geraten wird, und haengt
den Baustein an keine Kontext-Semantik. type: string wie alle Eingaben hier,
siehe #4.

OP_CONNECT_HOST kommt aus einer Variablen des aufrufenden Repositories statt
fest verdrahtet aus dem Baustein — ein geteilter Baustein traegt nicht die
Infrastruktur-Adresse eines einzelnen Mandanten. Ein erster Schritt prueft die
Variable und benennt sie beim Abbruch, statt zwei Schritte spaeter an einer
leeren Adresse zu scheitern.

Alles andere ist unveraendert uebernommen, samt der Kommentare, die es
begruenden: curl --config - statt --user, --fail-with-body, der gebaute
Dateiname mit test -f, die :?-Waechter je Tresor-Feld, die Leerzeichen-Pruefung
an den Zugangsdaten, das Maskieren des Hosts und die drei Formwaechter an
charts-url.

Der workflow_dispatch-Block ist entfallen: Ein Dispatch hier liefe gegen dieses
Repository, in dem kein Chart liegt. Die Huellen des Aufrufers tragen ihn.

ci/helm-dependencies.sh bleibt ein Skript des aufrufenden Repositories; der
Checkout holt dessen Baum, also laeuft es weiter aus dessen Kopie.

Part of jlab-cluster/helm-charts#66
2026-10-01 22:39:17 +00:00
4 changed files with 624 additions and 0 deletions

No files matched your search

+249
View File
@@ -0,0 +1,249 @@
name: Publish Helm Chart with patched Application Version
# Called through "uses:" only, never triggered here. The thin wrappers in the
# calling repository carry their own "workflow_dispatch" and "on: push", so the
# workflow_dispatch block this file used to have is gone: a dispatch in THIS
# repository would run against this repository's files, where there is no chart.
#
# The comments below are kept from the copy this file replaces, in English as
# they were written. Each of them records something that was paid for once -
# none of it is restating what the YAML already says.
on:
workflow_call:
inputs:
chart-location:
required: false
type: string
default: "."
description: "location of the helm chart directory"
chart-name:
required: true
type: string
description: "name of the helm chart"
# Deliberately required and without a default, following the house rule
# of this repository: an owner segment is never guessed. The caller
# passes "${{ github.repository_owner }}", where the value is measured
# (jlab-cluster, run 282 of validate-charts.yml) - inside a workflow
# called from another repository it is documented to stay the caller's
# owner, but that is not measured on this Gitea version, and a published
# chart landing under the wrong owner is not worth relying on a context
# semantic for. As an input the value is evaluated in the caller, where
# it is known to be right.
#
# type: string, like every input here. 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 this block is
# therefore a string, even one that is a switch in meaning.
registry-owner:
required: true
type: string
description: >-
Owner segment of the chart registry URL, i.e. the <owner> in
<base>/<owner>/helm/api/charts. Deliberately has no default: an owner
segment is never guessed. Callers pass github.repository_owner, which
resolves in the calling repository, where the value is measured.
outputs:
chart-version:
description: "the version of the published helm chart"
value: ${{ jobs.publish-helm-chart.outputs.chart-version }}
secrets:
OP_CONNECT_TOKEN:
required: true
jobs:
publish-helm-chart:
outputs:
chart-version: ${{ steps.get-chart-version.outputs.CHART_VERSION }}
runs-on: ubuntu-latest
steps:
- name: "Checkout Repository"
uses: actions/checkout@v7
- name: "Install Helm"
uses: azure/setup-helm@v5
# The Connect host is a variable of the CALLING repository, not a value
# wired into this file: a shared building block must not carry one
# tenant's infrastructure address. The price of that is a prerequisite
# that can be forgotten, and an unset variable arrives as an empty
# string. An empty connect-host does not fail here - it fails two steps
# later, inside the loading action, against a URL nobody can see. So the
# guard comes first and names the setting, the place to set it and the
# shape of the value.
- name: "Check the calling repository's prerequisites"
env:
OP_CONNECT_HOST: ${{ vars.OP_CONNECT_HOST }}
run: |-
if [ -z "$OP_CONNECT_HOST" ]; then
echo "::error::the variable OP_CONNECT_HOST is not set in the calling repository (Settings -> Actions -> Variables). It must hold the address of the 1Password Connect server, e.g. https://op-connect.example.com. This workflow deliberately does not hardcode a host."
exit 1
fi
- name: "Configure OP-Connect"
uses: 1password/load-secrets-action/configure@v5
with:
connect-host: ${{ vars.OP_CONNECT_HOST }}
# helm package exits 1 with "found in Chart.yaml, but missing in charts/
# directory" while a declared dependency has not been fetched, so this
# comes first. It is a no-op for every chart without a dependencies:
# block; ci/helm-dependencies.sh explains why it resolves from Chart.yaml
# instead of a committed Chart.lock. Note the consequence for the
# published artifact: the subchart .tgz is packaged INTO ours, so the
# version pinned in Chart.yaml is the version operators get, and nothing
# downstream ever sees the dependency again.
#
# The script belongs to the CALLING repository and that is unchanged by
# the move: the checkout above clones the repository the run belongs to -
# the caller - so ${{ github.workspace }} is the caller's tree and the
# script is the caller's copy. It is therefore a prerequisite of every
# caller, the same way OP_CONNECT_HOST is, and the guard below says so
# rather than letting the shell report a missing file.
- name: "Fetch chart dependencies"
env:
HELM_DEPENDENCIES_SCRIPT: ${{ github.workspace }}/ci/helm-dependencies.sh
CHART_NAME: ${{ inputs.chart-name }}
run: |-
test -x "$HELM_DEPENDENCIES_SCRIPT" || {
echo "::error::${HELM_DEPENDENCIES_SCRIPT} is missing or not executable - ci/helm-dependencies.sh is a script of the calling repository and this workflow runs it before helm package"
exit 1
}
"$HELM_DEPENDENCIES_SCRIPT" "$CHART_NAME"
working-directory: ${{ inputs.chart-location }}
- name: "Package Helm Chart"
run: helm package ${{ inputs.chart-name }}
working-directory: ${{ inputs.chart-location }}
- name: "Get Helm Chart Version"
id: get-chart-version
run: |-
CHART_VERSION=$(grep '^version:' ${{ inputs.chart-name}}/Chart.yaml | awk '{print $2}')
echo $CHART_VERSION
echo "CHART_VERSION=$CHART_VERSION" >> $GITHUB_OUTPUT
working-directory: ${{ inputs.chart-location }}
- name: "Load Chart Registry Credentials"
id: load-registry-creds
uses: 1password/load-secrets-action@v5
env:
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
REGISTRY_PASSWORD: op://GitHub-Actions/jlab-registry-access/password
REGISTRY_USERNAME: op://GitHub-Actions/jlab-registry-access/username
REGISTRY_CHARTS_URL: op://GitHub-Actions/jlab-registry-access/charts-url
# The target is a classic, index-based Helm registry, not an OCI one, so
# there is no "helm registry login" session and no "helm push": the upload
# is a single HTTP POST of the packaged .tgz to
# <charts-url>/<owner>/helm/api/charts, authenticated per request.
#
# That URL is composed, not stored (jlab-cluster/helm-charts#63).
# charts-url holds only the base up to "/api/packages"; the owner segment
# comes from the registry-owner input, which the caller fills with
# github.repository_owner - measured as "jlab-cluster" in run 282 of
# validate-charts.yml, on a temporary step that was removed again. So no
# value pointing at the registry - not even a path segment - lives in the
# calling repository, and a second owner on the same instance needs no
# second setting. Two guards make the switch from the old shape loud
# instead of puzzling: charts-url is rejected unless it ends in
# "/api/packages" (an old full URL still carrying "/helm", "/api/charts"
# or the owner is named explicitly), and the owner is rejected when empty
# or when it holds characters that are not a path segment - an empty one
# would build "<base>//helm/api/charts", i.e. another bare 404 against a
# masked URL.
#
# Three further properties of this step are load-bearing and each of them
# was paid for once already.
#
# 1) The values are mapped into the step's own env block and read as quoted
# shell variables - never interpolated into the command line. load-secrets-
# action v5 no longer exports the loaded values as environment variables
# for later steps the way v4 did; only the step outputs carry them. Every
# "${{ env.* }}" reference therefore resolved to an empty string from that
# bump on, with nothing reporting an error - the loading step logs
# "Populating variable" and stays green either way. The ":?" guards fail
# loudly and name the op:// field that is missing, and quoting keeps an
# empty value from silently shifting arguments.
#
# 2) The credentials never reach the process list. "curl --user $U:$P"
# would put them in argv; "curl --config -" reads its options from stdin
# instead, so nothing sensitive appears in any argument. The price of that
# config file is its value syntax: an unquoted value ends at the first
# space (measured on curl 8.5.0 - "has space" arrived as "has"), so the
# step refuses whitespace in the credentials up front rather than letting
# a truncated password come back as a puzzling 401. A Gitea access token
# is 40 hex characters, so this only ever fires on a mis-entered item.
#
# 3) "--fail-with-body" is what makes a failed upload fail the job. Unlike
# "helm push", curl exits 0 on HTTP 401/403/409 unless asked not to - the
# publish would go green while nothing was uploaded. Measured against Gitea
# 1.24.7: a 401 without the flag is exit 0, with the flag exit 22, and the
# response body is still printed so the log names the cause.
#
# The file name is built from the chart name and the version read above
# instead of a "<chart>-*.tgz" glob: --upload-file takes exactly one file,
# and a glob matching two turns the second into curl's URL argument, while
# a glob matching none is passed through literally. Both fail obscurely.
# "helm package" keeps the "+" of a "1.4.0+up1.9.2" version verbatim in the
# file name, so "<name>-<version>.tgz" is an exact match (measured).
- name: "Upload Helm Chart to the Chart Registry"
env:
REGISTRY_PASSWORD: ${{ steps.load-registry-creds.outputs.REGISTRY_PASSWORD }}
REGISTRY_USERNAME: ${{ steps.load-registry-creds.outputs.REGISTRY_USERNAME }}
REGISTRY_CHARTS_URL: ${{ steps.load-registry-creds.outputs.REGISTRY_CHARTS_URL }}
REGISTRY_OWNER: ${{ inputs.registry-owner }}
CHART_NAME: ${{ inputs.chart-name }}
CHART_VERSION: ${{ steps.get-chart-version.outputs.CHART_VERSION }}
run: |-
: "${REGISTRY_CHARTS_URL:?empty - check op://GitHub-Actions/jlab-registry-access/charts-url}"
: "${REGISTRY_USERNAME:?empty - check op://GitHub-Actions/jlab-registry-access/username}"
: "${REGISTRY_PASSWORD:?empty - check op://GitHub-Actions/jlab-registry-access/password}"
: "${REGISTRY_OWNER:?empty - the registry-owner input did not arrive; the caller passes it from github.repository_owner, do not hardcode an owner}"
: "${CHART_NAME:?empty - the chart-name input did not arrive}"
: "${CHART_VERSION:?empty - could not read version: from ${CHART_NAME}/Chart.yaml}"
# 1Password masks the full URL, but a curl connection error prints only
# "<host>:<port>", which that mask would not cover. Mask the host too.
CHARTS_HOST="${REGISTRY_CHARTS_URL#*://}"
echo "::add-mask::${CHARTS_HOST%%/*}"
# charts-url is the base only; this workflow appends the owner, the
# package type and the endpoint. Reject the old, full shape by name so
# the failure says what to change in the vault item instead of 404ing
# against a URL the log has masked.
CHARTS_BASE="${REGISTRY_CHARTS_URL%/}"
case "$CHARTS_BASE" in
*/api/charts)
echo "::error::op://GitHub-Actions/jlab-registry-access/charts-url still ends in /api/charts - it must hold only the base up to /api/packages (https://<host>/api/packages); this workflow appends /<owner>/helm/api/charts itself"
exit 1
;;
*/helm|*/helm/*)
echo "::error::op://GitHub-Actions/jlab-registry-access/charts-url still contains the /helm path segment - it must hold only the base up to /api/packages (https://<host>/api/packages); this workflow appends /<owner>/helm/api/charts itself"
exit 1
;;
*/api/packages) ;;
*)
echo "::error::op://GitHub-Actions/jlab-registry-access/charts-url must end in /api/packages (https://<host>/api/packages) - no owner, no package type, no endpoint; this workflow appends /<owner>/helm/api/charts itself"
exit 1
;;
esac
# An empty owner would build "<base>//helm/api/charts", a stray "/" or
# "?" would build something else entirely - both are bare 404s.
case "$REGISTRY_OWNER" in
*[!A-Za-z0-9._-]*)
echo "::error::the registry-owner input is not usable as a URL path segment: '${REGISTRY_OWNER}'"
exit 1
;;
esac
CHARTS_API_URL="${CHARTS_BASE}/${REGISTRY_OWNER}/helm/api/charts"
case "${REGISTRY_USERNAME}${REGISTRY_PASSWORD}" in
*[[:space:]]*)
echo "::error::username/password in op://GitHub-Actions/jlab-registry-access must not contain whitespace - curl --config truncates an unquoted value at the first space"
exit 1
;;
esac
CHART_PACKAGE="${CHART_NAME}-${CHART_VERSION}.tgz"
test -f "$CHART_PACKAGE" || {
echo "::error::${CHART_PACKAGE} does not exist - helm package produced: $(ls -1 ./*.tgz 2>&1)"
exit 1
}
echo "uploading ${CHART_PACKAGE}"
printf 'user = %s:%s\nurl = %s\n' \
"$REGISTRY_USERNAME" "$REGISTRY_PASSWORD" "$CHARTS_API_URL" |
curl --config - \
--fail-with-body \
--silent --show-error \
--request POST \
--upload-file "$CHART_PACKAGE" \
--write-out '\nHTTP %{http_code}\n'
working-directory: ${{ inputs.chart-location }}
+145
View File
@@ -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.
+224
View File
@@ -4,6 +4,12 @@ Geteilte CI/CD-Bausteine für die Gitea-Instanz `git.jk-dev.de`. Die Workflows
hier werden nicht selbst ausgelöst, sondern von anderen Repositories über
`uses:` aufgerufen (`on: workflow_call`).
Diese Datei ist die **Kurzreferenz für Aufrufer**: was ein Baustein tut, seine
Eingaben, ein Aufrufbeispiel, die Voraussetzungen. Wer einen Aufruf schreibt,
braucht nur das. Die Langform — warum die Dinge so sind und was dafür gemessen
wurde — steht im [Wiki](https://git.jk-dev.de/workflow-util/pipelines/wiki),
eine Seite je Baustein und eine für die Eigenheiten dieser Forge.
## Bausteine
### `.gitea/workflows/docker-build-push.yml`
@@ -148,6 +154,206 @@ Die eigentlichen Zugänge für Checkout und Registry liegen in 1Password
(`GitHub-Actions/jlab-git-checkout-token`, `GitHub-Actions/jlab-registry-access`)
und müssen in Gitea nicht hinterlegt werden.
### `.gitea/workflows/publish-helm-chart.yml`
Packt ein Helm-Chart und lädt es in eine **klassische, indexbasierte**
Helm-Registry. Das ist kein OCI-`helm push`: Es gibt keine Registry-Sitzung,
sondern ein einzelnes HTTP-`POST` des gepackten `.tgz`, je Anfrage
authentifiziert. Läuft auf `ubuntu-latest`.
Die Ziel-URL wird zusammengesetzt, nicht gespeichert:
```
<charts-url>/<registry-owner>/helm/api/charts
```
`<charts-url>` ist die Basis bis `/api/packages` und liegt in 1Password
(`GitHub-Actions/jlab-registry-access`, Feld `charts-url`), `<registry-owner>`
ist die Pflichteingabe. Damit steht im aufrufenden Repository kein Wert, der auf
die Registry zeigt — nicht einmal ein Pfadsegment —, und ein zweiter Eigentümer
auf derselben Instanz braucht keine zweite Einstellung.
#### Eingaben
| Eingabe | Pflicht | Vorgabe | Bedeutung |
| ---------------- | ------- | ------- | --------- |
| `chart-name` | ja | — | Name des Charts, zugleich der Name seines Verzeichnisses unterhalb von `chart-location` |
| `registry-owner` | ja | — | Eigentümer-Segment der Ziel-URL, also der `<owner>` in `<basis>/<owner>/helm/api/charts` |
| `chart-location` | nein | `.` | Verzeichnis, in dem das Chart-Verzeichnis liegt |
Dass `registry-owner` Pflicht ist und keine Vorgabe hat, folgt derselben
Hausregel wie `image-owner` oben: Ein Eigentümer-Segment wird nie geraten.
Aufrufer übergeben `${{ github.repository_owner }}`; der Wert wird damit im
**aufrufenden** Repository ausgewertet, wo er gemessen ist (`jlab-cluster`, Lauf
282 von `validate-charts.yml`). Dass der Kontext auch in einem von fremd
aufgerufenen Workflow der des Aufrufers bleibt, ist dokumentiert, auf dieser
Gitea-Fassung aber nicht gemessen — und ein Chart, das unter dem falschen
Eigentümer veröffentlicht wird, ist nichts, wofür man sich auf eine
Kontext-Semantik verlassen müsste. Als Eingabe hängt der Baustein an keiner.
Alle Eingaben sind `type: string`, auch dort, wo künftig ein Schalter
hinzukommt: Boolesche `workflow_call`-Eingaben werden auf dieser Gitea-Version
nicht durchgereicht — 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.
#### Ausgaben
| Ausgabe | Bedeutung |
| --------------- | --------- |
| `chart-version` | Die veröffentlichte Chart-Version, gelesen aus `version:` in `Chart.yaml` |
#### Aufrufbeispiel
```yaml
jobs:
publish-outline:
name: Publish Outline Helm Chart
uses: workflow-util/pipelines/.gitea/workflows/publish-helm-chart.yml@v3
with:
chart-name: 'outline'
registry-owner: ${{ github.repository_owner }}
secrets: inherit
```
`secrets: inherit` ist notwendig — der Workflow braucht `OP_CONNECT_TOKEN` aus
dem aufrufenden Repository.
Der Baustein löst sich nicht selbst aus. Wann veröffentlicht wird, entscheidet
die Hülle im aufrufenden Repository über ihr eigenes `on:` — üblicherweise ein
`push` auf `main`, eingeschränkt auf das Chart-Verzeichnis, dazu ein
`workflow_dispatch` für den Fall, dass von Hand nachgeschoben werden muss.
#### Form des Tresor-Felds `charts-url`
Das Feld enthält **nur** die Basis:
```
https://<host>/api/packages # so
https://<host>/api/packages/<owner>/helm/api/charts # nicht so
```
Drei Wächter weisen eine andere Form namentlich zurück: wenn der Wert auf
`/api/charts` endet, wenn er das Segment `/helm` enthält, und wenn er nicht auf
`/api/packages` endet. Der Grund ist die Lesbarkeit des Fehlers — 1Password
maskiert die URL im Protokoll, ein 404 gegen eine maskierte Adresse wäre nicht zu
deuten. Der Eigentümer wird aus demselben Grund geprüft: Ein leerer Wert ergäbe
`<basis>//helm/api/charts`, also wieder einen nackten 404.
#### Voraussetzungen im aufrufenden Repository
| Name | Art | Zweck |
| ------------------------- | ----------------- | ----- |
| `OP_CONNECT_HOST` | Variable | Adresse des 1Password-Connect-Servers im Cluster, etwa `https://op-connect.example.com` |
| `OP_CONNECT_TOKEN` | Secret | Zugang zu diesem Connect-Server, kommt über `secrets: inherit` |
| `ci/helm-dependencies.sh` | Datei im Repository | Ausführbares Skript, das die deklarierten Abhängigkeiten eines Charts nach `charts/` holt |
`OP_CONNECT_HOST` ist bewusst eine Variable des aufrufenden Repositories und
kein fest verdrahteter Wert: Ein geteilter Baustein trägt nicht die
Infrastruktur-Adresse eines einzelnen Mandanten. Der Preis dafür ist eine
Voraussetzung, die man vergessen kann, und eine nicht gesetzte Variable kommt als
leere Zeichenkette an. Deshalb prüft der erste Schritt sie und benennt beim
Abbruch die Einstellung samt Ort — statt zwei Schritte später an einer leeren
Adresse zu scheitern.
`ci/helm-dependencies.sh` ist ein Skript des **aufrufenden** Repositories, und
das bleibt nach dem Umzug hierher so: Der Checkout holt das Repository, zu dem
der Lauf gehört — das des Aufrufers —, also ist `${{ github.workspace }}` dessen
Arbeitsverzeichnis. Der Aufruf steht vor `helm package`, weil `helm package` mit
`found in Chart.yaml, but missing in charts/ directory` abbricht, solange eine
deklarierte Abhängigkeit nicht geholt ist. Für ein Chart ohne `dependencies:`
tut das Skript nichts, vorhanden und ausführbar muss es trotzdem sein; fehlt es,
sagt der Lauf genau das.
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 (`<owner>_<repo>`), 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:
@@ -169,3 +375,21 @@ bestehende Aufrufe, wird der Major-Anteil erhöht.
Aufrufe, die den Wert als YAML-Boolean übergeben. `v1` bleibt unverändert
stehen; wer umstellt, wechselt auf `@v2` und schreibt `push: 'true'` bzw.
`push: 'false'`.
### 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, `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 `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` 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.
+6
View File
@@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"config:recommended"
]
}