From c2a7fda79118f3dea39e78047fa162d08fef70d0 Mon Sep 17 00:00:00 2001 From: Justin Klein Date: Thu, 1 Oct 2026 22:43:47 +0000 Subject: [PATCH] Wiki anlegen: Langform zu den Bausteinen und das Gemessene MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Gemessenes-Verhalten.md | 95 ++++++++++++++++++++++ Home.md | 44 ++++++++++ docker-build-push.md | 133 +++++++++++++++++++++++++++++++ publish-helm-chart.md | 172 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 444 insertions(+) create mode 100644 Gemessenes-Verhalten.md create mode 100644 Home.md create mode 100644 docker-build-push.md create mode 100644 publish-helm-chart.md diff --git a/Gemessenes-Verhalten.md b/Gemessenes-Verhalten.md new file mode 100644 index 0000000..cf9b693 --- /dev/null +++ b/Gemessenes-Verhalten.md @@ -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. diff --git a/Home.md b/Home.md new file mode 100644 index 0000000..976ae34 --- /dev/null +++ b/Home.md @@ -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 — `//` ebenso wie +`//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. diff --git a/docker-build-push.md b/docker-build-push.md new file mode 100644 index 0000000..6a6eda5 --- /dev/null +++ b/docker-build-push.md @@ -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 `//:` — 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//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. diff --git a/publish-helm-chart.md b/publish-helm-chart.md new file mode 100644 index 0000000..650d092 --- /dev/null +++ b/publish-helm-chart.md @@ -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-.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 + +``` +//helm/api/charts +``` + +`` 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 `//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, `-.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 `:` 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.