Eine Pipeline, viele PHP- und Node-Versionen
Wer eine Bibliothek oder ein Tool fuer mehrere PHP- oder Node-Versionen pflegt, kommt an Testmatrizen kaum vorbei. Dieser Artikel zeigt, wie parallel: matrix: in GitLab CI aus einer Job-Definition automatisch mehrere parallele Testlaeufe erzeugt und wie sich Laufzeitkosten und Testabdeckung sinnvoll ausbalancieren lassen.
Inhaltsverzeichnis
- 1. Warum Testmatrizen in CI/CD ueberhaupt noetig sind
- 2. Grundsyntax: parallel: matrix: in der .gitlab-ci.yml
- 3. Node.js-Matrix fuer Frontend- oder Tooling-Projekte
- 4. Kombinierte Achsen: PHP-Version x Datenbank-Version
- 5. Laufzeitkosten gegen Testabdeckung abwaegen
- 6. Selektive Matrix mit rules: und branchabhaengiger Reduktion
- 7. Caching pro Matrix-Zelle richtig konfigurieren
- 8. Wann eine feste Version sinnvoller ist als eine Matrix
- 9. Fazit: Matrix-Strategie richtig dimensionieren
- 10. Zusammenfassung
- 11. FAQ
1. Warum Testmatrizen in CI/CD ueberhaupt noetig sind
Sobald ein PHP-Paket oder eine Node.js-Bibliothek von mehreren Nutzergruppen mit unterschiedlichen Laufzeitversionen eingesetzt wird, reicht ein einzelner CI-Job mit fest verdrahteter Version nicht mehr aus. Eine composer.json mit "php": "^8.1 || ^8.2 || ^8.3" verspricht Kompatibilitaet zu drei Versionen, getestet wird davon in der Praxis aber oft nur eine einzige, meist die auf dem Entwicklungsrechner installierte. Genau hier entsteht das Risiko: Sprachfeatures, die in PHP 8.3 verfuegbar sind, in 8.1 aber noch nicht existieren, oder Verhaltensaenderungen interner Funktionen zwischen Node 18 und Node 20 fallen erst auf, wenn ein Nutzer mit der abweichenden Version eine Exception meldet.
GitLab CI loest dieses Problem mit dem Schluesselwort parallel: matrix:, das aus einer einzigen Job-Definition automatisch mehrere parallele Job-Instanzen erzeugt, je eine pro Wertekombination. Statt fuer jede unterstuetzte PHP- oder Node-Version einen eigenen Job von Hand zu pflegen und bei einer neuen Version zu vergessen, wird die Liste der Versionen zentral gepflegt und die Pipeline generiert die passenden Jobs selbst. Das reduziert Redundanz in der .gitlab-ci.yml erheblich und macht die tatsaechlich getestete Versionsmatrix im Pipeline-Graph sichtbar.
2. Grundsyntax: parallel: matrix: in der .gitlab-ci.yml
Der Block parallel: matrix: unter einem Job erzeugt fuer jeden Eintrag im Array PHP_VERSION eine eigene Job-Instanz. GitLab benennt diese Instanzen automatisch durchlaufend, etwa test-php: [8.1], test-php: [8.2] und test-php: [8.3], sodass im Pipeline-Graph auf einen Blick erkennbar ist, welche Version fehlgeschlagen ist. Die Variable PHP_VERSION steht in jedem geklonten Job zur Verfuegung und kann ueberall dort verwendet werden, wo eine gewoehnliche CI-Variable erlaubt ist, inklusive image:, before_script: und script:.
Besonders praktisch ist die Verwendung der Matrix-Variable direkt im image:-Feld, wie im Beispiel mit php:${PHP_VERSION}-cli. Dadurch entfaellt die manuelle Pflege dreier fast identischer Jobs, die sich nur im Docker-Image unterscheiden. Wichtig ist, dass fuer jede unterstuetzte Minor-Version tatsaechlich ein offizielles Image existiert; bei selbst gebauten Images muss die Tag-Konvention entsprechend konsistent gehalten werden, damit ${PHP_VERSION} zuverlaessig auf ein existierendes Image aufloest.
test-php:
stage: test
image: php:${PHP_VERSION}-cli
parallel:
matrix:
- PHP_VERSION: ["8.1", "8.2", "8.3"]
before_script:
- php -v
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install --no-interaction --prefer-dist
script:
- vendor/bin/phpunit --colors=never
3. Node.js-Matrix fuer Frontend- oder Tooling-Projekte
Fuer Frontend-Projekte, CLI-Tools oder Node-basierte Build-Skripte funktioniert dasselbe Prinzip mit den offiziellen node-Images. Sinnvoll ist, sich an den offiziellen Node-LTS-Zyklen zu orientieren: die aktuell aktive LTS-Version, die vorherige LTS-Version im Wartungsmodus und optional die neueste Current-Version als Fruehwarnung vor kommenden Breaking Changes. Damit deckt eine Drei-Werte-Matrix meist den relevanten Nutzerkreis ab, ohne beliebig viele historische Versionen mitzuschleppen.
Der Cache-Block im Beispiel zeigt einen wichtigen Zusatzpunkt: Ohne versionsabhaengigen Cache-Key wuerden alle drei Node-Versionen denselben npm-Cache teilen, was zu Konflikten zwischen inkompatiblen Binaerpaketen fuehren kann, etwa bei nativen Node-Modulen, die pro Node-ABI neu kompiliert werden. Der Schluessel node-$NODE_VERSION sorgt dafuer, dass GitLab fuer jede Version einen eigenen, isolierten Cache anlegt und wiederverwendet.
test-node:
stage: test
image: node:${NODE_VERSION}
parallel:
matrix:
- NODE_VERSION: ["18", "20", "22"]
cache:
key: "node-$NODE_VERSION"
paths:
- .npm/
script:
- npm ci --cache .npm --prefer-offline
- npm run test -- --ci
- npm run build
4. Kombinierte Achsen: PHP-Version x Datenbank-Version
Werden innerhalb eines Matrix-Eintrags mehrere Arrays angegeben, bildet GitLab das kartesische Produkt aus allen Werten. Im Beispiel entstehen aus drei PHP-Versionen und zwei MySQL-Versionen sechs Job-Instanzen, jede mit einer eindeutigen Kombination aus PHP_VERSION und DB_IMAGE. Das ist die richtige Wahl, wenn wirklich jede Kombination getestet werden muss, etwa weil eine Bibliothek explizit Kompatibilitaet zu PHP 8.1 mit MySQL 5.7 und zu PHP 8.3 mit MySQL 8.0 verspricht.
Kartesische Produkte wachsen jedoch schnell: Drei PHP-Versionen mal zwei Datenbank-Versionen mal zwei Betriebssystem-Images ergeben bereits zwoelf Jobs fuer einen einzigen Test-Schritt. Wird eine weitere Achse wie eine Redis-Version ergaenzt, verdoppelt oder verdreifacht sich die Zahl erneut. Deshalb lohnt es sich, vor dem Hinzufuegen einer weiteren Matrix-Achse zu pruefen, ob wirklich das volle Produkt benoetigt wird oder eine reduzierte, handverlesene Liste von Kombinationen ausreicht.
test-compatibility:
stage: test
image: php:${PHP_VERSION}-cli
parallel:
matrix:
- PHP_VERSION: ["8.1", "8.2", "8.3"]
DB_IMAGE: ["mysql:5.7", "mysql:8.0"]
services:
- name: $DB_IMAGE
alias: database
script:
- php composer.phar install --no-interaction
- vendor/bin/phpunit --group=database
5. Laufzeitkosten gegen Testabdeckung abwaegen
Jede zusaetzliche Zeile in einer Matrix multipliziert sich in echte Runner-Minuten. Ein PHPUnit-Lauf mit Coverage-Erfassung, der isoliert vier Minuten dauert, kostet bei einer 3x2-Matrix zwoelf Job-Minuten pro Pipeline-Durchlauf, nicht vier. Bei shared GitLab.com-Runnern mit begrenztem CI/CD-Minutenkontingent oder bei selbstgehosteten Runnern mit begrenzter paralleler Kapazitaet wirkt sich das direkt auf Wartezeiten bis zum Merge und auf die monatliche Rechnung aus.
Eine bewaehrte Strategie ist, die volle Matrix nicht bei jedem Commit laufen zu lassen, sondern gestaffelt: Auf Feature-Branches und in Merge Requests genuegt haeufig ein reduzierter Satz, etwa nur die niedrigste unterstuetzte und die aktuellste Version, um grobe Inkompatibilitaeten frueh zu erkennen. Die vollstaendige Matrix mit allen Kombinationen laeuft dann nur noch auf dem main-Branch oder ueber eine naechtliche Scheduled-Pipeline, wo die zusaetzliche Laufzeit niemanden beim Warten auf ein Merge-Request-Ergebnis blockiert.
6. Selektive Matrix mit rules: und branchabhaengiger Reduktion
Mit rules: laesst sich steuern, welche Matrix-Variante in welchem Kontext laeuft. Im Beispiel definiert test-php eine schlanke Zwei-Werte-Matrix fuer Merge Requests, waehrend test-php-full-matrix per extends: die gemeinsame Konfiguration uebernimmt, die Matrix aber auf alle drei Versionen erweitert und ausschliesslich bei geplanten Pipelines aktiviert wird, erkennbar an CI_PIPELINE_SOURCE == "schedule".
Dieses Muster haelt die .gitlab-ci.yml wartbar, weil die eigentliche Testlogik nur einmal in test-php steht und per extends: wiederverwendet wird. Aendert sich das PHPUnit-Kommando, muss es nur an einer Stelle angepasst werden. Gleichzeitig bleibt die Moeglichkeit erhalten, jederzeit manuell ueber die GitLab-Oberflaeche eine geplante Pipeline mit der vollen Matrix anzustossen, etwa vor einem groesseren Release.
test-php:
stage: test
image: php:${PHP_VERSION}-cli
script:
- vendor/bin/phpunit --colors=never
parallel:
matrix:
- PHP_VERSION: ["8.1", "8.3"]
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
when: on_success
test-php-full-matrix:
extends: test-php
parallel:
matrix:
- PHP_VERSION: ["8.1", "8.2", "8.3"]
rules:
- if: $CI_PIPELINE_SOURCE == "schedule"
when: on_success
7. Caching pro Matrix-Zelle richtig konfigurieren
Ein haeufiger Fehler bei Matrix-Jobs ist ein Cache-Key, der ueber alle Matrix-Zellen identisch bleibt, etwa ein pauschales cache: key: composer. Da composer install je nach PHP-Version unterschiedliche Paketversionen aufloesen kann, weil manche Pakete versionsabhaengige Constraints haben, landen dann inkompatible vendor/-Verzeichnisse im selben Cache-Slot und ueberschreiben sich gegenseitig zwischen den parallel laufenden Jobs. Das Ergebnis sind sporadische, schwer reproduzierbare Testfehler.
Die Loesung ist, die Matrix-Variable Teil des Cache-Keys zu machen, wie im Beispiel mit composer-$PHP_VERSION. GitLab legt dann fuer jede PHP-Version einen eigenen, isolierten Cache an. Der zusaetzliche Speicherbedarf ist gering im Vergleich zum Zeitgewinn, den ein funktionierender Cache gegenueber einem kompletten composer install ohne Cache bringt, insbesondere bei Projekten mit vielen Abhaengigkeiten.
test-php:
stage: test
image: php:${PHP_VERSION}-cli
parallel:
matrix:
- PHP_VERSION: ["8.1", "8.2", "8.3"]
cache:
key: "composer-$PHP_VERSION"
paths:
- vendor/
- .composer-cache/
script:
- composer install --no-interaction --prefer-dist
- vendor/bin/phpunit
8. Wann eine feste Version sinnvoller ist als eine Matrix
Nicht jedes Projekt profitiert von einer Matrix. Eine interne Anwendung, die auf genau einem produktiven Server mit einer einzigen, vom Team kontrollierten PHP-Version laeuft, hat keinen praktischen Nutzen davon, zusaetzlich gegen zwei weitere Versionen zu testen, die nie zum Einsatz kommen. Hier erzeugt eine Matrix nur zusaetzliche Pipeline-Laufzeit und Komplexitaet in der .gitlab-ci.yml, ohne dass jemals ein fuer Produktion relevanter Bug durch eine der zusaetzlichen Versionen entdeckt wuerde.
Ein sinnvoller Mittelweg fuer Anwendungen mit geplantem Versions-Upgrade ist eine Zwei-Werte-Matrix aus aktuell produktiver und naechster geplanter Version, etwa PHP 8.2 als aktuell live und PHP 8.3 als geplantes Ziel-Upgrade in drei Monaten. Das gibt fruehzeitiges Feedback zur Upgrade-Kompatibilitaet, ohne den Aufwand einer vollstaendigen, fuer die Praxis irrelevanten Versionsmatrix zu betreiben. Bibliotheken und Pakete, die von Dritten in unterschiedlichsten Umgebungen eingesetzt werden, sind dagegen der klassische Fall fuer eine breite Matrix.
9. Fazit: Matrix-Strategie richtig dimensionieren
Matrix-Jobs sind kein Automatismus, den jede Pipeline braucht, sondern ein gezieltes Werkzeug fuer Projekte, deren Zielumgebung tatsaechlich variiert. Die Entscheidung sollte immer mit der Frage beginnen, wer die Software mit welchen Versionen einsetzt, nicht mit der Frage, welche Versionen technisch theoretisch unterstuetzt werden koennten.
In der Praxis bewaehrt sich eine gestaffelte Strategie: schlanke Matrix fuer schnelles Feedback in Merge Requests, volle Matrix fuer Release-Kandidaten und naechtliche Scheduled-Pipelines, dazu ein bewusster Cache- und rules:-Aufbau, damit die zusaetzliche Testabdeckung nicht durch unnoetig lange Wartezeiten oder falsch geteilte Caches erkauft wird.
| Szenario | Empfohlene Strategie | Trigger | Job-Anzahl (Beispiel) |
|---|---|---|---|
| Bibliothek fuer Drittanbieter | Volle Matrix aller unterstuetzten Versionen | Jeder Push / MR | 6-12 |
| Interne Anwendung, feste Version | Keine Matrix, ein fester Job | Jeder Push | 1 |
| Anwendung vor geplantem Upgrade | Zwei-Werte-Matrix (aktuell + Ziel) | Jeder Push | 2 |
| Release-Kandidat / Nightly | Volle Matrix inkl. Datenbank-Achse | Scheduled Pipeline | 6-12 |
Mironsoft
CI/CD-Pipelines, Zero-Downtime-Deployments und Release-Automatisierung
Deployments, die ohne Ausfallzeit und ohne Nervenkitzel laufen?
Wir prüfen bestehende GitLab-Pipelines auf fragile Deployment-Schritte und fehlende Absicherung und bauen daraus einen Release-Prozess mit Zero-Downtime-Deployments, automatisierten Checks und einem Rollback, dem ihr im Ernstfall vertrauen könnt.
Pipeline-Review
Bestehende .gitlab-ci.yml auf Fragilität, fehlende Stages und Sicherheitslücken prüfen.
Zero-Downtime-Deployment
Symlink-Releases, Health-Checks und Rollback-Strategien für Magento-Shops aufbauen.
CI/CD-Automatisierung
Tests, Security-Scans und Deployments zu einer zuverlässigen Pipeline verbinden.
10. Zusammenfassung
Matrix-Jobs in GitLab CI: Das Wichtigste auf einen Blick
Grundsyntax
parallel: matrix: erzeugt pro Wertekombination automatisch eine eigene Job-Instanz.
Kartesisches Produkt
Mehrere Arrays in einem Matrix-Eintrag werden vollstaendig miteinander kombiniert.
Caching
Der Cache-Key muss die Matrix-Variable enthalten, sonst ueberschreiben sich Versionen gegenseitig.
Kostenkontrolle
Reduzierte Matrix in Merge Requests, volle Matrix nur auf main oder als Scheduled Pipeline.