Matrix-Builds mit Bash-Schleifen lokal simulieren, bevor sie in die CI wandern
AI generated
$_
#!/
Bash · CI/CD · Testautomatisierung · Docker
Matrix-Builds mit Bash-Schleifen simulieren
Versions- und Umgebungs-Kombinationen lokal durchtesten, bevor die CI-Matrix geschrieben wird

Bevor eine Matrix-Konfiguration in GitHub Actions oder GitLab CI live geht, lohnt sich ein Prototyp in Bash: dieselbe Kombinatorik aus Versionen, Betriebssystemen und Umgebungen lokal per Schleife durchlaufen, Fehler direkt in der Shell debuggen und erst danach die endgültige YAML-Matrix schreiben. Das spart teure Trial-and-Error-Commits und macht sichtbar, welche Kombinationen überhaupt sinnvoll sind.

16 Min. Lesezeit for-Schleifen · xargs -P · Docker GitHub Actions · GitLab CI

1. Warum eine Matrix-Build-Idee zuerst lokal in Bash simulieren

Ein Matrix-Build testet dieselbe Codebasis gegen mehrere Kombinationen aus Sprachversion, Betriebssystem und Abhängigkeits-Set, zum Beispiel PHP 8.2, 8.3 und 8.4 jeweils gegen zwei Datenbank-Versionen. In der CI-Konfiguration entsteht daraus schnell ein Dutzend paralleler Jobs, aber jede Änderung an dieser Matrix kostet einen Commit, einen Push und mehrere Minuten Wartezeit, bis alle Runner durchgelaufen sind, nur um festzustellen, dass eine Kombination einen Tippfehler im Image-Tag enthält.

Eine lokale Bash-Schleife, die dieselbe Kombinatorik durchläuft, liefert dieselbe Information in Sekunden statt Minuten, weil sie ohne Warteschlange, ohne Runner-Zuteilung und ohne Netzwerk-Overhead direkt auf der eigenen Maschine läuft. Wer die Matrix zuerst als Bash-Prototyp baut, findet falsche Versionsnummern, fehlende Umgebungsvariablen und unerwartete Abhängigkeitskonflikte, bevor sie in der echten CI-Pipeline auftauchen und dort deutlich teurer zu debuggen sind.

2. Grundmuster: eine Bash-Schleife über Versionen und Umgebungen

Der Kern einer lokalen Matrix-Simulation ist eine verschachtelte Schleife über zwei oder mehr Bash-Arrays, eines pro Matrix-Dimension. Jede Kombination aus den Elementen der Arrays entspricht genau einer Zelle der späteren CI-Matrix, und die Schleife durchläuft sie in derselben kartesischen Reihenfolge, wie es strategy.matrix in GitHub Actions später auch tun würde.

Wichtig ist, die Dimensionen von Anfang an so zu benennen, wie sie später in der YAML-Matrix heißen werden, etwa PHP_VERSIONS und DB_VERSIONS, damit die Übersetzung am Ende ein reines Copy-Paste der Werte ist und keine neue Namensfindung erfordert. Das reduziert die Fehlerquelle, dass die lokale Simulation etwas anderes testet als die spätere echte Matrix.


#!/usr/bin/env bash
set -euo pipefail

readonly PHP_VERSIONS=("8.2" "8.3" "8.4")
readonly DB_VERSIONS=("mysql:8.0" "mariadb:10.11")

for php in "${PHP_VERSIONS[@]}"; do
  for db in "${DB_VERSIONS[@]}"; do
    echo "=== Matrix-Zelle: PHP ${php} x ${db} ==="
    # Platzhalter fuer den eigentlichen Testlauf dieser Kombination
    echo "  -> wuerde hier PHP ${php} gegen ${db} testen"
  done
done

3. Docker als Isolationsschicht pro Matrix-Zelle nutzen

Eine reine Bash-Schleife allein reicht nicht aus, um verschiedene PHP-Versionen realistisch zu testen, weil auf der lokalen Maschine meist nur eine Version installiert ist. Docker schließt diese Lücke: Für jede Matrix-Zelle wird ein Container mit exakt der Image-Version gestartet, die auch die spätere CI-Matrix verwenden würde, sodass die Simulation nicht nur die Logik, sondern auch die tatsächliche Laufzeitumgebung abbildet.

Entscheidend ist, dieselben Image-Tags zu verwenden, die auch in der CI-Konfiguration stehen werden, zum Beispiel php:8.3-cli statt einer lokal abweichenden Version. Wird das Projektverzeichnis per Bind-Mount eingehängt, laufen die Tests im Container exakt gegen den aktuellen Arbeitsstand, ohne dass Artefakte zwischen den Matrix-Zellen sich gegenseitig beeinflussen können.


#!/usr/bin/env bash
set -euo pipefail

readonly PHP_VERSIONS=("8.2" "8.3" "8.4")

for php in "${PHP_VERSIONS[@]}"; do
  echo "=== PHP ${php} in Docker ==="
  docker run --rm \
    -v "$PWD":/app -w /app \
    "php:${php}-cli" \
    ./run-tests.sh
done

4. Matrix-Zellen parallel statt sequenziell ausführen

Eine sequenzielle Schleife über sechs oder mehr Matrix-Zellen summiert die Laufzeit jedes einzelnen Testlaufs, während eine echte CI-Matrix die Zellen parallel auf mehreren Runnern verteilt. Um diesen Vorteil lokal zumindest teilweise nachzubilden, lässt sich xargs -P nutzen, das die Kombinationen aus einer Liste liest und bis zu einer festgelegten Anzahl gleichzeitig als eigene Prozesse startet.

Die Anzahl paralleler Prozesse sollte sich an der Kernanzahl der lokalen Maschine orientieren, nicht an der Anzahl der CI-Runner, da hier ein einzelner Rechner alle Zellen gleichzeitig verarbeiten muss. Wer stattdessen das klassische &-Muster mit anschließendem wait bevorzugt, sammelt die Prozess-IDs in einem Array und wartet am Ende gesammelt auf alle, was die Kontrolle über einzelne Exit-Codes erleichtert.


#!/usr/bin/env bash
set -euo pipefail

readonly PHP_VERSIONS=("8.2" "8.3" "8.4")

run_cell() {
  local php="$1"
  docker run --rm -v "$PWD":/app -w /app "php:${php}-cli" ./run-tests.sh
}
export -f run_cell

printf '%s\n' "${PHP_VERSIONS[@]}" | xargs -P 3 -I{} bash -c 'run_cell "$@"' _ {}

5. Ergebnisse sammeln und einen Report erzeugen

Parallel gestartete Testläufe schreiben ihre Ausgabe wild durcheinander auf dasselbe Terminal, wenn nicht jede Matrix-Zelle in eine eigene Log-Datei umgeleitet wird. Ein assoziatives Array, das den Exit-Code jeder Zelle unter einem sprechenden Schlüssel wie php83-mariadb speichert, macht am Ende in einer Schleife sichtbar, welche Kombinationen fehlgeschlagen sind, ohne die verstreuten Log-Dateien einzeln öffnen zu müssen.

Ein einfacher Abschlussreport, der Pass- und Fail-Zeilen zusammenfasst, entspricht in reduzierter Form der Matrix-Übersicht, die GitHub Actions oder GitLab CI in der Weboberfläche zeigen. Für schnelles lokales Debugging reicht das völlig aus, weil der Fokus hier nicht auf einem hübschen Dashboard liegt, sondern darauf, in Sekunden zu erkennen, welche Version-Kombination als Nächstes Aufmerksamkeit braucht.


#!/usr/bin/env bash
set -euo pipefail

declare -A RESULTS=()

for php in 8.2 8.3 8.4; do
  log="/tmp/matrix-${php}.log"
  if docker run --rm -v "$PWD":/app -w /app "php:${php}-cli" ./run-tests.sh > "$log" 2>&1; then
    RESULTS["php${php}"]="PASS"
  else
    RESULTS["php${php}"]="FAIL (siehe $log)"
  fi
done

echo "=== Matrix-Report ==="
for key in "${!RESULTS[@]}"; do
  echo "  ${key}: ${RESULTS[$key]}"
done

6. Von der lokalen Simulation zur echten CI-Matrix-Konfiguration

Sobald die Bash-Simulation zuverlässig läuft, ist die Übersetzung in eine echte CI-Matrix meist reine Formsache: Die Werte aus PHP_VERSIONS und DB_VERSIONS wandern unverändert in strategy.matrix unter GitHub Actions, beziehungsweise in die parallel: matrix:-Sektion unter GitLab CI. Weil die Kombinationen lokal schon einmal alle durchgelaufen sind, entfällt die übliche Serie kleiner Korrektur-Commits, die nur dazu dienen, Tippfehler in Versionsnummern zu finden.

Wer zusätzlich in der Bash-Schleife einzelne Kombinationen explizit ausschließt, etwa weil PHP 8.2 mit einer bestimmten Datenbank-Version nicht kompatibel ist, kann diese Ausschlüsse direkt als exclude-Einträge in die YAML-Matrix übernehmen. Die lokale Simulation wird damit nicht nur zum Testlauf, sondern auch zur Dokumentation, welche Zellen der Matrix überhaupt sinnvoll sind.

7. Vor- und Nachteile gegenüber nativer CI-Matrix

Der größte Vorteil der lokalen Bash-Simulation ist die Feedback-Geschwindigkeit: kein Warten auf freie Runner, keine CI-Minuten-Kosten und die Möglichkeit, mit einem Debugger oder interaktiver Shell direkt in eine fehlgeschlagene Matrix-Zelle hineinzuspringen, was in einer laufenden CI-Pipeline nur mit Zusatzwerkzeugen wie SSH-Debug-Sessions möglich ist.

Der Nachteil ist, dass eine lokale Maschine niemals die Parallelität einer CI-Farm mit Dutzenden Runnern erreicht, und dass die hübsche Matrix-Übersicht mit grünen und roten Kacheln, Artefakt-Uploads pro Zelle und automatischen Pull-Request-Kommentaren komplett fehlt. Die Bash-Simulation ist ein Entwicklungswerkzeug für die Entstehungsphase einer Matrix, kein Ersatz für die produktive CI-Ausführung.

8. Caching und Ressourcen-Grenzen beim lokalen Testen

Jede zusätzliche Matrix-Dimension multipliziert die Anzahl benötigter Docker-Images, und ohne Aufräumen füllt sich die lokale Festplatte schnell mit Dutzenden Image-Varianten, die nach dem Prototyping nicht mehr gebraucht werden. Ein regelmäßiger docker image prune nach abgeschlossener Simulation hält den Plattenverbrauch im Rahmen, ohne die Images zu löschen, die noch aktiv verwendet werden.

Weil eine lokale Maschine typischerweise deutlich weniger CPU-Kerne und Arbeitsspeicher hat als eine ganze CI-Runner-Flotte, sollte die lokale Matrix nicht zwangsläufig jede einzelne Kombination der späteren echten Matrix abdecken. Eine repräsentative Teilmenge, etwa die älteste und die neueste unterstützte Version pro Dimension, deckt die meisten realen Fehlerquellen ab und hält die lokale Laufzeit überschaubar.

9. Wann sich der Umweg über Bash lohnt, und wann nicht

Der Umweg über eine Bash-Simulation lohnt sich vor allem beim Aufbau einer neuen Matrix, beim Debuggen einer Version-spezifischen Fehlermeldung, die nur in einer bestimmten Zelle auftritt, oder wenn CI-Minuten teuer sind und jeder unnötige Pipeline-Lauf vermieden werden soll. Auch beim Reproduzieren eines gemeldeten Bugs, der nur unter einer alten Sprachversion auftritt, ist die lokale Schleife oft schneller als ein CI-Retrigger.

Für eine bereits stabile, seit Monaten unveränderte Matrix lohnt sich der zusätzliche Pflegeaufwand einer parallelen Bash-Version dagegen nicht, weil hier kaum noch Iteration stattfindet und die native CI-Matrix ihre Stärken bei Parallelität und Reporting voll ausspielt. Die Bash-Simulation ist ein Werkzeug für die Entwicklungsphase, nicht für den Dauerbetrieb einer eingespielten Pipeline.

Kriterium Bash-Schleife lokal Native CI-Matrix Empfehlung
Feedback-Geschwindigkeit Sekunden, kein Warten auf Runner Minuten, abhängig von Runner-Warteschlange Bash für schnelle Iteration
Parallelität Begrenzt durch lokale CPU-Kerne Dutzende Runner gleichzeitig möglich CI für große Matrizen
Kosten Keine CI-Minuten verbraucht CI-Minuten pro Zelle Bash beim Prototyping
Debugging Direkter Shell-Zugriff auf fehlgeschlagene Zelle Nur über Logs oder SSH-Debug-Sessions Bash für Root-Cause-Analyse
Reporting Einfacher Text-Report Grafische Matrix-Übersicht, PR-Kommentare CI für produktiven Betrieb

Mironsoft

Shell-Automatisierung, DevOps-Tooling und Deployment-Infrastruktur

Shell-Skripte, die in der Produktion zuverlässig laufen?

Wir analysieren bestehende Bash-Skripte, erkennen fragile Muster und ersetzen sie durch robuste Bash-Patterns: mit vollständiger Fehlerbehandlung, Logging und sicherer Parallelisierung für euren Deployment-Stack.

Code-Review

ShellCheck-Analyse und manuelle Prüfung auf kritische Bash-Pattern-Verstöße.

Refactoring

Fehlerbehandlung, Logging und sichere Dateioperationen nachrüsten.

CI-Integration

ShellCheck und BATS in Pipelines integrieren und Regressionstests aufbauen.

10. Zusammenfassung

Matrix-Builds mit Bash simulieren: Das Wichtigste auf einen Blick

Grundprinzip

Verschachtelte Bash-Schleifen über Arrays bilden dieselbe Kombinatorik ab, die später in strategy.matrix oder parallel: matrix landet.

Isolation

Docker mit denselben Image-Tags wie die spätere CI-Matrix macht die lokale Simulation realistisch, nicht nur logisch korrekt.

Parallelisierung

xargs -P oder das & mit wait-Muster verkürzt die Gesamtlaufzeit, begrenzt durch die lokale CPU-Kernanzahl.

Grenzen

Keine Runner-Parallelität, kein grafisches Reporting. Die Bash-Simulation ist ein Entwicklungswerkzeug, kein CI-Ersatz.

11. FAQ: Matrix-Builds mit Bash simulieren: Das Wichtigste auf einen Blick

1Was ist eine Matrix-Build in der CI überhaupt?
Ein Matrix-Build testet dieselbe Codebasis gegen mehrere Kombinationen aus Versionen, Betriebssystemen oder Abhängigkeiten, zum Beispiel drei PHP-Versionen gegen zwei Datenbanken. Jede Kombination läuft als eigener Job.
2Warum nicht gleich in der echten CI testen?
Jede Änderung an der CI-Matrix braucht einen Commit, einen Push und mehrere Minuten Wartezeit auf Runner. Eine lokale Bash-Schleife liefert dieselbe Information in Sekunden und spart unnötige Trial-and-Error-Commits.
3Wie strukturiere ich Bash-Arrays für mehrere Dimensionen?
Ein Array pro Dimension, etwa PHP_VERSIONS und DB_VERSIONS, mit verschachtelten for-Schleifen, die alle Kombinationen kartesisch durchlaufen. Die Namen sollten schon so heißen wie später in der YAML-Matrix.
4Wie parallelisiere ich Matrix-Zellen sicher?
Mit xargs -P und einer Funktion, die per export -f verfügbar gemacht wird, oder mit dem klassischen & und wait-Muster, das Prozess-IDs in einem Array sammelt und am Ende gemeinsam abwartet.
5Wie übertrage ich das Ergebnis in GitHub-Actions-Matrix-Syntax?
Die Werte aus den Bash-Arrays wandern unverändert in strategy.matrix. Lokal ausgeschlossene, inkompatible Kombinationen werden dort als exclude-Einträge übernommen.
6Was ist der Unterschied zu GitLab parallel:matrix?
Funktional identisch, nur die YAML-Syntax unterscheidet sich. GitLab nutzt parallel: matrix: mit Listen von Variablen-Objekten statt der include/exclude-Struktur von GitHub Actions.
7Wie vermeide ich zu große lokale Matrizen?
Statt jede Kombination der späteren CI-Matrix lokal zu testen, reicht meist eine repräsentative Teilmenge, etwa die älteste und neueste unterstützte Version pro Dimension.
8Wie sammle ich Exit-Codes zuverlässig ein?
Mit einem assoziativen Array, das den Exit-Code oder Status jeder Matrix-Zelle unter einem sprechenden Schlüssel speichert, und einer abschließenden Schleife, die einen Report ausgibt.
9Kann ich Docker durch etwas Leichteres ersetzen?
Für reine Sprachversionen können Versionsmanager wie phpenv oder asdf ausreichen, wenn keine vollständige Betriebssystem-Isolation nötig ist. Für realistische CI-Nachbildung ist Docker aber näher am tatsächlichen Runner-Setup.
10Wann sollte ich die Bash-Simulation komplett weglassen?
Bei einer bereits stabilen, unveränderten Matrix ohne aktuelle Iteration lohnt sich der doppelte Pflegeaufwand nicht mehr. Dann übernimmt die native CI-Matrix die Aufgabe vollständig.