Kurzfassung Fast alles, was über Branching-Strategien geschrieben wird, geht davon aus, dass sich ein Update gleichzeitig an alle ausrollen und im Zweifel in Minuten zurückrollen lässt. SPS-Software läuft auf physischen Maschinen, die eine bestimmte Version oft jahrelang betreiben, teilweise auf Hardware, bei der ein Wechsel der Major-Version kein Download ist, sondern ein Serviceeinsatz. Damit fallen die meisten verbreiteten Modelle weg, und übrig bleiben langlebige Release-Branches. Dieser Beitrag stellt die Optionen vor und geht dann auf das ein, was Teams wirklich Zeit kostet: was parallele Release-Linien mit Pipeline-Triggern, Versionsableitung und Library-Auflösung anstellen, und wie man das Modell in einem Repository nachrüstet, das längst ausgeliefert ist.
Die Branching-Konzepte hier gelten allgemein, aber die Tooling-Referenzen beziehen sich auf TwinCAT und das Beckhoff-Ecosystem. Wir entwickeln CI/CD- und Paketmanagement-Tooling für TwinCAT-Teams, weshalb die Fragen zur Versionsverwaltung weiter unten keine Theorie sind, sondern Probleme, die wir in unseren eigenen Produkten lösen mussten.
Das ist der dritte Beitrag unserer Serie zu CI/CD für TwinCAT. Wer noch überhaupt keine Pipeline am Laufen hat, findet die Optionen dafür im Beitrag zur Build-Tooling-Landschaft; dieser hier setzt voraus, dass etwas läuft, und behandelt die Frage, wie die Branches es füttern sollen. Wer direkt zum Vergleich springen möchte: Es gibt eine Übersichtstabelle am Ende.
Warum Maschinensoftware eine durchdachte Branching-Strategie braucht
Wenn Teams CI/CD einrichten, steht die Branching-Strategie meist nicht auf der Agenda. Das unmittelbare Ziel ist, dass überhaupt eine Pipeline läuft: main als Quelle eintragen, ein Build-Skript zusammenbauen (irgendwo kopiert, per LLM generiert oder selbst geschrieben), fertig. Und eine Weile, manchmal erstaunlich lange, reicht das auch aus.
Wenn ein Projekt wirklich ein einzelnes Produkt für einen einzelnen Kunden ist und nie mehr als eine Version gleichzeitig gepflegt werden muss, ist das in Ordnung. Der Rest dieses Beitrags richtet sich an Teams, die diese Annahme nicht treffen können, also in der Praxis die meisten Maschinenbauer, sobald sie mehr als einen Kunden oder mehr als eine Maschinenvariante im Feld haben.
Web- und SaaS-Teams können einen Commit pushen und ihn innerhalb von Minuten für alle Nutzer live schalten; wenn etwas schiefläuft, wird zurückgerollt. SPS-Software wird auf einer physischen Maschine ausgeliefert, die genau diese Version vielleicht fünf Jahre lang betreibt. Eine Abfüllanlage in einer Fabrik wird nicht alle zwei Wochen ein Software-Update einspielen. Das Wartungsfenster beim Kunden gibt es einmal im Quartal, wenn man Glück hat. Gleichzeitig laufen drei andere Kunden auf v1.2.1, einer noch auf v1.1.4, und man selbst ist schon bei v2.0.
Dazu kommt: Ein neues Software-Release hängt oft von neuen oder anderen Hardware-Komponenten ab: ein anderer Sensor, ein neues I/O-Modul, ein geändertes Antriebssystem. Die Maschine im Feld ist Teil des Releases. Einen Kunden von v1.x auf v2.x zu aktualisieren kann einen Servicebesuch, physische Hardware-Änderungen, Umverkabelung und eine erneute Inbetriebnahme bedeuten. “Einfach auf die neueste Version updaten” kann ohne eine wesentliche Änderung an der installierten Maschine physisch unmöglich sein, und das liegt ausschließlich in der Entscheidung des Kunden, auf seinem Zeitplan und mit seinem Budget.
Das bedeutet: Der installierte Maschinenpark ist kein vorübergehender Zustand, den man ignorieren kann. Manche Maschinen bleiben auf v1.x, und sie müssen trotzdem unterstützt werden. Der Bedarf zeigt sich typischerweise in einem dieser Momente:
- Maschine A ist ausgeliefert und in Betrieb. Die Entwicklung von Maschine B beginnt (eine andere Variante, andere Hardware, anderes Förderkonzept). Der Code von Maschine A ist “fertig”, aber der Kunde ruft an, wenn etwas nicht stimmt. Den Code von Maschine B auf Maschine A zu spielen ist keine Option.
- v1.0 wurde vor sechs Monaten ausgeliefert. Drei Kunden betreiben sie. Man ist mitten in v2.0. Ein Kunde meldet einen Bug in v1.0. Man öffnet das Repository, und es gibt keinen Branch, der diese Version repräsentiert. Den Fix sauber einzupflegen bedeutet, gegen einen rohen Commit zu arbeiten, oder den Kunden auf das nächste Update warten zu lassen.
- Man fügt dem v2.0-Projekt eine gemeinsame Library hinzu. Die neue Version dieser Library bricht die API. Der v1.x-Build bricht mit. Die einzige Pipeline ist jetzt aus dem falschen Grund defekt, auf einem Branch, an dem gar nicht aktiv entwickelt wird.
Bei Libraries vervielfacht sich das Problem. Sobald v1.x und v2.x als Release-Linien existieren, muss jede gemeinsame Library, von der das Projekt abhängt, demselben Muster folgen, und der Dependency-Resolver muss dieses Muster ebenfalls kennen.
Keines dieser Probleme liegt am CI/CD-Tooling. Sie entstehen, weil nie entschieden wurde, welches Verhältnis zwischen Branches und Releases bestehen soll, und die Pipeline macht die Lücke nur schneller sichtbar. Trigger, Artefakt-Versionen und die Antwort eines Resolvers auf die Frage nach “dem neuesten v1” ergeben sich alle aus dem Branching-Modell. Genau deshalb lohnt es sich, diese Entscheidung bewusst zu treffen, statt zu warten, bis die Situation sie erzwingt.
Die Strategien im Überblick
Trunk-based Development
Trunk-based Development ist das Modell von Google und Meta: Alle committen auf einen einzigen main, Feature-Flags verstecken unfertige Arbeit, und der Trunk ist immer deploybar. Es setzt sehr schnelle automatisierte Tests voraus, ein ausgereiftes Feature-Flag-System und die Möglichkeit, alle Nutzer aus einem einzigen Artefakt zu versorgen. Für SPS-Software trifft nichts davon zu: Builds brauchen echte Windows-Hardware, und einen Knopf für “gleichzeitig auf alle Maschinen deployen” gibt es nicht. Es schadet nicht, das Muster zu kennen, hierher passt es aber nicht. Wer herausfindet, wie man eine Förderstrecke per Feature-Flag nachrüstet, kann sich gerne melden.
GitHub Flow
GitHub Flow (Scott Chacon, 2011) reduziert alles auf eine Regel: main ist immer deploybar, Feature-Branches sind kurzlebig, nach dem Merge sofort deployen. Für SaaS-Teams lässt sich das kaum kritisieren. Die Einschränkung für Maschinensoftware ist, dass es keinen vorgesehenen Weg gibt, eine ausgelieferte Version weiterzupflegen: Sobald v2.0 getaggt ist, ist v1.x ein alter Commit ohne strukturierten Ort für einen v1.2.1-Hotfix. Die meisten Teams starten hier und wachsen mit dem zweiten Kunden daraus heraus.
Git Flow
Git Flow wurde 2010 von Vincent Driessen eingeführt und war besonders in Java- und Enterprise-Software-Communities weit verbreitet. Es definiert ein festes Branching-Modell mit zwei permanenten Branches: main (spiegelt immer den Produktionszustand) und develop (Integrations-Branch für das nächste Release).
gitGraph commit id: "init" branch develop checkout develop commit id: "dev work" branch feature/conveyor checkout feature/conveyor commit id: "conveyor POU" type: HIGHLIGHT checkout develop merge feature/conveyor branch "release/1.0" checkout "release/1.0" commit id: "version bump" type: HIGHLIGHT checkout main merge "release/1.0" tag: "v1.0" checkout develop merge "release/1.0" checkout main branch "hotfix/1.0.1" checkout "hotfix/1.0.1" commit id: "critical fix" type: HIGHLIGHT checkout main merge "hotfix/1.0.1" tag: "v1.0.1" checkout develop merge "hotfix/1.0.1"
main und develop sind permanent (●). feature/*-, release/*- und hotfix/*-Branches sind kurzlebig und werden nach dem Mergen gelöscht (◆).
Die zwei permanenten Branches trennen klar zwischen “was in der Produktion ist” und “was als nächstes gebaut wird”, mit expliziten hotfix/*-Branches für dringende Produktions-Fixes. Driessen selbst hat dem Originalbeitrag 2020 eine Anmerkung hinzugefügt, in der er feststellt, dass das Modell zu versionierter Software passt, die sich nicht einfach zurückrollen lässt, und das beschreibt Maschinensoftware ziemlich genau.
Der Haken ist, dass die release/*-Branches in Git Flow temporär sind. Sie existieren, um ein Release zu stabilisieren, und werden nach dem Mergen gelöscht. Es ist also nicht vorgesehen, v1.x über Jahre zu pflegen, während v2.x parallel aktiv entwickelt wird. Wer Kunden hat, die auf v1.x bleiben, dehnt Git Flow über seinen ursprünglichen Anwendungsbereich hinaus. Wem genau diese Trennung gefällt: Release Flow behält sie und macht die Release-Branches permanent.
GitLab Flow
GitLab Flow ist eher ein Rahmen als ein fest definiertes Modell. Es nimmt GitHub Flow als Ausgangspunkt und definiert zwei Erweiterungsmuster für Teams, die mehr Struktur brauchen.
Mit Environment-Branches: Code fließt von main durch Environment-Branches, die jeweils ein Deployment-Ziel repräsentieren, zum Beispiel main → staging → pre-production → production. Commits fließen nur in diese Richtung, was sicherstellt, dass alles in jeder Umgebung getestet wird, bevor es die Produktion erreicht. Hotfixes werden typischerweise auf einem Feature-Branch entwickelt, in main gemergt und dann Stufe für Stufe weitergereicht.
gitGraph commit branch staging commit checkout main branch production checkout main branch "feature/auth" checkout "feature/auth" commit type: HIGHLIGHT checkout main merge "feature/auth" checkout staging merge main checkout production merge staging checkout main branch "feature/conveyor" checkout "feature/conveyor" commit type: HIGHLIGHT checkout main merge "feature/conveyor" checkout staging merge main
main, staging und production sind permanent (●). feature/*-Branches sind kurzlebig (◆). Code fließt nur in eine Richtung: Features landen auf main und werden dann durch jede Umgebung weitergereicht.
Mit Release-Branches: Für Teams, die mehrere Versionen gleichzeitig pflegen müssen, unterstützt GitLab Flow auch das Abspalten von v1- und v2-Branches (oder release/1.x, release/2.x) von main und ihre unabhängige Weiterentwicklung. In diesem Modus nähert es sich dem an, was Release Flow formalisiert, weshalb das Diagramm im nächsten Kapitel für beide gilt.
Environment-Branching zahlt sich vor allem aus, wenn das Artefakt nicht nur Code ist. Wird Software zusammen mit einer Konfigurationsdatenbank oder einer anderen zustandsbehafteten Komponente ausgeliefert, werden parallele Versions-Branches schnell mühsam, weil das Schema auf release/1.x von dem auf release/2.x abweicht und man am Ende zwei Migrationshistorien pflegt. Ein einziger Promotion-Pfad vermeidet das komplett.
Maschinensoftware ist dagegen meist in sich geschlossen: Code plus Library-Abhängigkeiten, ohne externe Datenbank, die sich parallel weiterentwickelt. Die Einschränkung, die Environment-Branching attraktiv macht, greift deshalb selten, und damit ist der Release-Branch-Modus das relevantere Muster.
Release Flow
Release Flow und der Release-Branch-Modus von GitLab Flow sehen strukturell fast gleich aus. Der Unterschied liegt in den Konventionen und darin, was die Pipeline daraus ableitet. Release Flow legt ein einheitliches Benennungsschema fest (release/1.x, release/2.x), definiert genau, wo Hotfixes landen (Branch von release/1.x, Cherry-pick nach main), und macht die Versionsnummerierung explizit: Die Version ergibt sich aus dem Branch-Namen, nicht aus einer manuell gepflegten Variable. Genau diese Festlegungen machen Pipeline und Paketmanager über alle aktiven Versionslinien hinweg vorhersehbar.
Der Begriff “Release Flow” wurde vom Azure DevOps-Team bei Microsoft in einem Blogbeitrag von 2018 geprägt, der beschreibt, wie das Team sein eigenes Produkt entwickelt. Das zugrundeliegende Muster (Release-Branches von einem stabilen main) ist älter und im Buch Continuous Delivery (Humble & Farley, 2010) unter “Release Branching” dokumentiert. releaseflow.org ist eine gute moderne Referenz.
Das Modell ist geradlinig. Entwicklung läuft auf main. Wenn ein Release bereit ist, wird ein release/1.x-Branch abgespalten. Bugfixes und Wartung für diese Version erfolgen auf dem Branch und werden per Cherry-pick nach main übertragen. Wenn v2.0 bereit ist, wird release/2.x abgespalten, und der Zyklus wiederholt sich. Beide Branches laufen dann parallel auf unbestimmte Zeit.
gitGraph commit id: "dev work" commit id: "v1 feature complete" branch "release/1.x" checkout "release/1.x" commit id: "v1.0" tag: "v1.0" commit id: "v1.1" tag: "v1.1" checkout main commit id: "v2 features" commit id: "v2 complete" branch "release/2.x" checkout "release/2.x" commit id: "v2.0" tag: "v2.0" checkout main commit id: "ongoing dev" checkout "release/1.x" commit id: "v1.2" tag: "v1.2"
main, release/1.x und release/2.x sind alle permanent (●). Jeder Release-Branch wird so lange gepflegt, wie Maschinen mit dieser Version im Einsatz sind.
Microsofts eigene Variante ist auf einen Cloud-Dienst optimiert, der jeden Sprint in dieselbe Produktionsumgebung deployed: Hotfixes gehen immer zuerst auf main und werden dann per Cherry-pick auf den aktuellen Release-Branch übertragen. Für Maschinensoftware ist die relevantere Variante die mit langlebigen Versions-Branches: release/1.x kann jahrelang gepflegt werden, während release/2.x parallel entwickelt wird, weil das Update einer Maschine im Feld oft einen formellen Inbetriebnahmeprozess erfordert und in manchen Fällen Hardware-Änderungen, durch die der Wechsel der Major-Version selbst zum Projekt wird.
Der Entwicklungszyklus Schritt für Schritt:
- Alle Feature-Arbeiten laufen auf
main. CI läuft bei jedem Push, mit derselben Build-Pipeline, die später auch auf dem Release-Branch läuft. - Wenn das Team Feature-Complete für v1 erklärt, wird
release/1.xvonmainabgespalten. Dieser Branch ist ab jetzt die v1-Versionslinie. - Der CI-Trigger wird auf
mainundrelease/**erweitert. Dieselbe Pipeline-Definition läuft auf beiden; nur die Versionierung unterscheidet sich. - Ein Bug wird auf einer Maschine mit v1.1 gemeldet. Fix auf
release/1.x, Tagv1.2, CI baut das Artefakt. Cherry-pick des Fixes nachmain, damit er nicht in v2 wieder auftaucht. - Wenn v2-Features fertig sind, wird
release/2.xvonmainabgespalten. Jetzt laufen zwei Release-Branches parallel, jeder mit eigenem CI und eigenem Artefakt-Stream. - Ein Fix, der sowohl v1.x als auch v2.x betrifft, muss auf beide Branches aufgebracht werden. Das ist Doppelarbeit. Und die richtige Antwort: Man hat Kunden auf zwei Versionen, und beide verdienen den Fix.
Was das konkret für die Pipeline bedeutet
Das Branching-Modell ist ein Diagramm. In der Pipeline entscheidet sich, ob es funktioniert oder eben lautlos nicht, und genau das unterschätzen Teams durchgängig. Fünf Dinge ändern sich, sobald eine zweite Release-Linie existiert.
Der Trigger muss jede Release-Linie erfassen. Eine Pipeline, die nur auf main hört, ist in jedem CI-System die Standardeinstellung, und sie läuft nach dem Abspalten von release/1.x einwandfrei weiter. Deshalb fällt der Fehler kaum auf: Es gibt keine Fehlermeldung, Pushes erzeugen einfach keinen Build. Der Trigger braucht ein Glob-Muster.
# GitHub Actions
on:
push:
branches:
- main
- 'release/**'
In Jenkins ist das Äquivalent eine Multibranch-Pipeline mit release/** im Branch-Discovery-Filter, in Azure Pipelines ein Eintrag unter trigger.branches.include. Unabhängig vom System gilt: mit einem leeren Commit auf einen Release-Branch prüfen, ob wirklich ein Build erscheint. Wer darauf vertraut, dass der Trigger greift, erfährt es beim ersten Hotfix.
Versionsnummern müssen abgeleitet werden, nicht hinterlegt. Eine Version in einer Variable, die jemand von Hand pflegt, ist zu jedem Zeitpunkt auf mindestens einem Branch falsch. Besser aus dem Branch und dessen eigener Tag-Historie ableiten.
# Auf release/1.x: den neuesten Tag DIESER Linie und den Abstand
# dazu von git erfragen, z. B. v1.2.0-14-gb3f21ac -> 1.2.0.14
git describe --tags --match "v1.*" --long
Die verlockende Abkürzung ist, die Build-Nummer des CI-Systems als letzte Stelle zu nehmen. Besser nicht: Dieser Zähler ist global, beide Release-Linien bedienen sich daraus, und die v1-Artefakte heißen dann 1.0.0.41, 1.0.0.58, 1.0.0.77, mit unerklärlichen Sprüngen. Schlimmer noch: Bei einer Migration oder einem Reset des CI-Servers kann eine neue Build-Nummer niedriger ausfallen als eine bereits vergebene, und dann hält der Paketmanager einen frischen Build für älter als das, was schon veröffentlicht ist. Der Commit-Abstand pro Branch ist innerhalb seiner Linie monoton und übersteht einen Umzug des CI-Systems.
Die Version muss vor dem Kompilieren ins Projekt geschrieben werden. Dieser Punkt ist TwinCAT-spezifisch und wird gerne übersehen. Eine Library trägt ihre Version im Element <ProjectVersion> ihrer .plcproj, nicht im Dateinamen des Artefakts. Den Git-Commit zu taggen reicht also nicht. Wird dieses Element vor dem Build-Schritt nicht gesetzt, entsteht eine .library, die weiterhin TwinCATs Standardwert 0.0.0.1 meldet, während der Tag 1.2.0 sagt, und jedes Projekt, das dagegen auflöst, sieht die falsche Nummer. Die Version abzuleiten ist nur die halbe Arbeit. Die andere Hälfte ist, sie ins Projekt zu schreiben, weshalb unser Build-CLI diesen Schritt selbst übernimmt und ihn nicht dem Autor der Pipeline überlässt.
Jede Linie braucht ihr eigenes “Latest”. Builds aus release/1.x und release/2.x müssen im Storage nebeneinander existieren und unabhängig adressierbar sein. Jede Linie hat ihr eigenes Latest: die neueste Version, die innerhalb dieser Linie API-, ABI- und hardwarekompatibel ist. Eine Registry, die nur ein globales Latest über alle Versionen führt, verliert diese Unterscheidung, und zwar unbemerkt, bis ein v1-Projekt ein v2-Artefakt bekommt.
Die Libraries verzweigen mit. Hier multipliziert sich das Ganze. Sobald das Maschinenprojekt v1.x- und v2.x-Linien hat, gilt für jede gemeinsame Library dasselbe: library/1.x für die Maschinen im Feld, library/2.x für die Neuentwicklung. Die Release-Linien müssen in Abhängigkeitsreihenfolge angelegt werden, von unten nach oben, und jedes Projekt, das eine Library konsumiert, muss angeben, auf welcher Release-Linie es liegt, statt einfach den neuesten Build anzufordern. Ein Dependency-Resolver, der verzweigte Release-Historien nicht kennt, kann hier keine korrekte Antwort liefern, egal wie sorgfältig die Branches benannt sind.
Dazu kommt eine Ressourcenfrage, die man einplanen sollte. Zwei aktive Release-Linien bedeuten zwei unabhängige Build-Ströme, die um denselben Windows-Build-Agent und dieselbe Testhardware konkurrieren, und damit hängt ein v1-Hotfix in der Warteschlange hinter einem v2-Feature-Build. Ab diesem Punkt reicht ein einzelner Build-Agent nicht mehr.
Kompliziert ist davon nichts, sobald das Branching-Modell steht. Der typische Fehler ist, CI nur für main zu bauen, v1.0 auszuliefern, release/1.x anzulegen und die Lücken anschließend eine nach der anderen unter Druck zu entdecken.
Release Flow nachträglich einführen
Kaum jemand fängt bei null an. Die realistische Situation ist: ein einzelner main, v1.0 läuft bei zwei Kunden, und mehrere Monate v2-Arbeit liegen schon darüber. Release Flow lässt sich nachträglich einführen, und mit Git ist das weniger Aufwand, als es klingt, aber die Reihenfolge der Schritte ist entscheidend.
1. Den Commit finden, der tatsächlich ausgeliefert wurde. Wer das Release getaggt hat, hat ihn schon. Wer nicht, betreibt jetzt Archäologie: das Artefakt auf der Maschine über Build-Datum, die im Projekt hinterlegte Version oder das Changelog mit der Historie abgleichen. Diesen Commit jetzt taggen, auch wenn der Tag eine begründete Schätzung ist, die man in der Commit-Message als ungefähr kennzeichnet. Alles Weitere hängt daran, einen benannten Punkt zum Abspalten zu haben.
2. Den Release-Branch von diesem Commit abspalten, nicht von main.
git branch release/1.x v1.0.0
git push -u origin release/1.x
Das ist die gesamte strukturelle Änderung. Der Branch enthält die v2-Arbeit bewusst nicht, und genau deshalb gibt er ehrlich wieder, was beim Kunden läuft.
3. Prüfen, ob er überhaupt noch baut, bevor man sich darauf verlässt. Ein Branch, der von einem acht Monate alten Commit abgespalten wurde, fällt im CI häufig beim ersten Versuch durch, und die Gründe sind lehrreich. Meist steht eine Library-Referenz auf * und löst inzwischen auf etwas Neueres auf, als auf der Maschine installiert ist, oder die Pipeline-Definition selbst hat sich seit diesem Commit weiterentwickelt. Auch das Build-Tooling auf den Nodes ist meist nicht mehr dasselbe, was ein gutes Argument dafür ist, diese CLIs auf einer festen Version zu installieren statt auf dem, was gerade das Neueste ist. Die Library-Referenzen jetzt auf explizite Ranges pinnen, solange man ohnehin hinsieht, denn das ist der letzte ruhige Moment dafür.
4. Den Trigger erweitern und dann nachweisen, dass er greift. Einen leeren Commit auf release/1.x pushen und prüfen, dass ein Build erscheint und das Artefakt als 1.x versioniert ist, nicht als 2.x. Jetzt kostet das zehn Minuten. Während ein Kunde auf einen Fix wartet, deutlich mehr.
5. Festhalten, wo Fixes entstehen. Für Maschinensoftware lautet die Antwort meist “auf dem ältesten betroffenen Release-Branch, dann per Cherry-pick nach vorne”, also genau umgekehrt zu Microsofts Variante. Beide Konventionen funktionieren, aber ein Team, das sich auf keine geeinigt hat, produziert Fixes, die in v1.x existieren und in v2.x still wieder verschwinden.
6. Die Historie von main in Ruhe lassen. Die Versuchung ist groß, sie so umzuschreiben, dass es aussieht, als hätte man das schon immer so gemacht. Besser nicht. Alle Clones brechen, die bestehenden Tags passen nicht mehr, und es bringt nichts: Der Branch aus Schritt 2 liefert schon alles, was man ab jetzt braucht.
Der teure Schritt ist fast nie Schritt 2, sondern Schritt 3. Ein Release-Branch, der nicht baut, ist kein Release-Branch, und herauszufinden, welche Library-Referenzen seit einem Jahr frei laufen, wirft schnell eine ganze Sprintplanung um. Das ist auch der ehrliche Grund, warum die Nachrüstung liegen bleibt: Vorher weiß niemand, wie viel Arbeit daraus wird. Wer diesen Sprint nicht damit verbringen möchte, herauszufinden, welcher der sechs Schritte im eigenen Repository der harte ist: Wir machen das gemeinsam mit Teams.
Semantic Versioning und Paketmanager-Kompatibilität
TwinCAT kennt genau zwei Möglichkeiten, eine Library-Version zu referenzieren: eine exakte vierstellige Version oder * für das, was zur Build-Zeit das Neueste ist. Dazwischen gibt es nichts. Es gibt keine Schreibweise für “das neueste v1, aber niemals v2”. Mit einer einzigen Release-Linie lässt sich damit leben, weil exakt-oder-neuestes die Fälle abdeckt, die real vorkommen. Sobald release/1.x und release/2.x gleichzeitig existieren, ist genau das, was man ausdrücken müsste, das Einzige, was die Referenz nicht ausdrücken kann.
Hier verdient sich ein Paketmanager seinen Platz, und die Dimension, die er ergänzen muss, ist kein Range-Operator, sondern die Release-Linie selbst. Twinpack macht den Branch deshalb zum Teil jeder Referenz. In der .Zeugwerk/config.json trägt jedes Paket einen Namen, eine Version und einen Branch:
{
"name": "MyConveyorLib",
"version": null,
"branch": "release/1.x"
}
Eine version von null bedeutet weiterhin “neueste”, aber jetzt die neueste auf diesem Branch, weil der Branch die Kandidaten einschränkt, bevor die höchste Version unter ihnen ausgewählt wird. Ein Maschinenprojekt, das auf release/1.x festgelegt ist, bekommt weiterhin die v1-Wartungsbuilds und niemals 2.0.0, und niemand muss nach jedem Patch eine exakte Version von Hand nachziehen.
Der Standard-Branch ist main, und das ist das Detail, das man verinnerlichen sollte: Eine Referenz ohne Branch-Angabe folgt der aktiven Entwicklungslinie. Für eine Library, gegen die gerade entwickelt wird, ist das der richtige Standard, für eine Maschine, die vor achtzehn Monaten ausgeliefert wurde, der falsche.
Das ist auch der Grund, warum allgemeines Tooling hier oft scheitert. Viele git-native Resolver bestimmen die neueste Version einer Library, indem sie Tags auf dem Default-Branch durchsuchen. Taggt man 1.2.3 auf release/1.x und 2.0.0 auf release/2.x, sieht ein Resolver, der nur main inspiziert, 1.2.3 entweder gar nicht, oder er hält 2.0.0 für dessen Ablösung.
Der *-Versions-Platzhalter in TwinCAT
TwinCAT erlaubt es, in Library-Referenzen * als Version anzugeben, also “zur Build-Zeit die jeweils neueste Version auflösen.” In einer Welt mit nur einem Branch ist das vorhersehbar: Latest ist Latest. Sobald release/1.x und release/2.x im Spiel sind, hat * keine sinnvolle Antwort. Je nachdem, wie der Resolver das Repository durchsucht, liefert er möglicherweise 2.0.0 an ein Projekt, das gegen die v1.x-API geschrieben wurde und auf einer Maschine läuft, die hardwareseitig nicht auf v2.x upgraden kann. Der Build läuft durch; der Interface-Mismatch zeigt sich auf der Maschine. Beim Kunden vor Ort. An einem Freitag.
* zu verwenden bedeutet, das Versions-Management abzuschalten: das “Werden wir schon irgendwann lösen” der Dependency-Resolution, und “irgendwann” ist meistens eine Maschine in der Produktion. Es funktioniert, bis eine zweite Major-Version eingeführt wird, ab dann ist es eine Lotterie zur Laufzeit. Die Lösung ist keine geschicktere Versionsangabe, denn die bietet TwinCAT nicht. Die Lösung ist, die Entscheidung eine Ebene höher zu ziehen: die Release-Linie benennen und “neueste” innerhalb dieser Linie gelten lassen. Genau für dieses Problem haben wir Twinpack gebaut, unseren Open-Source-Paketmanager für TwinCAT.
Die richtige Strategie wählen
Jede Zeile in der Tabelle ist eine Anforderung, die häufig darüber entscheidet, welche Strategien realistisch bleiben. Die eigene Situation in den Zeilen suchen und schauen, welche Spalten dann noch übrig bleiben.
| Trunk-based | GitHub Flow | Git Flow | GitLab Flow (env) | GitLab Flow (release) | Release Flow | |
|---|---|---|---|---|---|---|
| Hotfix auf alte ausgelieferte Version | ◑ | ◑ | ✓ | ◑ | ✓ | ✓ |
| Parallele Versionen im Feld | ✗ | ✗ | ◑ | ✗ | ✓ | ✓ |
| Zustandsbehaftetes Artefakt (DB / Config) | ✗ | ✗ | ◑ | ✓ | ◑ | ✗ |
| Staging-Gate vor Produktion | ◑ | ✗ | ✓ | ✓ | ✓ | ✓ |
| CI/CD-Komplexität | Niedrig | Niedrig | Hoch | Mittel | Mittel | Mittel |
| Wann wird ausgeliefert | Jeder Commit | Jeder Commit | Pro Release | Kontrolliert | Pro Release | Pro Release |
| Eignung: Web / SaaS | ✓ | ✓ | ◑ | ✓ | ◑ | ◑ |
| Eignung: Maschinen / SPS-Software | ✗ | ✗ | ◑ | ◑ | ✓ | ✓ |
✓ definierter Bestandteil der Strategie · ◑ möglich, aber man erweitert die Strategie selbst · ✗ von der Strategie nicht abgedeckt
Eine Zeile verdient eher eine Erläuterung als ein Symbol. Die Zeile zum zustandsbehafteten Artefakt ist die einzige, in der Environment-Branching klar gewinnt, und gleichzeitig die, die auf Maschinensoftware am seltensten zutrifft: Wer Code plus Library-Abhängigkeiten ausliefert, ohne Datenbankschema, das sich parallel mitentwickelt, kann sie komplett ignorieren. Wer dagegen eine Konfigurationsdatenbank ausliefert, die zwischen Versionen migrieren muss, findet hier unter Umständen genau die Zeile, die alles entscheidet, und sollte sie gegen den Rest der Tabelle abwägen, bevor die Entscheidung auf Release-Branches fällt.
Wer Maschinen im Feld hat, die unabhängige Versionspflege brauchen, erzeugt mit Release Flow oder dem Release-Branch-Modus von GitLab Flow langfristig die wenigste Reibung. Bleiben die Fragen, welcher der beiden zum Arbeitsstil des Teams passt und ob das eigene Tooling Versionen tatsächlich über verzweigte Release-Linien auflösen kann, statt eine einzige Historie anzunehmen.
Fragen zur Branching-Strategie für TwinCAT, oder Interesse an einer zweiten Meinung dazu, wie Release-Linien und Pipeline bei euch zusammenpassen? Einfach melden. Wir bieten außerdem Training für Teams, die das lieber direkt am eigenen Repository durcharbeiten.
