Mathias Eberlein —

Wenn die CI/CD-Pipeline streikt – ein praxisnaher Leitfaden zur Reparatur

Continuous Integration und Continuous Delivery sollen uns Entwicklern das Leben leichter machen. Doch wenn die Pipeline ständig fehlschlägt, wird aus dem Helfer schnell ein Hindernis. Dieser Beitrag beschreibt ein typisches Desaster und zeigt, wie man es Schritt für Schritt behebt.


Das Problem: Jeder Commit ist ein Glücksspiel

Das Setup war eigentlich standardmäßig: Ein GitHub-Repository, eine GitHub Actions-Workflow-Datei und der Plan, bei jedem Push automatisch zu testen, bauen und zu deployen. Die ersten Commits liefen reibungslos. Dann kam der erste Feature-Branch – und die Pipeline begann zu zucken.

Symptome

  1. Tests schlagen sporadisch fehl – Mal läuft die Suite durch, mal bricht sie an derselben Stelle ab. Kein erkennbares Muster.
  2. Build-Artefakte fehlen – Der Deploy-Job findet die gebauten Dateien nicht, obwohl der vorherige Job sie erstellt haben sollte.
  3. Umgebungsvariablen werden nicht geladen – Secrets, die lokal funktionieren, sind im Runner plötzlich leer.
  4. Cache-Probleme – Nach einem Node-Modul-Update läuft npm install ewig und scheitert trotzdem an Peer-Dependencies.

Die Folge: Jeder Merge dauert doppelt so lang wie die eigentliche Entwicklung. Das Team verliert das Vertrauen in die Automation.


Die Ursachenanalyse

Bevor man repariert, muss man verstehen. Ich habe die Pipeline zerlegt und vier zentrale Fehlerquellen identifiziert:

1. Fehlende Isolation zwischen Jobs

Der Test-Job und der Deploy-Job liefen auf demselben Runner ohne explizite Abhängigkeitsdeklaration. Wenn Job A die Artefakte nicht sauber ablegt, sucht Job B ins Leere.

2. Veraltete Cache-Strategie

Der actions/cache-Eintrag für node_modules wurde nie invalidiert. Nach einem package.json-Update baute der Cache mit veralteten Modulen – und die Tests liefen gegen eine unerwartete Umgebung.

3. Secrets wurden falsch referenziert

Die Umgebungsvariablen waren im Repository-Kontext definiert, aber der Workflow nutzte einen environment-Block, für den keine Protection Rules aktiviert waren. Der Runner überschrieb die Werte stillschweigend mit Leerstrings.

4. Kein explizites Node-Version-Pinning

Der Standard-Runner aktualisierte Node im Hintergrund. Was lokal mit Node 18 funktionierte, brach auf dem Runner mit Node 20 an einer veralteten ESLint-Regel.


Die Lösung: Schritt für Schritt zur stabilen Pipeline

Schritt 1: Jobs explizit verketten

Jeder Job bekommt einen klaren needs-Bezug und spezifiziert, welche Artefakte er weitergibt:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
      - run: npm ci
      - run: npm test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '18'
          cache: 'npm'
      - run: npm ci
      - run: npm run build
      - uses: actions/upload-artifact@v4
        with:
          name: dist
          path: dist/

Schritt 2: Cache sauber verwalten

Statt blind zu cachen, nutze ich einen eindeutigen Cache-Schlüssel, der auf package-lock.json basiert:

- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: npm-${{ hashFiles('package-lock.json') }}
    restore-keys: npm-

Damit wird der Cache automatisch ungültig, sobald sich die Lock-Datei ändert.

Schritt 3: Korrekte Secret-Zuordnung

Secrets werden direkt im env-Block des jeweiligen Jobs referenziert, nicht über ein mehrdeutiges environment-Mapping:

- run: npm run deploy
  env:
    DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}

Schritt 4: Node-Version explizit pinnen

Jeder Job erhält die gleiche Node-Version wie die lokale Entwicklung. Das verhindert Überraschungen durch Runner-Updates:

- uses: actions/setup-node@v4
  with:
    node-version: '18'

Ergebnis: Was sich verbessert hat

MetrikVorherNachher
Durchlaufzeit pro Push12–18 Minuten6–8 Minuten
Flaky-Test-Rateca. 25 %< 2 %
Fehlgeschlagene Deploys pro Woche3–50
Zeit für Pipeline-Debugging2–4 Stunden/Woche< 30 Minuten/Woche

Die Pipeline läuft stabil. Jeder Commit lässt sich zuverlässig mergen, ohne dass das Team nachts um drei den Runner neu starten muss.


Die Lehre: Automation braucht Disziplin

Diese Erfahrung hat mich daran erinnert, dass CI/CD-Pipelines keine Set-it-and-forget-it-Lösung sind:

  1. Explizit statt implizit – Jede Abhängigkeit, jede Version, jedes Secret muss klar benannt sein.
  2. Reproduzierbarkeit ist alles – Was lokal läuft, muss auch in der Pipeline laufen. Gleiche Versionen, gleiches Lock-File, gleiche Umgebung.
  3. Beobachten, nicht ignorieren – Wenn eine Pipeline mal flackert, ist das ein Warnsignal. Es wird nicht besser von allein.
  4. Kleine Änderungen, große Wirkung – Oft reicht ein korrekt gesetzter Cache-Schlüssel oder ein explizites node-version, um das gesamte System zu stabilisieren.

Fazit

Eine kaputte CI/CD-Pipeline kostet mehr Zeit, als sie einspart. Mit klaren Job-Abhängigkeiten, sauberem Caching, korrekter Secret-Verwaltung und versiongepinnten Runnern wird die Automation wieder zum echten Helfer. Der Aufwand lohnt sich – bei jedem zukünftigen Commit.