GitLab CI Matrix-Jobs: Testmatrizen fuer mehrere PHP- und Node-Versionen
AI generated
CI/CD
.yml
GitLab · CI/CD · Testmatrix
Matrix-Jobs in GitLab CI
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.

17 Min. Lesezeit parallel:matrix PHP-Versionen Node.js Testabdeckung

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.

11. FAQ: Matrix-Jobs in GitLab CI: Das Wichtigste auf einen Blick

1Was macht parallel: matrix: in GitLab CI?
Das Schluesselwort erzeugt aus einer einzigen Job-Definition automatisch mehrere parallele Job-Instanzen, eine fuer jede angegebene Werte-Kombination, zum Beispiel eine Instanz je PHP-Version.
2Wie viele Jobs erzeugt eine Matrix mit mehreren Arrays?
GitLab bildet das kartesische Produkt aller Werte. Drei PHP-Versionen kombiniert mit zwei Datenbank-Versionen ergeben sechs Job-Instanzen, eine pro moeglicher Kombination.
3Wie greife ich im script auf die Matrix-Variable zu?
Die in matrix: definierten Variablen wie PHP_VERSION stehen als gewoehnliche CI-Variablen zur Verfuegung und koennen in image:, before_script:, script: und cache: verwendet werden.
4Warum schlaegt mein Cache zwischen Matrix-Jobs fehl?
Ein identischer Cache-Key fuer alle Matrix-Zellen fuehrt dazu, dass Jobs mit unterschiedlichen Versionen sich denselben Cache teilen und gegenseitig ueberschreiben. Der Cache-Key muss die Matrix-Variable enthalten.
5Kann ich die Matrix nur fuer bestimmte Branches aktivieren?
Ja, ueber rules: mit Bedingungen wie CI_PIPELINE_SOURCE oder CI_COMMIT_BRANCH lassen sich unterschiedliche Matrix-Groessen fuer Merge Requests, main-Branch oder Scheduled Pipelines definieren.
6Wie unterscheiden sich die Job-Namen einer Matrix im Pipeline-Graph?
GitLab haengt die jeweilige Wertekombination in eckigen Klammern an den Job-Namen an, zum Beispiel test-php: [8.1], sodass jede Instanz eindeutig zuordenbar bleibt.
7Ab wie vielen Kombinationen wird eine Matrix unpraktikabel?
Eine feste Grenze gibt es nicht, aber ab etwa zehn bis zwoelf parallelen Jobs fuer einen einzigen Test-Schritt lohnt sich meist eine Ueberpruefung, ob wirklich jede Kombination noetig ist oder eine reduzierte Liste ausreicht.
8Kann ich einzelne Kombinationen aus dem kartesischen Produkt ausschliessen?
Direktes Ausschliessen einzelner Kombinationen unterstuetzt matrix: nicht. Stattdessen listet man die gewuenschten Kombinationen explizit als mehrere Eintraege innerhalb des matrix:-Arrays auf.
9Lohnt sich eine Matrix fuer interne Anwendungen?
Meist nicht, wenn nur eine feste Produktionsversion existiert. Sinnvoll wird sie erst bei geplanten Versions-Upgrades oder wenn die Software von Dritten in unterschiedlichen Umgebungen eingesetzt wird.
10Wie kombiniere ich Matrix mit needs: fuer schnellere Pipelines?
needs: kann auf einzelne Matrix-Job-Instanzen oder auf die gesamte Matrix verweisen, wodurch nachfolgende Stages nicht auf alle anderen Stage-Jobs warten muessen, sondern nur auf die tatsaechlich benoetigten Matrix-Ergebnisse.