diff --git a/.gitea/workflows/docker-build-push.yml b/.gitea/workflows/docker-build-push.yml index 40b49aa..4fe5005 100644 --- a/.gitea/workflows/docker-build-push.yml +++ b/.gitea/workflows/docker-build-push.yml @@ -1,18 +1,43 @@ on: workflow_call: inputs: + # Die vier Versionsangaben dienen ausschliesslich der Tag-Liste. Ohne Push + # gibt es nichts zu taggen, deshalb sind sie nicht mehr Pflicht — ein + # reiner Prueflauf muesste sonst Platzhalter erfinden. Bestehende Aufrufer + # uebergeben sie unveraendert weiter; required von true auf false zu + # senken ist rueckwaertskompatibel. + # + # Die Vorgabe ist 0 bzw. 0.0.0 und nicht etwa leer: docker/build-push-action + # gibt die Tag-Liste auch ohne Push als --tag an den Build weiter. Ein + # leerer Wert ergaebe "//:" und liesse den Build an + # "invalid reference format" scheitern. 0.0.0 ist ein gueltiger, sichtbar + # unechter Wert. version-number: - required: true + required: false + default: 0.0.0 type: string version-number-major: - required: true + required: false + default: '0' type: string version-number-minor: - required: true + required: false + default: '0' type: string version-number-patch: - required: true + required: false + default: '0' type: string + push: + description: >- + Whether the built image is pushed. With push: false no credentials are + fetched from 1Password and no registry login happens, so the workflow + doubles as a build check. Caveat: the checkout token comes from the + same 1Password step, so without push the checkout falls back to the + job token — which cannot clone private submodules. + required: false + default: true + type: boolean container-name: required: true type: string @@ -22,14 +47,25 @@ on: docker-context: required: true type: string + # image-owner und registry bilden zusammen das Praefix des Images. Dass + # das eine Pflicht ist und das andere nicht, ist Absicht: der Eigentuemer + # ist je Projekt verschieden und soll nie stillschweigend geraten werden — + # genau das war der Fehler des frueheren, fest verdrahteten "suer", unter + # dem die Images aller Projekte landeten. Die Registry ist dagegen + # praktisch immer dieselbe und darf deshalb eine Vorgabe haben. image-owner: description: >- Owner segment of the pushed image, i.e. the middle part of //. Used to be hardcoded to "suer", which sent every image of every calling project to the same account. - Kept as the default so existing callers stay unchanged. + Deliberately has no default: every caller states its owner explicitly. + required: true + type: string + registry: + description: >- + Registry the image is tagged for and, when pushing, logged in to. required: false - default: suer + default: git.jk-dev.de type: string timeout-minutes: description: 'Hard limit for the build job. Without one a wedged build runs into the 6h default.' @@ -54,8 +90,12 @@ on: name: build and push Docker Image -env: - REGISTRY: git.jk-dev.de +# Einen env-Block auf Workflow-Ebene gibt es nicht mehr: sein einziger Eintrag +# war REGISTRY, und der ist jetzt eine Eingabe. Dort haette er auch nicht +# bleiben koennen — env auf Workflow-Ebene wird ausgewertet, bevor es einen +# inputs-Kontext gibt. Registry und Eigentuemer werden deshalb an den beiden +# Stellen gelesen, an denen der Kontext sicher vorhanden ist: im +# Login-Schritt und in der Tag-Liste. jobs: build-image: @@ -80,8 +120,24 @@ jobs: # (container.Authenticate antwortet mit 401). Beides deckt je ein eigener # Zugang ab, gepflegt in 1Password und hier zur Laufzeit ueber den # Connect-Server im Cluster geholt; in Gitea liegt nur dessen Zugang. + # + # Ohne Push werden gar keine Zugangsdaten geholt. Das hat eine Folge, die + # man kennen muss: der Checkout-Token faellt damit ebenfalls weg, der + # Checkout nutzt dann den Standard-Token des Jobs — und der kann die + # privaten Submodul-Repositories nicht klonen. Projekte MIT privaten + # Submodulen koennen den Modus push: false daher nicht nutzen; ihr Klon + # scheitert an "Repository not found". Fuer Projekte ohne Submodule ist + # der Modus unproblematisch. + # + # Zur Bedingung: ein boolescher workflow_call-Input kann je nach + # Schreibweise des Aufrufers als echter Boolean oder als String ankommen. + # `inputs.push` allein waere bei dem String 'false' wahr (nichtleere + # Strings sind truthy), `inputs.push == 'true'` waere beim echten Boolean + # true falsch (ein Vergleich ueber Typgrenzen laeuft ueber Zahlen, 'true' + # wird dabei zu NaN). Die Kombination aus beidem deckt alle vier Faelle ab. - name: Zugangsdaten aus 1Password laden id: op-secrets + if: ${{ inputs.push && inputs.push != 'false' }} uses: 1password/load-secrets-action@v2 with: # Die Werte sollen nur als Step-Outputs entstehen und nicht in die @@ -104,7 +160,15 @@ jobs: submodules: true # Die Action legt je Referenz einen Output an, benannt wie die # Umgebungsvariable, in der die op://-Referenz stand. - token: ${{ steps.op-secrets.outputs.CHECKOUT_TOKEN }} + # + # Ist der 1Password-Schritt uebersprungen (push: false), sind seine + # Outputs leer. Ein leer uebergebenes token wuerde die Vorgabe der + # Action (github.token) ueberschreiben und den Checkout ohne jeden + # Zugang laufen lassen — er scheiterte schon am eigenen Repository. + # Der Fallback macht daraus den regulaeren Job-Token. Der genuegt + # fuer das ausfuehrende Repository, nicht fuer private Submodule + # (siehe Kommentar am 1Password-Schritt). + token: ${{ steps.op-secrets.outputs.CHECKOUT_TOKEN || github.token }} # Schlaegt der Buildx-Start fehl, ist die erste Frage immer, welcher # Socket im Job-Container ueberhaupt ankommt. Der Schritt beantwortet sie, # ohne den Job scheitern zu lassen. @@ -118,10 +182,15 @@ jobs: - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@master + # Ohne Push gibt es nichts anzumelden — und die Zugangsdaten waeren + # ohnehin leer, weil der 1Password-Schritt uebersprungen wurde. Ein + # Login-Versuch mit leerem Benutzer scheiterte mit einer nichtssagenden + # Meldung; dieselbe Bedingung wie oben verhindert das. - name: Authenticate To Registry + if: ${{ inputs.push && inputs.push != 'false' }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ inputs.registry }} username: ${{ steps.op-secrets.outputs.REGISTRY_USER }} password: ${{ steps.op-secrets.outputs.REGISTRY_TOKEN }} - name: Build @@ -132,15 +201,22 @@ jobs: file: ${{ inputs.dockerfile }} platforms: ${{ inputs.platforms }} build-args: ${{ inputs.build-args }} - push: true + # Dieselbe Normalisierung wie bei den if:-Bedingungen. Das Ergebnis + # ist immer genau "true" oder "false" — die Action liest den Wert als + # Boolean und akzeptiert nichts dazwischen. + push: ${{ inputs.push && inputs.push != 'false' }} # Der Eigentümer stand frueher als IMAGE_OWNER im env-Block des # Workflows. Dort kann er nicht bleiben: env auf Workflow-Ebene wird # ausgewertet, bevor es einen inputs-Kontext gibt. Der Wert wird # deshalb hier unmittelbar aus inputs gelesen — an dieser Stelle ist # der Kontext sicher vorhanden, anders als in jedem env-Block. + # + # Ohne Push bleibt die Liste rein lokal: die Action gibt sie als --tag + # an den Build weiter, veroeffentlicht aber nichts. Die Versionen + # tragen dann ihre Vorgabe 0.0.0. tags: | - ${{ env.REGISTRY }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:latest - ${{ env.REGISTRY }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:${{ inputs.version-number }} - ${{ env.REGISTRY }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:${{ inputs.version-number-major }} - ${{ env.REGISTRY }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:${{ inputs.version-number-major }}.${{ inputs.version-number-minor }} - ${{ env.REGISTRY }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:${{ inputs.version-number-major }}.${{ inputs.version-number-minor }}.${{ inputs.version-number-patch }} + ${{ inputs.registry }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:latest + ${{ inputs.registry }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:${{ inputs.version-number }} + ${{ inputs.registry }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:${{ inputs.version-number-major }} + ${{ inputs.registry }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:${{ inputs.version-number-major }}.${{ inputs.version-number-minor }} + ${{ inputs.registry }}/${{ inputs.image-owner }}/${{ inputs.container-name }}:${{ inputs.version-number-major }}.${{ inputs.version-number-minor }}.${{ inputs.version-number-patch }} diff --git a/README.md b/README.md index 5b867ea..b682112 100644 --- a/README.md +++ b/README.md @@ -9,38 +9,55 @@ hier werden nicht selbst ausgelöst, sondern von anderen Repositories über ### `.gitea/workflows/docker-build-push.yml` Baut ein Docker-Image und schiebt es als Multi-Arch-Manifest -(`linux/amd64,linux/arm64`) in die Registry `git.jk-dev.de`. Läuft auf einem -Runner mit dem Label `ubuntu-docker`. +(`linux/amd64,linux/arm64`) in eine Registry, vorgabegemäß `git.jk-dev.de`. +Läuft auf einem Runner mit dem Label `ubuntu-docker`. Gepusht wird unter fünf Tags: ``` -git.jk-dev.de//:latest -git.jk-dev.de//: -git.jk-dev.de//: -git.jk-dev.de//:. -git.jk-dev.de//:.. +//:latest +//: +//: +//:. +//:.. ``` Der Checkout holt Submodule mit; die Zugangsdaten dafür und für die Registry kommen zur Laufzeit aus 1Password Connect. +Mit `push: false` entfällt der Push — der Baustein baut dann nur. Damit nutzt +die CI eines Projekts dieselbe Bau-Logik wie das Deployment, statt sie zu +duplizieren. Siehe [Prüfmodus](#prüfmodus-ohne-push). + #### Eingaben | Eingabe | Pflicht | Vorgabe | Bedeutung | | ---------------------- | ------- | ------------------------- | --------- | -| `version-number` | ja | — | Vollständige Version, wird als Tag gesetzt | -| `version-number-major` | ja | — | Major-Anteil für die mitwandernden Tags | -| `version-number-minor` | ja | — | Minor-Anteil | -| `version-number-patch` | ja | — | Patch-Anteil | | `container-name` | ja | — | Name des Images | | `dockerfile` | ja | — | Pfad zum Dockerfile | | `docker-context` | ja | — | Build-Kontext | -| `image-owner` | nein | `suer` | Eigentümer-Segment des Images, also der mittlere Teil von `//` | +| `image-owner` | ja | — | Eigentümer-Segment des Images, also der mittlere Teil von `//` | +| `push` | nein | `true` | Ob das Image gepusht wird. Bei `false` werden keine Zugangsdaten aus 1Password geholt und es findet kein Registry-Login statt — siehe [Prüfmodus](#prüfmodus-ohne-push) | +| `registry` | nein | `git.jk-dev.de` | Registry, für die getaggt und — beim Push — bei der angemeldet wird | +| `version-number` | nein | `0.0.0` | Vollständige Version, wird als Tag gesetzt | +| `version-number-major` | nein | `0` | Major-Anteil für die mitwandernden Tags | +| `version-number-minor` | nein | `0` | Minor-Anteil | +| `version-number-patch` | nein | `0` | Patch-Anteil | | `timeout-minutes` | nein | `45` | Harte Grenze für den Build-Job | | `platforms` | nein | `linux/amd64,linux/arm64` | Zielplattformen der Manifest-Liste | | `build-args` | nein | — | Zusätzliche Build-Argumente, ein `NAME=wert` je Zeile | +Dass `image-owner` Pflicht ist und `registry` nicht, ist Absicht: Der Eigentümer +ist je Projekt verschieden und soll nie stillschweigend geraten werden — genau +das war der Fehler des früher fest verdrahteten `suer`, unter dem die Images +aller Projekte landeten. Die Registry ist dagegen praktisch immer dieselbe. + +Die vier Versionsangaben dienen ausschließlich der Tag-Liste. Sie sind keine +Pflicht mehr, damit ein Prüflauf keine Platzhalter erfinden muss; wer pusht, +übergibt sie wie bisher. Ihre Vorgabe ist bewusst `0.0.0` und nicht leer — die +Tag-Liste wird auch ohne Push gebildet, und ein leerer Wert ergäbe das ungültige +`…/:`. + #### Aufrufbeispiel ```yaml @@ -62,12 +79,45 @@ jobs: `secrets: inherit` ist notwendig — der Workflow braucht `OP_CONNECT_TOKEN` aus dem aufrufenden Repository. +#### Prüfmodus ohne Push + +Mit `push: false` baut der Baustein nur und veröffentlicht nichts. Es werden +dann keine Zugangsdaten aus 1Password geholt und es findet kein Registry-Login +statt. Die CI eines Projekts nutzt damit dieselbe Bau-Logik wie das Deployment, +statt sie zu duplizieren. + +Weder Secrets noch Versionsangaben sind dafür nötig: + +```yaml +jobs: + build-check: + uses: workflow-util/pipelines/.gitea/workflows/docker-build-push.yml@v1 + with: + container-name: bk-dev-website + dockerfile: ./Dockerfile + docker-context: . + image-owner: bekemeyer-klein + push: false +``` + +> **Private Submodule funktionieren in diesem Modus nicht.** +> Der Checkout-Token stammt aus demselben 1Password-Schritt, der ohne Push +> übersprungen wird. Der Checkout fällt deshalb auf den Standard-Token des Jobs +> zurück. Der genügt für das ausführende Repository, nicht aber für die +> Submodule in eigenen privaten Repositories — deren Klon scheitert mit +> `Repository not found`. Projekte **mit** privaten Submodulen können +> `push: false` daher nicht verwenden; für Projekte ohne Submodule ist der +> Modus unproblematisch. + +`secrets: inherit` kann hier entfallen. Es zu belassen schadet nicht — der +Workflow greift ohne Push auf kein Secret zu. + #### Voraussetzungen im aufrufenden Repository | Name | Art | Zweck | | ------------------ | -------- | ----- | -| `OP_CONNECT_HOST` | Variable | Adresse des 1Password-Connect-Servers im Cluster | -| `OP_CONNECT_TOKEN` | Secret | Zugang zu diesem Connect-Server | +| `OP_CONNECT_HOST` | Variable | Adresse des 1Password-Connect-Servers im Cluster. Nur beim Push nötig | +| `OP_CONNECT_TOKEN` | Secret | Zugang zu diesem Connect-Server. Nur beim Push nötig | | `DOCKER_HOST` | Variable | Nur nötig, wenn der Runner-Daemon rootless läuft und sein Socket nicht unter `/var/run/docker.sock` liegt (dann z. B. `unix:///run/user/1000/docker.sock`). Ist die Variable nicht gesetzt, nutzt Docker seinen Default. | Die eigentlichen Zugänge für Checkout und Registry liegen in 1Password