Wiki anlegen: Langform zu den Bausteinen und das Gemessene

Vier Seiten, bewusst nicht mehr. Die Aufteilung ist dieselbe wie in
jlab-cluster/helm-charts: Das README bleibt die Kurzreferenz fuer Aufrufer —
was ein Baustein tut, die Eingabe-Tabelle, ein Aufrufbeispiel, die
Voraussetzungen — und wird dafuer nicht gekuerzt. Das Wiki ist die Langform:
warum die Dinge so sind und was dafuer gemessen wurde.

Home nennt das Repository, verweist auf die Bausteine und fasst die zwei
Hausregeln in je einem Satz: an einen Tag binden, ein Eigentuemer-Segment nie
raten.

docker-build-push und publish-helm-chart tragen die Begruendungen. Beim
Publish-Baustein vor allem die Eigenschaften des Upload-Schritts: warum
--fail-with-body noetig ist, weil curl einen 401 sonst mit Code 0 verlaesst,
und warum der Dateiname gebaut und nicht geglobt wird, weil --upload-file genau
eine Datei nimmt. Im README waeren diese Saetze zu lang; ohne sie entfernt sie
irgendwann jemand als Umstand.

Gemessenes-Verhalten sammelt die Eigenheiten dieser Forge, die hier teuer waren:
boolesche workflow_call-Eingaben kommen nicht an, wandernde Action-Verweise,
und ein fehlendes Secret laesst die Job-Erzeugung scheitern statt den Job, sodass
der Lauf ohne Schritte dasteht. Letzteres ist in jlab-cluster/helm-charts
gemessen; die Quelle steht auf der Seite.

Keine Tag-Nummern und keine Fassungen als Momentaufnahme: Welcher Tag gilt,
steht im README und in der Workflow-Datei. Was hier steht, sind Begruendungen
und Messungen.
jklein committed 2026-10-01 22:43:52 +00:00
commit c2a7fda791
4 files changed
+444

No files matched your search

+95
@@ -0,0 +1,95 @@
# Gemessenes Verhalten dieser Forge
Eigenheiten der Gitea-Instanz, an der diese Bausteine hängen. Alles hier ist
**gemessen**, nicht aus einer Dokumentation übernommen, und jedes Stück hat
einmal Zeit gekostet. Die Seite steht quer zu den Bausteinen, weil keine dieser
Eigenheiten nur einen davon betrifft.
Gemeinsames Muster, das sie teuer macht: Keiner dieser Fehler ist rot. Der Lauf
bleibt grün oder sieht harmlos aus, und erst viel später sucht jemand das
Ergebnis dort, wo es hätte landen sollen.
## Boolesche `workflow_call`-Eingaben kommen nicht an
Gemessen an **Gitea 1.27.0**,
[Issue #4](https://git.jk-dev.de/workflow-util/pipelines/issues/4).
Eine Eingabe vom Typ `boolean` erreicht den aufgerufenen Workflow nicht. Weder
ihre Vorgabe noch ein ausdrücklich übergebener Wert: `inputs.push` blieb leer.
Der Baustein baute dadurch immer im Prüfmodus — **bei grünem Lauf**, mit einem
`--push`, das in der Kommandozeile einfach fehlte, und einer Warnung im
Protokoll, die niemand liest, solange nichts rot ist.
Zwei Läufe des ersten Abnehmers:
| Lauf | Aufruf | Ergebnis |
| --- | --- | --- |
| 272 | `push` weggelassen, Vorgabe gesetzt | kein `--push`, nichts veröffentlicht, Lauf grün |
| 276 | `push: true` ausdrücklich übergeben | kein `--push`, nichts veröffentlicht, Lauf grün |
Die Eingrenzung ist das Belastbare daran: Im **selben** Lauf kamen alle Eingaben
vom Typ `string` korrekt an — Plattformen, Registry, Eigentümer, Container-Name
wurden alle richtig aufgelöst. `push` war die einzige boolesche Eingabe und die
einzige, die versagte.
**Konsequenz für alle Bausteine hier: Jede Eingabe ist `type: string`, auch
eine, die inhaltlich ein Schalter ist.** Werte dann als `'true'` und `'false'`,
und im Aufruf in Anführungszeichen — ein unquotiertes `push: false` ist im YAML
ein Boolean und wird verschluckt.
Der Fehler war so langlebig, weil nirgends sichtbar war, was überhaupt ankommt.
Dagegen hilft nur eines, und `docker-build-push` hat es seitdem: ein
Diagnoseschritt, der den rohen Wert der Eingabe und den daraus abgeleiteten
Modus ausgibt. Rein informativ und so abgesichert, dass er den Job unter keinen
Umständen scheitern lässt.
## Ein fehlendes Secret lässt die Job-Erzeugung scheitern, nicht den Job
Gemessen in `jlab-cluster/helm-charts`; dort festgehalten im README-Abschnitt
„Where this repo lives, and what a Gitea runner does differently" und als
Kommentar an der Secret-Deklaration von dessen `install-test.yml`.
Wiederverwendbare Workflows mit `secrets: inherit` funktionieren auf dieser
Instanz. Die Eigenheit: Fehlt ein Secret, das der aufgerufene Workflow als
`required: true` deklariert, **scheitert nicht der Job, sondern seine
Erzeugung** — der Lauf steht dann ohne Schritte da. Nichts in ihm benennt die
Ursache, und man debuggt mit Vorliebe das Falsche: das Workflow-YAML, den
Runner, die Action.
Daraus folgt, dass `required` an einem Secret eine echte Entscheidung ist und
keine Formalie:
- Braucht **jeder** Aufrufer das Secret, ist `required: true` richtig — so hält
es [publish-helm-chart](publish-helm-chart), denn ohne Connect-Token kann
niemand veröffentlichen. Dass der Lauf ohne Schritte dasteht, ist dann der
verständliche Fall: Es fehlt genau diese Voraussetzung.
- Braucht nur **ein Teil** des Bausteins es, muss es `required: false` sein. Im
Herkunfts-Repository hängt das Token an genau einem Zweig einer Matrix; mit
`required: true` verlöre ein Pull Request aus einem Fork — der keine Secrets
trägt — die gesamte Prüfung, jedes Chart auf einmal, ohne dass etwas die
Ursache nennt. Optional deklariert scheitert nur der eine Zweig, und zwar laut
an einem Wächter, der das fehlende Tresor-Feld benennt.
Dieselbe Linie wie an jeder anderen Stelle dieses Repositories: Wenn etwas
fehlen kann, soll es laut und benannt fehlen, nicht still.
## Wandernde Action-Verweise
[Issue #5](https://git.jk-dev.de/workflow-util/pipelines/issues/5), Punkt 1.
Ein Verweis auf einen Zweig statt auf eine Hauptversion holt bei jedem Lauf den
jeweils neuesten Stand. Eine Änderung dort ändert das Verhalten **aller**
Projekte, ohne dass hier etwas angefasst wurde — und der Fehler zeigt sich bei
einem Aufrufer, der nichts geändert hat.
Das ist dieselbe Mechanik, wegen der das README von Aufrufern verlangt, auf
einen Tag statt auf den Hauptzweig zu zeigen: Ein beweglicher Verweis macht aus
dem Übernehmen einer neuen Fassung eine unbeobachtete Nebenwirkung statt einer
Entscheidung. Nach innen gilt die Regel genauso wie nach außen, sonst ist die
Sorgfalt an der Aufrufgrenze wertlos.
In `docker-build-push` steht noch ein solcher Verweis — die Buildx-Action zeigt
auf einen Zweig, während die übrigen Actions auf Hauptversionen gepinnt sind.
Festgehalten als bekannte Schwachstelle unter
[docker-build-push](docker-build-push), damit sie nicht erneut entdeckt werden
muss.
+44
@@ -0,0 +1,44 @@
# pipelines
Geteilte CI/CD-Bausteine für die Gitea-Instanz `git.jk-dev.de`. Die Workflows in
diesem Repository lösen sich nicht selbst aus; sie werden von anderen
Repositories über `uses:` aufgerufen (`on: workflow_call`).
## Die Bausteine
| Baustein | Was er tut |
| --- | --- |
| [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. |
Dazu eine Seite quer zu beiden: [Gemessenes-Verhalten](Gemessenes-Verhalten) —
die Eigenheiten dieser Forge, die hier Geld gekostet haben, mit dem Lauf oder
dem Issue, an dem sie gemessen wurden.
## Die zwei Hausregeln
**An einen Tag binden, nie an `@main`.** Mit `@main` ändert jede Anpassung hier
unangekündigt das Verhalten sämtlicher Projekte, und der Fehler zeigt sich dann
dort, wo niemand etwas geändert hat.
**Ein Eigentümer-Segment wird nie geraten.** Jeder Baustein, der einen
Eigentümer in eine Adresse einsetzt — `<registry>/<owner>/<image>` ebenso wie
`<basis>/<owner>/helm/api/charts` —, nimmt ihn als Pflichteingabe ohne Vorgabe.
Genau das war der Fehler des früher fest verdrahteten Eigentümers, unter dem die
Images aller Projekte in einem fremden Konto landeten.
## Was hier steht und was im README
Das `README.md` im Repository ist die **Kurzreferenz für Aufrufer**: was ein
Baustein tut, seine Eingaben als Tabelle, ein Aufrufbeispiel, die
Voraussetzungen im aufrufenden Repository. Wer einen Aufruf schreibt, braucht
nur das.
Dieses Wiki ist die **Langform**: warum die Dinge so sind, wie sie sind, und was
dafür gemessen wurde. Es wiederholt das README nicht, es begründet es.
Eine Grenze, die beide Seiten betrifft: **Tag-Nummern und Fassungen stehen nicht
hier.** Welcher Tag aktuell zu verwenden ist, steht im README und in der
jeweiligen Workflow-Datei; eine Nummer in diesem Wiki wäre eine Momentaufnahme,
die beim nächsten Tag still falsch wird. Was hier steht, sind Begründungen und
Messungen — die veralten nicht.
+133
@@ -0,0 +1,133 @@
# docker-build-push
`.gitea/workflows/docker-build-push.yml` — baut ein Docker-Image und schiebt es
als Multi-Arch-Manifest in eine Registry. Die Eingabe-Tabelle, das
Aufrufbeispiel und die Voraussetzungen stehen im README; diese Seite begründet,
warum der Baustein so aussieht.
## Warum `image-owner` Pflicht ist und `registry` nicht
Der Eigentümer ist je Projekt verschieden, die Registry praktisch immer
dieselbe. Deshalb hat `registry` eine Vorgabe und `image-owner` keine.
Das ist keine Geschmacksfrage: Der Eigentümer war früher fest verdrahtet, und
die Folge war, dass die Images **aller** aufrufenden Projekte unter demselben
fremden Konto landeten. Ein geratener Eigentümer scheitert nicht, er schiebt das
Image woandershin — und das fällt erst auf, wenn jemand es dort sucht, wo es
hingehört. Die Regel steht daher allgemein auf [Home](Home): Ein
Eigentümer-Segment wird nie geraten.
## Warum `push` eine Zeichenkette ist und kein Boolean
Weil ein boolescher `workflow_call`-Eingang auf dieser Forge-Fassung nicht
ankommt — nicht einmal seine Vorgabe. Der Baustein baute dadurch immer im
Prüfmodus und meldete dabei Erfolg. Die Messung, die Läufe und die Eingrenzung
stehen auf [Gemessenes-Verhalten](Gemessenes-Verhalten).
Zwei Dinge, die man dazu wissen muss:
**Die Anführungszeichen sind nicht kosmetisch.** `push: false` ohne sie ist im
YAML ein Boolean und kommt nicht an; es griffe dann die Vorgabe, und der Lauf
veröffentlichte trotz `push: false`. Richtig ist `push: 'false'`.
**Die Bedingung musste bei der Umstellung nicht angefasst werden.** Sie lautet
`inputs.push && inputs.push != 'false'` und war von Anfang an für beide
Schreibweisen gebaut: `inputs.push` allein wäre bei der Zeichenkette `'false'`
wahr, weil nichtleere Zeichenketten truthy sind; `inputs.push == 'true'` wäre
beim echten Boolean `true` falsch, weil ein Vergleich über Typgrenzen über
Zahlen läuft und `'true'` dabei zu `NaN` wird. Die Kombination deckt alle vier
Fälle ab.
Wer das beim nächsten Aufräumen „korrekt" auf `boolean` zurückstellt, holt den
Fehler still zurück: Es wird nichts veröffentlicht, und der Lauf bleibt grün.
Darum steht die Begründung auch als Kommentar an der Eingabe selbst.
## Der Prüfmodus und sein Preis
Mit abgeschaltetem Push baut der Baustein nur. Der Zweck ist, dass die CI eines
Projekts dieselbe Bau-Logik benutzt wie das Deployment, statt sie zu
duplizieren.
Der Preis hängt an einer Kopplung, die nicht offensichtlich ist: Ohne Push wird
der 1Password-Schritt übersprungen, und aus **demselben** Schritt stammt der
Checkout-Token. Der Checkout fällt dann auf den Standard-Token des Jobs zurück.
Der genügt für das ausführende Repository, nicht aber für Submodule in eigenen
privaten Repositories — deren Klon scheitert mit `Repository not found`.
Daraus folgt: Projekte **mit** privaten Submodulen können den Prüfmodus nicht
verwenden. Für Projekte ohne Submodule ist er unproblematisch.
## Warum zwei verschiedene Zugänge
Der Token, den die Forge jedem Job stellt, taugt für zwei Dinge nicht: Er gilt
nur für das ausführende Repository, während die Submodule in eigenen privaten
Repositories liegen, und die Container-Registry lehnt ihn ab
(`container.Authenticate` antwortet mit 401). Beides deckt je ein eigener Zugang
ab, gepflegt in 1Password und zur Laufzeit über den Connect-Server im Cluster
geholt. In der Forge liegt nur dessen Zugang.
Benutzername und Token für die Registry kommen aus **einem** Eintrag, damit sie
zwangsläufig zusammenpassen — die Registry prüft den Token gegen genau das
Konto, das im Feld `username` steht.
## Warum es keinen `env`-Block auf Workflow-Ebene gibt
`env` auf Workflow-Ebene wird ausgewertet, **bevor** es einen `inputs`-Kontext
gibt. Registry und Eigentümer standen dort früher; als sie Eingaben wurden,
konnten sie nicht bleiben. Sie werden deshalb an den beiden Stellen gelesen, an
denen der Kontext sicher vorhanden ist: im Login-Schritt und in der Tag-Liste.
Dieselbe Falle gilt für jeden künftigen Baustein hier.
## Die Vorgaben der Versionsangaben sind absichtlich unecht
Die vier Versionsangaben dienen ausschließlich der Tag-Liste. Ihre Vorgabe ist
ein sichtbar unechter Wert und nicht leer, weil die Tag-Liste auch ohne Push
gebildet wird: Die Bau-Action gibt sie als `--tag` an den Build weiter, und ein
leerer Wert ergäbe `<registry>/<owner>/<name>:` — der Build scheiterte an
`invalid reference format`.
## Der Runner braucht ein Docker-Socket, und das Label entscheidet darüber
Der Baustein läuft auf einem Runner mit einem eigenen Label, nicht auf dem
Standard-Label. Gemessen in `jlab-cluster/helm-charts`: Auf dieser Instanz hat
der Standard-Runner **kein** Docker-Socket, der mit dem Docker-Label hat eines.
Dasselbe Label bedeutet auf den beiden Forges, auf denen dieses Setup lebt,
verschiedene Maschinen.
Läuft der Daemon des Runners rootless, liegt sein Socket nicht unter
`/var/run/docker.sock`, sondern unter `/run/user/<uid>/docker.sock`. Der Pfad
kommt deshalb aus einer Variablen des aufrufenden Repositories; ist sie nicht
gesetzt, bleibt sie leer und Docker nutzt seinen Standard. Ein eigener,
rein informativer Schritt gibt aus, welches Socket im Job-Container ankommt —
weil das beim Scheitern des Buildx-Starts immer die erste Frage ist.
## Bekannte Schwachstellen
Festgehalten, damit sie nicht erneut entdeckt werden müssen; alle drei in
[Issue #5](https://git.jk-dev.de/workflow-util/pipelines/issues/5).
**Der Buildx-Action-Verweis wandert.** Er zeigt auf einen Zweig statt auf eine
Hauptversion. Das widerspricht genau der Regel, die das README von Aufrufern
verlangt — und hat dieselbe Folge: Eine Änderung dort ändert das Verhalten aller
Projekte, ohne dass hier etwas angefasst wurde. Siehe
[Gemessenes-Verhalten](Gemessenes-Verhalten).
**Die Beschreibung von `platforms` behauptet etwas Falsches.** Sie sagt, die
Bau-Stufen aller Dockerfiles seien auf die Bau-Plattform gepinnt, eine
zusätzliche Plattform fügte also nur eine Laufzeitstufe hinzu. Für das
Dockerfile des ersten Abnehmers stimmt das nicht, und zwar bewusst nicht:
Tailwind, lightningcss und der Next-Compiler bringen plattformgebundene
Binärmodule mit, ein Cross-Build baute die falsche Architektur ein. Eine
zusätzliche Plattform bedeutet dort einen **vollständigen** zweiten
Kompilierlauf unter Emulation. Gemessen: 72 Sekunden für eine Plattform gegen
417 Sekunden für zwei. Als allgemeine Aussage in einem geteilten Baustein führt
der Satz in die Irre; er hat bei einem Abnehmer zu einer Fehleinschätzung
beigetragen.
**Die Tag-Liste erzeugt einen Tag doppelt.** Bei einem dreiteiligen
Versionsschema ergeben die vollständige Version und die aus Major, Minor und
Patch zusammengesetzte denselben Wert. Harmlos — Docker pusht denselben Tag
zweimal —, aber beim Nachzählen verwirrend. Nebenbei: Bei einem Kalenderschema
sind die mitwandernden Tags „neuestes Image aus diesem Jahr" beziehungsweise
„aus diesem Monat" von zweifelhaftem Nutzen; bei SemVer ergeben sie Sinn.
+172
@@ -0,0 +1,172 @@
# publish-helm-chart
`.gitea/workflows/publish-helm-chart.yml` — packt ein Helm-Chart und lädt es in
eine Helm-Registry. Die Eingabe-Tabelle, das Aufrufbeispiel und die
Voraussetzungen stehen im README; diese Seite begründet den Baustein, und vor
allem den Upload-Schritt. Jede seiner Eigenschaften ist einmal bezahlt worden.
Der Baustein kommt aus `jlab-cluster/helm-charts`, wo er die dünnen
`publish-<chart>.yml`-Hüllen bedient, und ist mit dem Umzug hierher an zwei
Stellen angepasst worden — siehe „Der Eigentümer ist eine Eingabe" und „Der
Connect-Host ist eine Variable des Aufrufers" weiter unten.
## Die Registry ist klassisch, nicht OCI
Das Ziel ist eine **indexbasierte** Helm-Registry. Es gibt dort keine
Registry-Sitzung, also kein `helm registry login` und kein `helm push`: Der
Upload ist ein einzelnes HTTP-`POST` des gepackten `.tgz`, je Anfrage
authentifiziert. Das ist der Grund, warum der Schritt `curl` benutzt und nicht
`helm` — und damit der Grund für fast alles, was danach kommt. `helm push`
würde einen Fehler von selbst zum Fehler machen; `curl` tut das nicht.
## Die Ziel-URL wird zusammengesetzt, nicht gespeichert
```
<charts-url>/<registry-owner>/helm/api/charts
```
`<charts-url>` liegt im Tresor und enthält **nur** die Basis bis
`/api/packages`. Der Eigentümer kommt als Eingabe, Pakettyp und Endpunkt setzt
der Workflow selbst an.
Der Gewinn: Im aufrufenden Repository steht kein Wert, der auf die Registry
zeigt — nicht einmal ein Pfadsegment. Und ein zweiter Eigentümer auf derselben
Instanz braucht keine zweite Einstellung.
Der Preis: Eine falsche Form des Tresor-Felds ergibt eine falsche URL, und die
URL ist im Protokoll maskiert. Ein 404 dagegen wäre nicht zu deuten. Deshalb
weisen drei Wächter eine andere Form **namentlich** zurück und sagen, was im
Tresor-Eintrag zu ändern ist:
- der Wert endet auf `/api/charts` — das ist die alte, vollständige Form;
- der Wert enthält das Segment `/helm` — ebenfalls die alte Form;
- der Wert endet nicht auf `/api/packages` — irgendetwas anderes.
## Der Eigentümer ist eine Eingabe
`registry-owner` ist Pflicht und hat keine Vorgabe. Das folgt der Hausregel von
[Home](Home) — ein Eigentümer-Segment wird nie geraten — und ersetzt die frühere
Ableitung aus dem Eigentümer des laufenden Repositories.
Die Ableitung war im Herkunfts-Repository richtig, dort ist der Wert gemessen.
Dass der Kontext auch in einem **von fremd** aufgerufenen Workflow der des
Aufrufers bleibt, ist dokumentiert, auf dieser Forge-Fassung aber nicht
gemessen. Als Eingabe wird der Wert dort ausgewertet, wo er bekannt ist, und der
Baustein hängt an keiner Kontext-Semantik — auch nicht an einer zugesicherten.
Ein Chart, das unter dem falschen Eigentümer veröffentlicht wird, ist nichts,
wofür man sich darauf verlassen müsste.
Ein zweiter Wächter prüft den Wert auf URL-taugliche Zeichen. Ein leerer
Eigentümer ergäbe `<basis>//helm/api/charts`, ein `/` oder `?` darin etwas
völlig anderes — beides sind nackte 404 gegen eine maskierte Adresse.
Alle Eingaben des Bausteins sind `type: string`, auch wenn künftig ein Schalter
hinzukommt. Warum: [Gemessenes-Verhalten](Gemessenes-Verhalten).
## Der Connect-Host ist eine Variable des Aufrufers
Im Herkunfts-Repository war der 1Password-Connect-Host fest verdrahtet. Hier ist
er eine Variable des aufrufenden Repositories — ein geteilter Baustein trägt
nicht die Infrastruktur-Adresse eines einzelnen Mandanten.
Der Preis ist eine Voraussetzung, die man vergessen kann, und eine nicht
gesetzte Variable kommt als **leere Zeichenkette** an. Ein leerer Host scheitert
nicht dort, wo er gesetzt wird, sondern zwei Schritte später in der Lade-Action,
gegen eine Adresse, die niemand sieht. Deshalb prüft der erste Schritt des Jobs
die Variable und benennt beim Abbruch die Einstellung, den Ort und die erwartete
Form.
## Die Abhängigkeiten gehören dem Aufrufer
`ci/helm-dependencies.sh` ist ein Skript des **aufrufenden** Repositories, und
das bleibt nach dem Umzug so: Der Checkout holt den Baum des Repositories, zu
dem der Lauf gehört — den des Aufrufers —, also ist der Arbeitsbereich dessen
Baum und das Skript dessen Kopie. Es ist damit eine Voraussetzung jedes
Aufrufers, genauso wie der Connect-Host, und ein Wächter sagt das beim Fehlen,
statt die Shell eine fehlende Datei melden zu lassen.
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.
Eine Folge für das veröffentlichte Artefakt, die man kennen muss: Das `.tgz` des
Subcharts wird **in** unseres gepackt. Die in `Chart.yaml` gepinnte Version ist
damit die Version, die Betreiber bekommen, und nichts stromabwärts sieht die
Abhängigkeit jemals wieder — ein Sicherheitsupdate des Subcharts erreicht einen
Cluster erst, wenn das äußere Chart neu veröffentlicht wird.
## Der Upload-Schritt, Eigenschaft für Eigenschaft
Dies ist der Kern der Seite. Jeder Punkt ist einmal teuer gewesen; wer einen
davon als Umstand entfernt, holt den jeweiligen Fehler zurück.
### `--fail-with-body`, sonst ist ein 401 ein Erfolg
`curl` verlässt einen HTTP-Fehler mit Code **0**, solange man nichts anderes
verlangt. Ohne dieses Flag wäre die Veröffentlichung also grün, während nichts
hochgeladen wurde. Gemessen gegen Gitea 1.24.7: Ein 401 ohne das Flag ist
Exit 0, mit dem Flag Exit 22 — und der Antwortkörper wird weiterhin ausgegeben,
sodass das Protokoll die Ursache nennt.
Das ist der wichtigste Unterschied zu `helm push`, das von selbst scheitert.
### `curl --config -` statt `--user`
`curl --user $U:$P` schreibt die Zugangsdaten in `argv`, wo sie in der
Prozessliste stehen. `curl --config -` liest seine Optionen stattdessen von der
Standardeingabe; nichts Sensibles erscheint in irgendeinem Argument.
Der Preis dieser Konfigurationsdatei ist ihre Wertsyntax: Ein **unquotierter
Wert endet am ersten Leerzeichen**. Gemessen an curl 8.5.0 — aus `has space`
kam `has` an. Ein Leerzeichen im Passwort käme deshalb als rätselhafter 401
zurück. Der Schritt weist Leerzeichen in den Zugangsdaten vorher ab und sagt
warum. Ein Zugriffstoken dieser Forge ist 40 Hexzeichen lang, der Wächter
greift also nur bei einem falsch eingetragenen Tresor-Eintrag.
### Der Dateiname wird gebaut, nicht geglobt
`--upload-file` nimmt **genau eine** Datei. Ein Glob, der zwei trifft, macht die
zweite zum URL-Argument von `curl`; ein Glob, der keine trifft, wird wörtlich
durchgereicht. Beides scheitert unverständlich.
Der Name wird deshalb aus Chart-Name und gelesener Version zusammengesetzt, und
ein `test -f` prüft ihn vorher — beim Fehlschlag listet die Meldung auf, was
`helm package` tatsächlich erzeugt hat. Dass das zusammenpasst, ist gemessen:
`helm package` behält das `+` einer Version der Form `1.4.0+up1.9.2` wörtlich im
Dateinamen, `<name>-<version>.tgz` ist also eine exakte Entsprechung.
### Die Werte stehen im `env` des Schritts, nicht in der Kommandozeile
Sie werden als quotierte Shell-Variablen gelesen und nie in die Kommandozeile
interpoliert. Jeder Wert hat einen `:?`-Wächter, der beim Fehlen **das fehlende
`op://`-Feld benennt**.
Warum das nötig wurde: Mit dem Sprung auf die neue Hauptfassung der
1Password-Lade-Action exportiert sie die geladenen Werte **nicht mehr** als
Umgebungsvariablen für Folgeschritte, wie die Vorfassung es tat; nur noch die
Step-Outputs tragen sie. Jeder Verweis über die Umgebung löste von diesem Sprung
an zu einer leeren Zeichenkette auf — und **nichts meldete einen Fehler**: Der
Ladeschritt protokolliert „Populating variable" und bleibt so oder so grün. Die
Wächter scheitern laut, und das Quoten verhindert, dass ein leerer Wert
stillschweigend die Argumente verschiebt.
### Der Host wird zusätzlich maskiert
1Password maskiert die vollständige URL im Protokoll. Ein Verbindungsfehler von
`curl` gibt aber nur `<host>:<port>` aus, und das deckt diese Maske nicht. Der
Schritt leitet den Host aus der rohen URL ab und maskiert ihn selbst, bevor
sonst etwas läuft.
## Was der Baustein nicht tut
Er löst sich nicht selbst aus und hat keinen `workflow_dispatch`. Ein Dispatch
hier liefe gegen dieses Repository, in dem kein Chart liegt. Wann veröffentlicht
wird, entscheidet die Hülle im aufrufenden Repository über ihr eigenes `on:` —
üblicherweise ein Push auf den Hauptzweig, eingeschränkt auf das
Chart-Verzeichnis, dazu ein `workflow_dispatch` für den Fall, dass von Hand
nachgeschoben werden muss.
Er prüft das Chart auch nicht. Lint, Render und Install-Test sind Sache des
aufrufenden Repositories, weil sie an dessen Fixtures hängen. Ein Baustein, den
nur ein Repository benutzen kann, wäre nicht geteilt, sondern verlagert.