Warum @main ein stiller Breaking-Change-Kanal ist
Sobald du einen zentralen Reusable Workflow oder eine Composite Action baust, auf die mehrere Repos per uses: verweisen, hast du ein Verteilungsproblem geschaffen, das mit dem eigentlichen Workflow-Code nichts mehr zu tun hat. Der Code selbst kann fehlerfrei sein. Trotzdem reicht ein einziger Merge in main, um zehn Consumer-Repos gleichzeitig rot zu machen, ohne dass jemand einen Pull Request in einem dieser Repos angefasst hat.
Der Grund ist die Referenzierung selbst:
jobs: build: uses: meine-org/ci-workflows/.github/workflows/build.yml@main
@main bindet den Consumer an den aktuellen Stand des Branches, nicht an einen Stand, den irgendjemand geprüft hat. Wenn du im zentralen Repo ein Input umbenennst, einen Default änderst oder eine neue Pflichtangabe einführst, gilt das ab dem nächsten Push für jeden Consumer, der @main referenziert. Niemand im Consumer-Repo bekommt eine Benachrichtigung. Der erste Hinweis ist ein roter Check, oft Tage später, wenn ohnehin schon jemand unter Zeitdruck einen Release bauen will.
Das Problem ist nicht GitHub Actions. Es ist die Annahme, dass ein Branch ein stabiler Referenzpunkt sei. Er ist genau das Gegenteil: der instabilste Punkt im Repo.
Major-Tags als Vertrag, nicht als Empfehlung
Die Lösung ist nicht kompliziert, sie erfordert nur Disziplin: du referenzierst semantische Tags statt Branches.
jobs: build: uses: meine-org/ci-workflows/.github/workflows/build.yml@v1
Entscheidend ist, was @v1 bedeutet. Ein Major-Tag ist ein Vertrag: solange ein Consumer @v1 referenziert, darfst du im zentralen Repo Bugfixes liefern, neue optionale Inputs mit sinnvollem Default hinzufügen, Performance verbessern. Du darfst nicht: ein Pflicht-Input entfernen, einen Output umbenennen, ein Verhalten so ändern, dass ein bestehender Aufruf anders reagiert als vorher. Das ist dieselbe Logik wie bei Semantic Versioning für Bibliotheken, nur dass der Consumer hier ein YAML-Aufruf statt ein Import ist.
Praktisch heißt das: v1 zeigt nicht auf einen Commit, sondern auf einen beweglichen Zeiger, der bei jedem Fix/Feature-Release aktualisiert wird, während der jeweilige konkrete Stand zusätzlich unter einem unveränderlichen Tag liegt.
git tag -a v1.4.0 -m "fix: cache key normalisieren"git tag -f v1 v1.4.0git push origin v1.4.0git push origin v1 --force
Der Force-Push beim Major-Tag ist hier bewusst, das ist der einzige Ort, an dem ein Force-Push auf einen Tag Teil des Designs ist, nicht ein Unfall. Wer stattdessen exakt pinnen will, referenziert @v1.4.0 statt @v1. Beide Optionen bleiben offen, das eine ist nur bequemer, das andere nur reproduzierbarer.
Breaking Changes ankündigen statt überraschen
Ein Major-Tag löst das Referenzierungsproblem, aber nicht die Frage, was passiert, wenn du wirklich eine Breaking Change brauchst, etwa weil ein Input strukturell falsch war oder eine veraltete Action-Version ersetzt werden muss.
Dafür brauchst du drei Dinge, die im zentralen Repo dokumentiert leben, nicht nur im Kopf desjenigen, der den Workflow zuletzt geändert hat.
Erstens ein Changelog im zentralen Repo, das pro Major-Version festhält, was sich geändert hat und ab wann. Zweitens ein Deprecation-Fenster: v1 wird nicht am Tag des v2-Releases abgeschaltet, sondern bleibt für eine angekündigte Frist parallel lauffähig, damit Consumer-Repos migrieren können, statt migrieren zu müssen.
.github/workflows/build-v1.yml # Tag v1 zeigt hierauf, nur noch Fixes.github/workflows/build.yml # Tag v2 zeigt hierauf, aktive Entwicklung
Drittens eine sichtbare Kennzeichnung im Workflow selbst, etwa ein Kommentar oder eine Warnung im Job-Summary, wenn ein Consumer noch auf der auslaufenden Major-Version steht. Das ersetzt keine Kommunikation, aber es macht die Frist im Interface sichtbar, nicht nur im Changelog, das ohnehin niemand liest, bevor es brennt.
Ein Test-Repo, bevor ein Tag geschnitten wird
Der Teil, der am häufigsten fehlt: bevor du v1.4.0 oder gar v2.0.0 schneidest, muss der neue Stand gegen mehrere Consumer-Szenarien laufen, nicht nur gegen den einen internen Test, den der Autor gerade im Kopf hatte.
Dafür lohnt sich ein eigenes, kleines Repo, dessen einziger Zweck ist, den zentralen Workflow mit unterschiedlichen, realistischen Eingaben aufzurufen.
name: Zentraler Workflow gegen Consumer-Szenarienon: workflow_dispatch: push: branches: [main]jobs: szenario: strategy: matrix: include: - name: minimal-input with-cache: false - name: mit-cache with-cache: true - name: monorepo-pfad with-cache: true uses: meine-org/ci-workflows/.github/workflows/build.yml@feature-branch with: cache: ${{ matrix.with-cache }} secrets: inherit
Der entscheidende Punkt ist die Referenz: während der Entwicklung zeigt das Test-Repo auf den Feature-Branch des zentralen Workflows, nicht auf @main und nicht auf einen Tag. Erst wenn die Matrix grün ist, wird der Branch gemerged und ein neues Tag geschnitten. Ohne dieses Zwischenglied ist der erste echte Test ein Produktions-Consumer, und der merkt es dann so, wie oben beschrieben: durch einen roten Check ohne eigene Änderung.
Was das hier nicht löst
Versionierung beantwortet, wie sich ein zentraler Workflow verändern darf, ohne Consumer zu überraschen. Sie beantwortet nicht, wer diese Änderung überhaupt vornehmen darf, wer ein Tag schneiden darf, welches Team Approval-Rechte auf das zentrale Repo hat. Das ist ein Berechtigungscluster, kein Versionierungsproblem, und verdient eine eigene Betrachtung statt eines Nebensatzes hier.
Wer sich für die Frage interessiert, wie Governance-Fragen wie diese über mehrere Teams hinweg organisatorisch skalieren, findet auf digital-business.blog mehr dazu, dort geht es um genau diese Zuständigkeitsfragen jenseits des einzelnen YAML-Files.
Hinterlasse einen Kommentar