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
- Tests schlagen sporadisch fehl – Mal läuft die Suite durch, mal bricht sie an derselben Stelle ab. Kein erkennbares Muster.
- Build-Artefakte fehlen – Der Deploy-Job findet die gebauten Dateien nicht, obwohl der vorherige Job sie erstellt haben sollte.
- Umgebungsvariablen werden nicht geladen – Secrets, die lokal funktionieren, sind im Runner plötzlich leer.
- Cache-Probleme – Nach einem Node-Modul-Update läuft
npm installewig 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
| Metrik | Vorher | Nachher |
|---|---|---|
| Durchlaufzeit pro Push | 12–18 Minuten | 6–8 Minuten |
| Flaky-Test-Rate | ca. 25 % | < 2 % |
| Fehlgeschlagene Deploys pro Woche | 3–5 | 0 |
| Zeit für Pipeline-Debugging | 2–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:
- Explizit statt implizit – Jede Abhängigkeit, jede Version, jedes Secret muss klar benannt sein.
- Reproduzierbarkeit ist alles – Was lokal läuft, muss auch in der Pipeline laufen. Gleiche Versionen, gleiches Lock-File, gleiche Umgebung.
- Beobachten, nicht ignorieren – Wenn eine Pipeline mal flackert, ist das ein Warnsignal. Es wird nicht besser von allein.
- 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.