DevOps

CI/CD Pipelines

CI/CD verwandelt jede Änderung in ein getestetes, unveränderliches Artefakt und versendet genau dieses Artefakt jedes Mal auf die gleiche Weise. Eine Pipeline ist schlicht eine Sequenz von Jobs, die immer dann ausgeführt wird, wenn sich der Code bewegt.

intermediate15 min readUpdated 16. Sept. 2026
.github/workflows/ci.yml
yaml
# .github/workflows/ci.yml
name: ci

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test
Trigger
Push, Pull Request oder Tag
Runner
Eine saubere, disposable Maschine
Artefakt
Einmal gebaut, überall ausgerollt
Gates
Lint, Typecheck und Tests
Delivery
Automatisierter Release, manuelle Freigabe
Deployment
Vollautomatisch bis zur Production

Warum es wichtig ist

Was eine Pipeline dir bietet

Jede Änderung wird verifiziert

Jeder Push führt die gleichen Build-, Lint-, Typecheck- und Test-Schritte in einer sauberen Umgebung aus, sodass fehlerhafter Code bereits während des Reviews und nicht erst nach einem Release entdeckt wird.

Ein Artefakt, viele Umgebungen

Die Pipeline baut das Image oder Bundle genau einmal und befördert dieselben Bytes durch Staging und Production, sodass das, was du getestet hast, auch das ist, was du auslieferst.

Releases werden langweilig

Deployments sind automatisiert, beobachtbar und reversibel. Ein Rollback bedeutet das erneute Deployment des vorherigen Tags, nicht das unter Zeitdruck neu Bauen aus einem alten Branch.

Das Gesamtbild

Die drei Grundideen hinter CI/CD

Ein Event löst die Arbeit aus, ein disposable Runner führt sie aus, und ein unveränderliches Artefakt ist das, was zwischen den Umgebungen bewegt wird.

Der Trigger

Reagieren

Ein Push, ein Pull Request oder ein Tag startet die Pipeline. Das Event entscheidet, welche Jobs laufen und welche Umgebung sie ansteuern.

Der Runner

Ausführen

Eine disposable Maschine checkt den Code aus und führt jeden Schritt aus. Nichts überlebt zwischen den Jobs, was das Ergebnis reproduzierbar macht.

Das Artefakt

Ausliefern

Der Build erzeugt ein unveränderliches Output, wie ein Container-Image oder ein Bundle, das unverändert durch jede Umgebung befördert wird.

HTML5 auf einen Blick

Die beweglichen Teile einer Pipeline

Trigger

on push, pull_request oder tag entscheidet, wann eine Pipeline startet.

Jobs und Steps

Ein Job läuft auf einem Runner; seine Steps werden nacheinander ausgeführt und brechen bei Fehlern sofort ab (fail fast).

Artefakte

Der Build-Output wird einmal gespeichert und von späteren Jobs heruntergeladen.

Caching

Dependency- und Build-Caches sparen bei jedem Durchlauf wertvolle Minuten.

Matrices

Führe denselben Job über verschiedene Node-Versionen und Betriebssysteme hinweg aus.

Secrets

Zugangsdaten werden zur Laufzeit injiziert und in den Logs maskiert.

Ablauf

Was eine Pipeline bei jedem Push tut

Das gleiche Prinzip gilt, egal ob die Pipeline zehn oder hundert Zeilen umfasst und ob sie auf eine VM oder einen Kubernetes Cluster deployt.

  1. 1

    Trigger bei Push oder Pull Request

    Die Forge sendet ein Event mit dem Commit, dem Branch und dem Actor, und die Pipeline wählt die zutreffenden Jobs aus.

  2. 2

    Code auschecken

    Ein frischer Runner klont den exakten Commit, sodass nichts aus einem vorherigen Durchlauf in diesen hineinfließen kann.

  3. 3

    Caches wiederherstellen

    Dependency- und Build-Caches werden über den Lockfile-Hash wiederhergestellt, wodurch eine Kaltinstallation zu einer Warminstallation wird.

  4. 4

    Dependencies installieren

    Eine Lockfile-basierte Installation fixiert den Dependency-Tree, sodass jeder Durchlauf und jede Maschine dieselben Versionen auflöst.

  5. 5

    Lint, Typecheck und Test

    Schnelle Prüfungen laufen zuerst und lassen die Pipeline früh scheitern. Ein rotes Gate verhindert, dass das Artefakt überhaupt gebaut wird.

  6. 6

    Artefakt oder Image bauen

    Das Output wird einmal gebaut und in eine Registry hochgeladen oder gepusht, mit dem Commit getaggt, um die Rückverfolgbarkeit zu gewährleisten.

  7. 7

    Deploy auf dem Main-Branch

    Nur Merges in den geschützten Branch führen zum Deployment, meist hinter einer Umgebung mit eigenen Secrets und Freigaben.

  8. 8

    Smoke Check ausführen

    Ein kurzer Request gegen den neuen Release bestätigt die Funktionsfähigkeit, bevor der Traffic vollständig umgeleitet wird.

  9. 9

    Status melden

    Der Commit erhält einen Pass- oder Fail-Check; ein fehlgeschlagenes Deployment wird zurückgerollt oder die vorherige Version bleibt aktiv.

Der vollständige Leitfaden

CI/CD Pipelines: Alles was Sie wissen müssen

Was CI und CD eigentlich bedeuten

Continuous Integration ist die Praxis, die Arbeit jedes Entwicklers häufig in einen gemeinsamen Branch zusammenzuführen und diese automatisch zu verifizieren. Jeder Merge löst einen Build, einen Lint-Pass und die Test-Suite auf einer Maschine aus, auf der niemand aktiv entwickelt. Ziel ist es, Integrationsprobleme innerhalb von Minuten zu finden, solange die Änderung noch klein ist, anstatt erst bei einem mühsamen Merge am Ende eines langen Branches.

Continuous Delivery erweitert diese Idee: Der Main-Branch befindet sich immer in einem releasefähigen Zustand, und die Pipeline kann ihn jederzeit in die Produktion deployen. Das Artefakt wird bei jeder Änderung gebaut und getestet; ein Mensch entscheidet, wann das Release erfolgt. Continuous Deployment entfernt diese menschliche Entscheidung und schickt jede Änderung, die die Prüfschritte erfolgreich durchläuft, direkt in die Produktion.

Der einzige Unterschied zwischen Delivery und Deployment ist das Approval-Gate. Alles davor – der Build, die Tests, das Artefakt – ist identisch.

Teams behaupten oft, sie würden „CI/CD machen“, obwohl sie beides nicht tun. Continuous Integration setzt voraus, dass der Build auf einer gemeinsamen, autoritativen Maschine läuft und nicht nur auf einem Laptop vor einem Commit. Continuous Delivery setzt voraus, dass das von der Pipeline erzeugte Artefakt auch dasjenige ist, das ausgeliefert wird, und nicht etwas, das zum Zeitpunkt des Releases manuell neu gebaut wird. Wenn eines dieser Elemente fehlt, ist der Kreislauf unterbrochen und die Vorteile gehen verloren.

Die Anatomie einer Pipeline

Jede Pipeline, von einer zehnzeiligen GitHub Actions-Datei bis hin zu einem Enterprise-Setup mit tausend Jobs, basiert auf demselben Vokabular.

  • Trigger — das Ereignis, das einen Durchlauf startet: ein Push, ein Pull Request, ein Tag, ein Zeitplan oder ein manueller Dispatch.
  • Workflow — die Datei, in der Trigger und Jobs definiert werden. In GitHub Actions befindet sich diese in .github/workflows/.
  • Job — eine Arbeitseinheit, die auf einem Runner ausgeführt wird. Jobs laufen parallel, es sei denn, Sie definieren Abhängigkeiten mit needs.
  • Step — ein einzelner Befehl oder eine wiederverwendbare Action innerhalb eines Jobs. Steps werden nacheinander ausgeführt; der Job bricht beim ersten Fehler ab.
  • Runner — die Maschine, die einen Job ausführt. Hosted Runner sind Wegwerf-Instanzen; self-hosted Runner sind Maschinen, die Sie selbst verwalten.
  • Artifact — eine von einem Job erzeugte Datei, die für spätere Jobs oder für Menschen gespeichert wird: eine Binary, ein Bundle, ein Testbericht.
  • Cache — ein Verzeichnis, das zwischen Durchläufen wiederhergestellt wird, um aufwendige Arbeiten wie die Installation von Abhängigkeiten zu vermeiden.
  • Environment — ein benanntes Ziel wie staging oder production mit eigenen Secrets, Schutzregeln und einer URL.
  • Secret — ein verschlüsselter Wert, der zur Laufzeit in einen Job injiziert und in den Logs maskiert wird.
  • Status check — das Ergebnis (bestanden oder fehlgeschlagen), das an den Commit zurückgemeldet wird und welches durch Branch Protection vorausgesetzt werden kann.

Sobald diese Begriffe klar sind, wird jedes CI-System lesbar. Jenkins, GitLab CI, CircleCI und GitHub Actions nutzen alle dieselben Konzepte, verwenden lediglich unterschiedliche Bezeichnungen.

Ein GitHub Actions Workflow, Zeile für Zeile

Der kleinste nützliche Workflow checkt den Code aus, installiert die Abhängigkeiten und führt die Tests aus.

name: ci

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npm test

on definiert die Trigger. Ein Push auf main sowie jeder Pull Request starten jeweils einen Run. jobs.test wird auf einem frischen ubuntu-latest Runner ausgeführt. uses führt eine wiederverwendbare Action aus; run führt einen Shell-Befehl aus. npm ci installiert exakt das, was in der Lockfile fixiert ist, wodurch der Run reproduzierbar wird. cache: npm weist die Setup-Action an, das npm-Verzeichnis zu cachen, wobei die Lockfile als Key dient.

Fügen Sie concurrency hinzu, um veraltete Runs abzubrechen, was auf aktiven Branches sowohl Zeit als auch Geld spart:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

Wenn ein Entwickler drei Commits hintereinander pusht, überlebt nur der neueste Run. Die älteren werden abgebrochen, und der Pull Request zeigt einen einzigen aktuellen Status anstelle einer Warteschlange veralteter Statusmeldungen.

Einmal bauen, dasselbe Artefakt deployen

Die wertvollste Gewohnheit in einer Pipeline ist es, genau einmal zu bauen. Die Pipeline kompiliert, testet und paketiert die Anwendung ein einziges Mal, und jede Umgebung von Staging bis Production führt genau dieses Ergebnis aus.

Die Alternative – für jede Umgebung neu zu bauen – scheint harmlos zu sein, ist es aber nicht. Unterschiedliche Builds können verschiedene Dependency-Versionen auflösen, unterschiedliche Base-Image-Digests verwenden oder ein anderes NODE_ENV einbetten. Staging testet dann ein Binary, das in Production niemals laufen wird, und „es ist durch Staging gekommen“ beweist somit gar nichts.

Zwei Regeln machen den Build-Once-Ansatz praktikabel. Erstens: Taggen Sie das Artefakt nach Commit, niemals nach einem veränderbaren Namen wie latest:

docker build -t ghcr.io/me/app:$GIT_SHA .
docker push ghcr.io/me/app:$GIT_SHA

Zweitens: Konfiguration zur Laufzeit injizieren. Das Image sollte nicht wissen, ob es in Staging oder Production läuft. Datenbank-URLs, Feature-Flags und API-Keys werden als Umgebungsvariablen übergeben, wenn der Container startet. Der Build ist überall identisch; nur die Umgebung unterscheidet sich.

Um ein Release zu promoten, referenzieren Sie den unveränderlichen Digest anstatt des Tags, sodass selbst ein neu getaggtes Image nicht abweichen kann:

kubectl set image deploy/app \
  app=ghcr.io/me/app@sha256:...

Dies ist das Fundament, das Rollbacks trivial macht. Das vorherige Release ist einfach der vorherige Digest, der immer noch in der Registry liegt und in Sekundenschnelle deployt werden kann.

Tests, Linting und Typechecking als Gates

Ein Gate ist eine Prüfung, die erfolgreich bestanden werden muss, bevor die Pipeline fortgesetzt wird. Die essenziellen Gates sind schnell und kostengünstig: Linting, Typechecking und die Unit-Test-Suite. Diese laufen bei jedem Pull Request und verhindern den Build des Artifacts, falls sie fehlschlagen.

Ordnen Sie diese von der schnellsten zur langsamsten Ausführung. Linting und Typechecking sind meist innerhalb von Sekunden abgeschlossen und fangen eine große Klasse von Fehlern ab. Danach folgen die Unit-Tests. Langsamere Integration- und End-to-End-Tests können parallel oder ausschließlich auf dem Main-Branch laufen.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 22, cache: npm }
      - run: npm ci
      - run: npm test

Zwei Jobs laufen parallel, jeder auf seinem eigenen Runner. Das Linting schlägt innerhalb von zwanzig Sekunden fehl, ohne auf die Test-Suite warten zu müssen. Der Kompromiss besteht darin, dass beide Jobs die Abhängigkeiten installieren; ein gemeinsamer Cache hält diesen Prozess kostengünstig.

Gates sind nur dann sinnvoll, wenn sie verpflichtend sind. Markieren Sie die Checks in den Branch-Protection-Einstellungen Ihrer Forge als „required“, sodass ein Pull Request nicht gemergt werden kann, solange einer der Checks auf „rot“ steht.

Matrizen und Parallelisierung

Eine Matrix führt eine Job-Definition über mehrere Kombinationen von Inputs aus. So lassen sich verschiedene Node-Versionen und Betriebssysteme testen, ohne das YAML duplizieren zu müssen.

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, macos-latest]
        node: [20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: npm
      - run: npm ci
      - run: npm test

Dies erweitert sich zu vier Jobs. fail-fast: false sorgt dafür, dass die anderen Jobs beendet werden, selbst wenn einer fehlschlägt – das ist besonders nützlich, um herauszufinden, ob ein Fehler versionsspezifisch ist.

Parallelisierung ist nicht kostenlos. Jeder Job stellt einen Runner bereit und installiert Abhängigkeiten, weshalb eine breite Matrix mehr Zeit und Geld kostet als eine schmale. Testen Sie die Versionen, die Sie tatsächlich unterstützen, und nicht jede existierende Version.

Bei großen Test-Suites sollten Sie die Tests selbst sharden: Übergeben Sie einen Shard-Index und die Gesamtzahl an den Test-Runner, sodass jeder von vier Jobs ein Viertel der Tests ausführt. Die Suite ist dann in etwa einem Viertel der Zeit abgeschlossen.

Secrets und Umgebungsvariablen

Secrets sind verschlüsselte Werte, die die Pipeline zur Laufzeit injiziert. Sie gehören in den Secret Store der Plattform, niemals in das Repository und niemals in das Image.

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          API_KEY: ${{ secrets.API_KEY }}

Die Plattform maskiert bekannte Secret-Werte in den Logs, allerdings erfolgt diese Maskierung nach dem Best-Effort-Prinzip. Sie kann keine Secrets erfassen, die transformiert, base64-kodiert oder von einem Tool ausgegeben wurden, das sie neu formatiert. Die goldene Regel ist einfach: Geben Sie niemals ein Secret aus. Führen Sie env nicht in einem Debug-Schritt aus, führen Sie kein echo eines Tokens durch und übergeben Sie keine Secrets an einen Befehl, der seine Argumente protokolliert.

Beschränken Sie den Scope von Secrets auf die Umgebung, die sie tatsächlich benötigt. Ein Staging-Deployment hat keinen Grund, das Passwort der Produktionsdatenbank zu besitzen. Auf GitHub sind Environment Secrets nur für Jobs verfügbar, die diese Umgebung deklarieren, und können eine vorherige Genehmigung erfordern.

Bevorzugen Sie bei Cloud-Providern OIDC gegenüber langlebigen Keys. Die Pipeline tauscht ein kurzlebiges Identity-Token gegen temporäre Anmeldedaten aus, sodass es kein statisches Secret gibt, das geleakt werden oder rotiert werden muss.

permissions:
  id-token: write
  contents: read

steps:
  - uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789012:role/deploy
      aws-region: eu-west-1

Das Token ist nur für wenige Minuten gültig und auf eine einzige Rolle beschränkt. Falls es geleakt wird, beschränkt sich der Schadensradius auf einen einzigen Durchlauf.

Eine Warnung verdient besondere Aufmerksamkeit: Pull Requests aus Forks führen nicht vertrauenswürdigen Code aus. Geben Sie niemals Secrets für diesen Code frei. Verwenden Sie pull_request anstelle von pull_request_target für alles, was Contributor-Code auscheckt und baut, und verlangen Sie eine Genehmigung, bevor Workflows von Erstmitwirkenden ausgeführt werden.

Environments, Approvals und Protected Branches

Ein Environment ist ein benanntes Deployment-Ziel mit eigenen Secrets, Schutzregeln und einer URL. Es ist der Mechanismus, der die Production-Umgebung von allem anderen trennt.

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://app.example.com
    steps:
      - run: ./deploy.sh

Environments können eine manuelle Genehmigung (Approval) erfordern, bevor ein Job ausgeführt wird, einschränken, welche Branches in sie deployen dürfen, und festlegen, wie lange ein Deployment wartet, bevor es in einen Timeout läuft. Hier befindet sich der „menschliche Button“ von Continuous Delivery: Die Pipeline läuft automatisch bis zum Gate, und ein Reviewer gibt sie frei.

Protected Branches ergänzen die Environments. Erfordern Sie für den Main-Branch einen Pull Request, erfolgreich bestandene Status-Checks sowie ein Review und verbieten Sie Force Pushes. Zusammen stellen sie sicher, dass der einzige Weg in die Production über die Pipeline führt – und genau das ist das Ziel.

Deployment-Strategien und Rollbacks

Wie eine neue Version eine alte ersetzt, ist eine Design-Entscheidung mit realen Auswirkungen auf die Nutzer.

Rolling Deployment ersetzt Instanzen nach und nach in kleinen Gruppen. Die Kapazität sinkt während des Wechsels leicht ab, aber es wird keine zusätzliche Infrastruktur benötigt. Dies ist auf den meisten Plattformen der Standard und der richtige Ausgangspunkt.

Blue-green Deployment betreibt zwei vollständige Umgebungen. Der Traffic wird auf „Blue“ geleitet, während „Green“ die neue Version erhält; sobald Green stabil läuft, schaltet eine einzige Routing-Änderung den gesamten Traffic um. Ein Rollback erfolgt durch das Zurückschalten, was nahezu instantan geschieht. Der Preis dafür ist der Betrieb von zwei parallelen Umgebungen.

Canary Deployment leitet einen kleinen Teil des Traffics – erst ein Prozent, dann fünf, dann fünfzig – an die neue Version weiter und überwacht Fehlerraten und Latenzen, bevor es fortfährt. So werden Probleme abgefangen, die ein Health Check nicht erkennt, allerdings auf Kosten eines komplexeren Routings und Monitorings.

Unabhängig von der Strategie muss das Deployment reversibel sein. Da das vorherige Artefakt unveränderlich (immutable) ist und sich noch in der Registry befindet, ist ein Rollback ein erneutes Deployment des vorherigen Digests und kein erneuter Build:

kubectl rollout undo deploy/app

Zwei Details machen Rollbacks sicher. Datenbank-Migrationen sollten für mindestens einen Release abwärtskompatibel sein, damit der alte Code weiterhin mit dem neuen Schema funktioniert. Und Feature Flags ermöglichen es, neues Verhalten zu deaktivieren, ohne überhaupt etwas deployen zu müssen – das ist der schnellste Rollback, den es gibt.

Artifact-Registries und Container-Images

Eine Artifact-Registry speichert die Build-Ergebnisse: Container-Images, npm-Pakete, Binärdateien, Tarballs. Sie ist der Übergabepunkt zwischen Build und Deployment und sollte immutable sein.

Container-Images sind die am häufigsten verwendeten Artifacts, da sie ihre eigene Runtime mitbringen. Eine Registry wie die GitHub Container Registry, Amazon ECR oder Docker Hub speichert Layer, und die Pipeline pusht pro Build einmal dorthin. Deployments ziehen dann den exakten Digest.

- uses: docker/build-push-action@v6
  with:
    context: .
    push: true
    tags: ghcr.io/${{ github.repository }}:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

Die Optionen cache-from und cache-to halten das Docker-Layer-Caching über mehrere Runs hinweg aufrecht, sodass ein unveränderter Dependency-Layer nicht jedes Mal komplett neu gebaut werden muss.

Legen Sie eine Retention-Policy fest. Registries sammeln schnell Gigabytes an Daten an, und das Löschen alter Images ist essenziell, um die Pipeline schnell zu halten. Behalten Sie mindestens die letzten Releases, damit ein Rollback weiterhin möglich ist.

Status-Checks und Branch-Protection

Ein Status-Check ist das Urteil der Pipeline über einen Commit. Die Forge verknüpft diesen mit dem Pull Request, und die Branch-Protection legt fest, ob dieser Check nur empfehlend oder zwingend erforderlich ist.

Konfiguriere den main-Branch so, dass Folgendes erforderlich ist:

  • Ein Pull Request vor dem Mergen, mit mindestens einer Genehmigung.
  • Erfolgreiche Status-Checks, einschließlich Lint, Typecheck und Tests.
  • Ein Branch, der vor dem Mergen auf den neuesten Stand von main gebracht wurde, damit die Checks gegen den Code laufen, der tatsächlich übernommen wird.
  • Keine Force-Pushes und keine Löschungen.

Das Ergebnis ist, dass der main-Branch immer „grün“ ist. Jeder Commit darauf hat dieselben Hürden genommen, und jedes Deployment davon ist ein bekanntes, funktionierendes Artefakt.

Halte die Liste der erforderlichen Checks kurz. Wenn eine langsame End-to-End-Suite bei jedem Pull Request zwingend erforderlich ist, müssen Entwickler warten; wenn sie stattdessen in der Merge-Queue oder auf dem main-Branch gefordert wird, bietet dies die gleiche Sicherheit ohne die Reibungsverluste.

Caching und inkrementelle Builds

Caching ist der Unterschied zwischen einer zwei-minütigen und einer zwölf-minütigen Pipeline. Das Prinzip dahinter ist, niemals Arbeit zu wiederholen, deren Inputs sich nicht geändert haben.

Der wertvollste Cache sind die Abhängigkeiten. Verwenden Sie den Hash der Lockfile als Key, damit der Cache genau dann ungültig wird, wenn sich die Abhängigkeiten ändern:

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

restore-keys ermöglicht einen partiellen Match: Selbst wenn der exakte Key nicht gefunden wird, wird der aktuellste Cache wiederhergestellt und inkrementell aktualisiert, was wesentlich schneller ist als eine komplette Neuinstallation (Cold Install).

Build-Tools bringen eigene Caches mit. Der inkrementelle Build von TypeScript, das Dependency Pre-Bundling von Vite und der Layer-Cache von Docker profitieren alle davon, zwischen den Durchläufen persistiert zu werden. Nutzen Sie bei Docker die Registry oder das Cache-Backend Ihres CI-Providers anstelle des lokalen Daemons, da dieser zusammen mit dem Runner verworfen wird.

Ein Cache ist eine Optimierung, niemals eine Source of Truth. Wenn ein Cache korrupt oder veraltet ist, muss der Durchlauf trotzdem korrekt sein. Build-Tools, die einem Cache blind vertrauen, können fehlerhafte Ergebnisse liefern. Setzen Sie Cache-Keys daher konservativ und behandeln Sie einen Cache-Miss als normalen Vorgang.

Pipelines für Monorepos

Ein Monorepo enthält viele Packages in einem einzigen Repository. Eine naive Pipeline baut und testet bei jeder Änderung alle Packages neu, was mit wachsendem Repository immer langsamer wird.

Die Lösung ist die Change Detection. Pfadfilter und Abhängigkeitsgraphen teilen der Pipeline mit, welche Packages von einem Diff betroffen sind, sodass nur diese gebaut werden.

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: pnpm install --frozen-lockfile
      - run: pnpm turbo run test --filter='...[origin/main]'

Der Filter wählt die seit main geänderten Packages sowie alles aus, was von ihnen abhängt. Eine Änderung an einem Shared Utility löst dessen Abhängige aus; eine Änderung an einem Leaf Package löst nur dieses selbst aus.

Fügen Sie einen Remote Cache hinzu, damit kompilierte Ausgaben und Testergebnisse über verschiedene Maschinen und Durchläufe hinweg geteilt werden. Wenn die Inputs eines Packages unverändert sind, wird dessen Build wiederhergestellt, anstatt ihn neu zu berechnen. In einem Monorepo ist dies oft ein größerer Gewinn als das Caching von Abhängigkeiten.

Halten Sie die Pipeline ehrlich: Eine Änderung an der Root Lockfile oder einer gemeinsamen Konfiguration sollte weiterhin die gesamte Testsuite ausführen. Selektives Testen ist eine Optimierung, die niemals zulassen darf, dass ein defektes Package durchrutscht.

Pipeline-Sicherheit

Die Pipeline enthält Zugangsdaten zur Produktionsumgebung, was sie zu einem attraktiven Ziel für Angriffe macht. Behandeln Sie Workflow-Dateien als sicherheitskritischen Code und prüfen Sie Änderungen daran sorgfältig.

Actions auf eine Version pinnen. Ein Tag wie @v4 ist ein beweglicher Zeiger; ein vollständiger Commit-SHA hingegen ist unveränderlich. Das Pinnen auf einen SHA ist die strengste Option und verhindert, dass ein kompromittierter Tag bösartigen Code in Ihrer Pipeline ausführt.

Das Prinzip der geringsten Berechtigung anwenden. Setzen Sie permissions explizit für jeden Workflow und jeden Job fest, standardmäßig auf schreibgeschützt (read-only), und fügen Sie Schreibberechtigungen nur dort hinzu, wo sie zwingend erforderlich sind.

permissions:
  contents: read

Die Standardeinstellung für GITHUB_TOKEN ist oft weitreichender, als es jeder einzelne Job benötigt. Durch eine Einschränkung wird verhindert, dass ein kompromittierter Schritt Commits pushen oder Pakete veröffentlichen kann.

OIDC gegenüber statischen Keys bevorzugen, wie oben beschrieben. Kurzlebige Zugangsdaten, die auf eine bestimmte Rolle beschränkt sind, eliminieren die langfristigen Secrets, nach denen Angreifer am meisten suchen.

Keinen nicht vertrauenswürdigen Code mit Secrets ausführen. Pull Requests aus Forks sind der klassische Angriffsvektor. Nutzen Sie pull_request für Build und Test, fordern Sie eine Genehmigung für Erstbeiträge an und checken Sie niemals Code aus einem Fork aus, um ihn in einem Workflow auszuführen, der Zugriff auf Secrets hat.

Third-Party Actions prüfen. Eine Action ist eine Abhängigkeit, die mit Ihren Zugangsdaten ausgeführt wird. Bevorzugen Sie Actions direkt von der Forge selbst oder von bekannten Publishern und prüfen Sie alle anderen sorgfältig.

Best Practices

  • Erstellen Sie das Artifact genau einmal und befördern Sie es durch jede Umgebung.
  • Taggen Sie Artifacts per Commit SHA oder Digest, niemals nur per latest.
  • Injizieren Sie die Konfiguration zur Laufzeit; halten Sie den Build überall identisch.
  • Führen Sie Linting und Typecheck vor der zeitintensiveren Test-Suite aus, damit Fehler schnell erkannt werden.
  • Machen Sie wichtige Prüfungen in den Branch Protection Rules verpflichtend.
  • Cachen Sie Abhängigkeiten basierend auf dem Hash der Lockfile und nutzen Sie Restore Keys.
  • Beschränken Sie Secrets auf bestimmte Umgebungen und bevorzugen Sie kurzlebige OIDC-Credentials.
  • Geben Sie Secrets niemals aus und legen Sie sie niemals für nicht vertrauenswürdige Pull Requests offen.
  • Pinnen Sie Third-Party Actions und setzen Sie Token-Berechtigungen nach dem Least-Privilege-Prinzip.
  • Nutzen Sie concurrency, um veraltete Runs abzubrechen.
  • Halten Sie die Pipeline schnell; eine langsame Pipeline wird umgangen.
  • Gestalten Sie Rollbacks als Redeploy des vorherigen Digests.

Häufige Fehler

  • Das Image für jede Umgebung unterschiedlich neu zu bauen und dies als „Promotion“ zu bezeichnen.
  • Umgebungskonfigurationen bereits zur Build-Zeit fest in das Image einzubauen.
  • latest als einzigen Tag zu verwenden und dadurch die Rückverfolgbarkeit zu verlieren.
  • Secrets in einem Debug-Schritt auszugeben und davon auszugehen, dass das Masking alles abfängt.
  • Secrets für Workflows offenzulegen, die durch Pull Requests von Forks ausgelöst werden.
  • Third-Party Actions über einen sich ändernden Branch zu referenzieren.
  • Die Standard-Token-Berechtigungen komplett offen zu lassen.
  • Jeden optionalen Check als erforderlich zu markieren, wodurch Merges extrem langsam werden.
  • Flaky Tests dauerhaft auf „rot“ zu lassen, bis niemand mehr die Ergebnisse liest.
  • Aggressives Caching ohne einen Key, der bei tatsächlichen Input-Änderungen invalidiert wird.
  • In einem Monorepo bei jeder Änderung alles neu zu bauen.
  • Keinen Rollback-Pfad zu haben, außer einen Commit zu revertieren und zu warten.

Wie geht es weiter?

Eine Pipeline endet mit dem Deployment eines Artifacts. Der nächste logische Schritt sind daher Cloud Platforms, wo das Release, Health Checks und Rollbacks tatsächlich stattfinden. Das Artifact ist in der Regel ein Container; der Docker-Guide behandelt die Images und Multi-Stage Builds, die von der Pipeline erzeugt werden. Da die meisten Runner Linux-Maschinen sind, erklärt der Linux-Guide die Shell und die Tools, von denen Ihre Steps abhängen, während Node.js die Grundlagen für die Runtime und die Toolchain liefert, die gerade aufgebaut wird.

In der Praxis

Vier Jobs, eine Pipeline

Continuous Integration, eine Test-Matrix, ein Image-Build und ein gated Deploy — die gesamte Struktur einer Production-Pipeline.

.github/workflows/ci.yml
name: ci

on:
  push:
    branches: [main]
  pull_request:

concurrency:
  group: ci-${{ github.ref }}
  cancel-in-progress: true

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm

      - run: npm ci
      - run: npm run lint
      - run: npm run typecheck
      - run: npm test

Einmal bauen, Artefakt befördern

Ein erneutes Bauen pro Umgebung bedeutet, dass Staging und Production unterschiedliche Binaries aus derselben Quelle ausführen. Baue einmal, tagge nach Commit und befördere genau dieses Artefakt.

Bevorzugt
# build once, tagged by the commit
docker build -t ghcr.io/me/app:$GIT_SHA .
docker push ghcr.io/me/app:$GIT_SHA

# every environment runs the same digest
kubectl set image deploy/app \
  app=ghcr.io/me/app@sha256:...
Vermeiden
# rebuild separately for each environment
docker build -t app:staging \
  --build-arg NODE_ENV=staging .
docker build -t app:prod \
  --build-arg NODE_ENV=production .

# staging and prod are different binaries

Gepinnte Action-Versionen

Eine Action, die über einen beweglichen Branch referenziert wird, kann sich jederzeit ändern, was sowohl ein Zuverlässigkeits- als auch ein Supply-Chain-Risiko darstellt.

Bevorzugt
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: docker/build-push-action@v6
Vermeiden
- uses: actions/checkout@master
- uses: some-org/some-action@main
# runs whatever the branch points at today

Abwägungen

Lohnt sich der Aufwand für eine vollständige Pipeline?

Eine Pipeline ist Code, den du warten musst. Für jedes Projekt mit mehr als einem Mitwirkenden amortisiert sie sich fast sofort.

Strengths

  • Feedback kommt früh

    Ein kaputter Build ist ein fünfminütiger Fix in einem Pull Request, kein Rollback um Mitternacht. Je früher ein Defekt gefunden wird, desto günstiger ist er.

  • Releases sind wiederholbar

    Die gleichen Schritte laufen jedes Mal ab, sodass das Ausliefern nicht mehr davon abhängt, wer an der Tastatur sitzt und welchen lokalen State diese Person gerade hat.

  • Das Artefakt ist rückverfolgbar

    Ein Commit ist einem Image-Digest zugeordnet, welcher wiederum einem laufenden Deployment entspricht. Die Antwort auf die Frage „Was ist in Production?“ wird zum einfachen Lookup statt zu einer Untersuchung.

Trade-offs

  • Langsame Pipelines werden ignoriert

    Sobald ein Durchlauf dreißig Minuten dauert, hören die Leute auf, ihn zu beobachten, und mergen trotz roter Tests. Halte die schnellen Gates schnell und lass die teuren parallel laufen.

  • Secrets sind eine echte Angriffsfläche

    Jeder Workflow, der nicht vertrauenswürdigen Code ausführt, kann dazu verleitet werden, Credentials preiszugeben. Scrope Tokens eng, nutze kurzlebige OIDC-Credentials und exponiere Secrets niemals gegenüber Pull Requests aus Forks.

  • Flaky Tests vergiften das Vertrauen

    Ein Test, der in einem von zehn Durchläufen fehlschlägt, lehrt das Team, auf 'Retry' zu drücken, statt den Fehler zu analysieren. Setze flaky Tests in Quarantäne, anstatt sie zu tolerieren.

Häufig gestellte Fragen

Häufig gestellte Fragen

Keep learning

Related topics from the roadmap.

$ Lernen Sie jetzt

Bereit, CI/CD Pipelines zu lernen?

Unser interaktives Tutorial führt Sie Schritt für Schritt durch CI/CD Pipelines — mit Quizzen und echtem Code, den Sie im Browser ausführen können.