Compare commits
6
Commits
75c309ac23
..
v4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0f7cd04c70
|
||
|
|
6123f469f6
|
||
|
|
7d0c1b9c22
|
||
|
|
b82d0a2155 | ||
|
|
47b77fac54 | ||
|
|
18f672ed2a |
No files matched your search
@@ -0,0 +1,267 @@
|
|||||||
|
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 "<registry>/<owner>/<name>:" und liesse den Build an
|
||||||
|
# "invalid reference format" scheitern. 0.0.0 ist ein gueltiger, sichtbar
|
||||||
|
# unechter Wert.
|
||||||
|
version-number:
|
||||||
|
required: false
|
||||||
|
default: 0.0.0
|
||||||
|
type: string
|
||||||
|
version-number-major:
|
||||||
|
required: false
|
||||||
|
default: '0'
|
||||||
|
type: string
|
||||||
|
version-number-minor:
|
||||||
|
required: false
|
||||||
|
default: '0'
|
||||||
|
type: string
|
||||||
|
version-number-patch:
|
||||||
|
required: false
|
||||||
|
default: '0'
|
||||||
|
type: string
|
||||||
|
# Bewusst `type: string` und nicht `boolean` — siehe Issue #4. Auf dieser
|
||||||
|
# Gitea-Version (gemessen an 1.27.0) kommt eine boolesche
|
||||||
|
# workflow_call-Eingabe im aufgerufenen Workflow gar nicht an: weder ihre
|
||||||
|
# Vorgabe noch ein ausdruecklich uebergebenes `push: true`. `inputs.push`
|
||||||
|
# war in beiden Faellen leer, der Baustein baute immer im Pruefmodus und
|
||||||
|
# meldete dabei Erfolg. Alle Eingaben vom Typ string desselben Laufs
|
||||||
|
# (platforms, registry, image-owner, container-name) kamen korrekt an;
|
||||||
|
# push war die einzige boolesche und die einzige, die versagte.
|
||||||
|
#
|
||||||
|
# Wer das beim naechsten Aufraeumen "korrekt" auf boolean zurueckstellt,
|
||||||
|
# holt den Fehler still zurueck: nichts wird veroeffentlicht, und der
|
||||||
|
# Lauf bleibt gruen.
|
||||||
|
#
|
||||||
|
# Die Bedingung `inputs.push && inputs.push != 'false'` musste dafuer
|
||||||
|
# nicht angefasst werden. Sie wurde seinerzeit ausdruecklich so gebaut,
|
||||||
|
# dass sie Boolean und String vertraegt (Begruendung am
|
||||||
|
# 1Password-Schritt), und traegt die Umstellung unveraendert: 'true' ist
|
||||||
|
# truthy und ungleich 'false', 'false' ist truthy aber gleich 'false'.
|
||||||
|
push:
|
||||||
|
description: >-
|
||||||
|
Whether the built image is pushed, as the string 'true' or 'false'.
|
||||||
|
Deliberately a string, not a boolean: boolean workflow_call inputs do
|
||||||
|
not reach the called workflow on this Gitea version (see issue #4).
|
||||||
|
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: string
|
||||||
|
container-name:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
dockerfile:
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
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
|
||||||
|
<registry>/<owner>/<container-name>. Used to be hardcoded to "suer",
|
||||||
|
which sent every image of every calling project to the same account.
|
||||||
|
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: 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.'
|
||||||
|
required: false
|
||||||
|
default: 45
|
||||||
|
type: number
|
||||||
|
platforms:
|
||||||
|
description: >-
|
||||||
|
Target platforms of the pushed manifest list. The build stages of all
|
||||||
|
dockerfiles are pinned to the build platform, so an extra platform only
|
||||||
|
adds its runtime stage, not another compile run.
|
||||||
|
required: false
|
||||||
|
default: linux/amd64,linux/arm64
|
||||||
|
type: string
|
||||||
|
build-args:
|
||||||
|
description: >-
|
||||||
|
Extra build arguments for the image, one NAME=value per line. The docs
|
||||||
|
image uses it to stamp the released version into writerside.cfg.
|
||||||
|
required: false
|
||||||
|
default: ''
|
||||||
|
type: string
|
||||||
|
|
||||||
|
name: build and push Docker Image
|
||||||
|
|
||||||
|
# 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:
|
||||||
|
name: Build ${{ inputs.container-name }} Container
|
||||||
|
permissions:
|
||||||
|
packages: write
|
||||||
|
contents: read
|
||||||
|
runs-on: [ ubuntu-docker ]
|
||||||
|
timeout-minutes: ${{ inputs.timeout-minutes }}
|
||||||
|
env:
|
||||||
|
# Ohne Angabe suchen Buildx und die Docker-CLI unter
|
||||||
|
# unix:///var/run/docker.sock. Laeuft der Daemon des Runners rootless,
|
||||||
|
# liegt sein Socket stattdessen unter /run/user/<uid>/docker.sock. Der
|
||||||
|
# Pfad kommt deshalb aus einer Repository-Variablen; ist sie nicht
|
||||||
|
# gesetzt, bleibt die Variable leer und Docker nutzt seinen Default.
|
||||||
|
DOCKER_HOST: ${{ vars.DOCKER_HOST }}
|
||||||
|
steps:
|
||||||
|
# Issue #4 blieb so lange unbemerkt, weil nirgends sichtbar war, was
|
||||||
|
# ueberhaupt ankommt — und weil ein Lauf ohne Push genauso gruen aussieht
|
||||||
|
# wie einer mit. Der Schritt macht beides sichtbar: den rohen Wert der
|
||||||
|
# Eingabe und den daraus abgeleiteten Modus. Gleiches Muster wie
|
||||||
|
# "Docker-Socket ermitteln": rein informativ, mit `|| true` abgesichert,
|
||||||
|
# damit er den Job unter keinen Umstaenden scheitern laesst.
|
||||||
|
#
|
||||||
|
# Der Wert wird ueber die Umgebung gereicht statt in die Kommandozeile
|
||||||
|
# interpoliert; so bleibt er auch dann harmlos, wenn ein Aufrufer etwas
|
||||||
|
# Unerwartetes uebergibt.
|
||||||
|
- name: Eingaben melden
|
||||||
|
env:
|
||||||
|
PUSH_ROH: ${{ inputs.push }}
|
||||||
|
PUSH_MODUS: ${{ (inputs.push && inputs.push != 'false') && 'veroeffentlichen' || 'nur bauen' }}
|
||||||
|
run: |
|
||||||
|
echo "push (roh): '${PUSH_ROH}'" || true
|
||||||
|
echo "Modus: ${PUSH_MODUS}" || true
|
||||||
|
|
||||||
|
# Der Token, den Gitea jedem Job stellt, taugt fuer zwei Dinge nicht:
|
||||||
|
# Er gilt nur fuer das ausfuehrende Repository, waehrend die Submodule in
|
||||||
|
# eigenen, privaten Repositories liegen — deren Klon scheitert an
|
||||||
|
# "Repository not found". Und die Container-Registry lehnt ihn ab
|
||||||
|
# (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.
|
||||||
|
#
|
||||||
|
# Genau deshalb blieb sie bei der Umstellung von push auf `type: string`
|
||||||
|
# (Issue #4) unveraendert: sie war von Anfang an fuer beide Schreibweisen
|
||||||
|
# gebaut. 'true' ergibt wahr, 'false' ergibt falsch, ein leerer Wert
|
||||||
|
# ergibt falsch — und die Boolean-Faelle gelten unveraendert weiter,
|
||||||
|
# falls ein Aufrufer doch einmal ein YAML-Boolean uebergibt.
|
||||||
|
- 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
|
||||||
|
# Umgebung aller folgenden Schritte wandern.
|
||||||
|
export-env: false
|
||||||
|
env:
|
||||||
|
OP_CONNECT_HOST: ${{ vars.OP_CONNECT_HOST }}
|
||||||
|
OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }}
|
||||||
|
# Klont die privaten Submodul-Repositories.
|
||||||
|
CHECKOUT_TOKEN: op://GitHub-Actions/jlab-git-checkout-token/credential
|
||||||
|
# Eigener Zugang fuer die Registry — der Checkout-Token darf dort
|
||||||
|
# nicht schreiben. Benutzername und Token kommen aus demselben Item,
|
||||||
|
# damit sie zwangslaeufig zusammenpassen; die Registry prueft den
|
||||||
|
# Token gegen genau das Konto aus `username`.
|
||||||
|
REGISTRY_USER: op://GitHub-Actions/jlab-registry-access/username
|
||||||
|
REGISTRY_TOKEN: op://GitHub-Actions/jlab-registry-access/password
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
submodules: true
|
||||||
|
# Die Action legt je Referenz einen Output an, benannt wie die
|
||||||
|
# Umgebungsvariable, in der die op://-Referenz stand.
|
||||||
|
#
|
||||||
|
# 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.
|
||||||
|
- name: Docker-Socket ermitteln
|
||||||
|
run: |
|
||||||
|
echo "DOCKER_HOST=${DOCKER_HOST:-<leer, Default wird genutzt>}"
|
||||||
|
ls -l /var/run/docker.sock /run/docker.sock 2>&1 || true
|
||||||
|
ls -l /run/user/*/docker.sock 2>&1 || true
|
||||||
|
docker version 2>&1 || true
|
||||||
|
|
||||||
|
- 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: ${{ inputs.registry }}
|
||||||
|
username: ${{ steps.op-secrets.outputs.REGISTRY_USER }}
|
||||||
|
password: ${{ steps.op-secrets.outputs.REGISTRY_TOKEN }}
|
||||||
|
- name: Build
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
builder: ${{ steps.buildx.outputs.name }}
|
||||||
|
context: ${{ inputs.docker-context }}
|
||||||
|
file: ${{ inputs.dockerfile }}
|
||||||
|
platforms: ${{ inputs.platforms }}
|
||||||
|
build-args: ${{ inputs.build-args }}
|
||||||
|
# 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: |
|
||||||
|
${{ 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 }}
|
||||||
@@ -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 }}
|
||||||
@@ -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.
|
||||||
@@ -1 +1,395 @@
|
|||||||
# Hier stehen wichtige Infos!
|
# pipelines
|
||||||
|
|
||||||
|
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`
|
||||||
|
|
||||||
|
Baut ein Docker-Image und schiebt es als Multi-Arch-Manifest
|
||||||
|
(`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:
|
||||||
|
|
||||||
|
```
|
||||||
|
<registry>/<image-owner>/<container-name>:latest
|
||||||
|
<registry>/<image-owner>/<container-name>:<version-number>
|
||||||
|
<registry>/<image-owner>/<container-name>:<major>
|
||||||
|
<registry>/<image-owner>/<container-name>:<major>.<minor>
|
||||||
|
<registry>/<image-owner>/<container-name>:<major>.<minor>.<patch>
|
||||||
|
```
|
||||||
|
|
||||||
|
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 |
|
||||||
|
| ---------------------- | ------- | ------------------------- | --------- |
|
||||||
|
| `container-name` | ja | — | Name des Images |
|
||||||
|
| `dockerfile` | ja | — | Pfad zum Dockerfile |
|
||||||
|
| `docker-context` | ja | — | Build-Kontext |
|
||||||
|
| `image-owner` | ja | — | Eigentümer-Segment des Images, also der mittlere Teil von `<registry>/<owner>/<name>` |
|
||||||
|
| `push` | nein | `'true'` | Zeichenkette `'true'` oder `'false'`. 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.
|
||||||
|
|
||||||
|
`push` ist eine Zeichenkette und kein Boolean — und das ist Absicht. Auf dieser
|
||||||
|
Gitea-Version (gemessen an 1.27.0) werden **boolesche `workflow_call`-Eingaben
|
||||||
|
nicht an den aufgerufenen Workflow durchgereicht**: Weder ihre Vorgabe noch ein
|
||||||
|
ausdrücklich übergebenes `push: true` kamen an, `inputs.push` blieb leer, und
|
||||||
|
der Baustein baute immer im Prüfmodus — bei grünem Lauf. Alle Eingaben vom Typ
|
||||||
|
`string` kamen im selben Lauf korrekt an. Details in
|
||||||
|
[Issue #4](https://git.jk-dev.de/workflow-util/pipelines/issues/4).
|
||||||
|
|
||||||
|
Deshalb bitte immer in Anführungszeichen schreiben: `push: 'true'` bzw.
|
||||||
|
`push: 'false'`. Ein unquotiertes `push: false` ist im YAML ein Boolean und
|
||||||
|
wird verschluckt.
|
||||||
|
|
||||||
|
> **Brechende Änderung gegenüber `v1`.** Aufrufer, die `push` als YAML-Boolean
|
||||||
|
> übergeben, müssen auf `@v2` wechseln und dabei auf die Schreibweise mit
|
||||||
|
> Anführungszeichen umstellen. Wer `@v1` weiterverwendet, behält das alte
|
||||||
|
> Verhalten samt Fehler.
|
||||||
|
|
||||||
|
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
|
||||||
|
`…/<name>:`.
|
||||||
|
|
||||||
|
#### Aufrufbeispiel
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
image:
|
||||||
|
uses: workflow-util/pipelines/.gitea/workflows/docker-build-push.yml@v2
|
||||||
|
with:
|
||||||
|
version-number: 2026.09.1
|
||||||
|
version-number-major: '2026'
|
||||||
|
version-number-minor: '09'
|
||||||
|
version-number-patch: '1'
|
||||||
|
container-name: bk-dev-website
|
||||||
|
dockerfile: ./Dockerfile
|
||||||
|
docker-context: .
|
||||||
|
image-owner: bekemeyer-klein
|
||||||
|
push: 'true'
|
||||||
|
secrets: inherit
|
||||||
|
```
|
||||||
|
|
||||||
|
`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@v2
|
||||||
|
with:
|
||||||
|
container-name: bk-dev-website
|
||||||
|
dockerfile: ./Dockerfile
|
||||||
|
docker-context: .
|
||||||
|
image-owner: bekemeyer-klein
|
||||||
|
push: 'false'
|
||||||
|
```
|
||||||
|
|
||||||
|
Die Anführungszeichen sind nicht kosmetisch: `push: false` ohne sie ist ein
|
||||||
|
YAML-Boolean und kommt im Baustein nicht an (siehe oben). Es griffe dann die
|
||||||
|
Vorgabe `'true'`, der Lauf versuchte also trotz `push: false` zu
|
||||||
|
veröffentlichen — sofern Gitea die Typabweichung nicht ohnehin als Fehler
|
||||||
|
zurückweist.
|
||||||
|
|
||||||
|
> **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. 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
|
||||||
|
(`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:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
uses: workflow-util/pipelines/.gitea/workflows/docker-build-push.yml@v2 # so
|
||||||
|
uses: workflow-util/pipelines/.gitea/workflows/docker-build-push.yml@main # nicht so
|
||||||
|
```
|
||||||
|
|
||||||
|
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 Tag macht das Übernehmen einer neuen Fassung zu einer bewussten Entscheidung
|
||||||
|
im jeweiligen Projekt.
|
||||||
|
|
||||||
|
Änderungen hier bekommen deshalb einen neuen Tag. Bricht eine Änderung
|
||||||
|
bestehende Aufrufe, wird der Major-Anteil erhöht.
|
||||||
|
|
||||||
|
`v2` ist genau so ein Fall: Die Umstellung von `push` auf `type: string` bricht
|
||||||
|
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.
|
||||||
Reference in new issue
Block a user