Ein GitHub Actions Workflow ist mehr als nur eine YAML-Datei mit Konfigurationsanweisungen. Es ist ein lebendiges Konstrukt, das auf Ereignisse reagiert, Aufgaben orchestriert und dabei eine klare, hierarchische Struktur befolgt. Bevor wir uns in die Syntax-Details stürzen, lohnt sich ein Blick auf das große Ganze – das mentale Modell, das uns hilft, Workflows nicht nur zu schreiben, sondern zu verstehen.
Stellen wir uns einen Workflow wie eine Fabrikproduktion vor: Ein Auslöser (etwa eine Bestellung) startet die Produktion. Die Produktionslinie besteht aus mehreren Stationen (Jobs), die nacheinander oder parallel arbeiten. Jede Station führt konkrete Handgriffe (Steps) aus, für die sie teils spezialisierte Werkzeuge (Actions) einsetzt. Diese Analogie mag vereinfacht sein, trägt aber das grundlegende Prinzip: Workflows sind strukturierte Automatisierungsketten mit klar definierten Ebenen.
GitHub Actions folgt einem strikten, hierarchischen Aufbau. Diese Hierarchie ist nicht willkürlich gewählt, sondern spiegelt die logische Aufteilung von Automatisierungsaufgaben wider. Schauen wir uns die einzelnen Ebenen von oben nach unten an.
Der Workflow bildet die oberste Ebene. Er ist die
Datei im .github/workflows-Verzeichnis und definiert, wann
die Automatisierung startet (on:), welche Jobs ausgeführt
werden und wie diese zusammenhängen. Ein Repository kann beliebig viele
Workflows enthalten – einen für CI-Tests, einen für Deployments, einen
für Release-Automatisierung. Jeder Workflow ist eine eigenständige
Einheit mit eigenem Lebenszyklus.
Jobs sind logische Arbeitseinheiten innerhalb eines
Workflows. Jeder Job läuft in einer isolierten Umgebung auf einem Runner
(einer virtuellen Maschine oder einem Container). Diese Isolation ist
wichtig: Jobs teilen sich standardmäßig keine Dateien oder
Umgebungsvariablen. Ein typischer CI/CD-Workflow könnte drei Jobs haben:
build, test und deploy. Jobs
können sequenziell oder parallel ausgeführt werden – eine Entscheidung,
die wir über Abhängigkeiten steuern.
Steps sind die konkreten Anweisungen innerhalb eines
Jobs. Sie werden sequenziell abgearbeitet, einer nach dem anderen. Ein
Step kann entweder ein Shell-Kommando ausführen (run:) oder
eine vorgefertigte Action aufrufen (uses:). Steps teilen
sich den Workspace und können auf Dateien zugreifen, die vorherige Steps
erstellt haben. Hier findet die eigentliche Arbeit statt: Code
auschecken, Dependencies installieren, Tests ausführen, Builds
erstellen.
Actions sind die unterste Ebene – wiederverwendbare
Bausteine, die komplexe Aufgaben kapseln. Eine Action ist selbst ein
kleines Programm, das in einem Step aufgerufen wird. Sie kann in
TypeScript geschrieben sein, als Docker-Container vorliegen oder selbst
wieder aus mehreren Steps bestehen (Composite Actions). Actions sind das
Geheimnis der Produktivität in GitHub Actions: Statt jedes Mal
Docker-Login-Logik neu zu implementieren, rufen wir einfach
docker/login-action@v3 auf.
Die Hierarchie lässt sich tabellarisch so zusammenfassen:
| Ebene | Scope | Isolation | Typische Anzahl | Ausführung |
|---|---|---|---|---|
| Workflow | Repository | Pro Datei | 1–10 pro Repo | Event-gesteuert |
| Job | Workflow | Eigener Runner | 1–5 pro Workflow | Parallel/Sequenziell |
| Step | Job | Shared Workspace | 5–20 pro Job | Sequenziell |
| Action | Step | In-Process | 0–mehrere pro Step | Per Step-Aufruf |
Ein Workflow ohne Trigger ist wie ein Programm ohne
main()-Funktion – er existiert, wird aber nie ausgeführt.
Der on:-Schlüssel in der Workflow-Datei definiert, welche
Ereignisse den Workflow starten. Diese Events können aus verschiedenen
Quellen stammen und haben unterschiedliche Charakteristiken.
Repository-Events sind die häufigste Trigger-Art.
Sie entstehen durch Aktionen im Repository selbst: ein Push auf einen
Branch, das Öffnen eines Pull Requests, das Erstellen eines Issues.
GitHub unterscheidet dabei zwischen dem Event-Typ (z.B.
push) und optionalen Filtern (z.B. nur für
main-Branch oder bestimmte Dateipfade). Ein typisches
Beispiel:
on:
push:
branches: [ main, develop ]
paths:
- 'src/**'
- 'tests/**'Dieser Workflow startet nur bei Pushes auf main oder
develop und nur, wenn sich Dateien in src/
oder tests/ geändert haben. Diese Granularität verhindert
unnötige Workflow-Läufe und spart Runner-Zeit.
Zeitbasierte Trigger folgen dem Cron-Format und
starten Workflows nach Zeitplan. Sie eignen sich für regelmäßige
Wartungsaufgaben, Nightly Builds oder Reports. Die Syntax
0 2 * * * bedeutet: jeden Tag um 2 Uhr morgens UTC. Ein
wichtiger Hinweis aus der Praxis: Scheduled Workflows laufen nur auf dem
Default-Branch. Wer in einem Feature-Branch mit Cron-Triggern
experimentiert, wird enttäuscht – erst nach dem Merge werden sie
aktiv.
Manuelle Trigger über workflow_dispatch
ermöglichen es, Workflows on-demand aus der GitHub-UI oder via API zu
starten. Sie können sogar Eingabeparameter definieren, etwa eine
Zielumgebung für ein Deployment. Das macht Workflows zu flexiblen Tools,
die sowohl automatisiert als auch manuell einsetzbar sind.
External Triggers über
repository_dispatch erlauben es, Workflows von außerhalb
GitHubs anzustoßen – etwa von einer CI/CD-Pipeline eines anderen Systems
oder einem Monitoring-Tool. Das Event trägt einen benutzerdefinierten
event_type, über den verschiedene Workflows auf
unterschiedliche externe Signale reagieren können.
Wenn ein Event eintritt, durchläuft GitHub einen präzisen Ablauf, um zu entscheiden, welche Workflows gestartet werden:
Sobald ein Event eintritt – sei es ein Push, ein PR-Comment oder ein Cron-Tick – notiert GitHub den zugehörigen Commit-SHA und Git-Ref. Das ist entscheidend: Der Workflow läuft mit genau der Version der Workflow-Datei, die in diesem Commit existiert. Änderungen an der Workflow-Datei in späteren Commits wirken sich nicht rückwirkend auf bereits gestartete Läufe aus.
GitHub durchsucht nun das .github/workflows/-Verzeichnis
im Repository und prüft alle YAML-Dateien. Für jeden Workflow wird
geprüft: Passt der on:-Trigger zum eingetretenen Event?
Sind eventuelle Filter (Branches, Paths, Types) erfüllt? Wenn ja, wird
ein Workflow-Lauf in die Queue gestellt.
Ein wichtiges Detail: Bei manchen Events – etwa
workflow_dispatch oder repository_dispatch –
muss die Workflow-Datei auf dem Default-Branch existieren, selbst wenn
das Event auf einem anderen Branch ausgelöst wird. Das schützt vor
Missbrauch durch beliebige Feature-Branches.
Jeder Workflow-Lauf ist eine eigenständige Instanz. Mehrere Läufe
desselben Workflows können gleichzeitig aktiv sein, etwa wenn zwei
Entwickler parallel pushen. Die Umgebungsvariablen
GITHUB_SHA und GITHUB_REF halten in jedem Lauf
fest, auf welchen Commit und Branch er sich bezieht. Das ermöglicht es,
im Workflow selbst auf den Kontext zuzugreifen, etwa um einen Build mit
der Commit-Message zu taggen.
Jobs innerhalb eines Workflows verhalten sich standardmäßig wie
nebenläufige Prozesse – sie starten gleichzeitig, sobald der Workflow
ausgelöst wird. Diese Parallelität ist gewollt: Während der
test-Job Unit-Tests ausführt, kann der
lint-Job gleichzeitig die Code-Qualität prüfen. Zeit ist
wertvoll, und GitHub Actions nutzt sie effizient.
Manchmal brauchen wir aber Kontrolle über die Reihenfolge. Ein
Deployment sollte erst starten, wenn Build und Tests erfolgreich waren.
Dafür gibt es das needs:-Keyword, mit dem wir
Abhängigkeiten zwischen Jobs deklarieren:
jobs:
build:
runs-on: ubuntu-latest
steps: [...]
test:
needs: build
runs-on: ubuntu-latest
steps: [...]
deploy:
needs: [build, test]
runs-on: ubuntu-latest
steps: [...]Hier entsteht eine Pipeline: build läuft zuerst, dann
test, und erst wenn beide erfolgreich waren, startet
deploy. Schlägt build fehl, werden
test und deploy gar nicht erst gestartet – das
spart Ressourcen und gibt schnelles Feedback.
Jobs laufen auf Runnern, die GitHub bereitstellt oder die wir selbst
hosten. Die Wahl des Runners über runs-on: hat praktische
Konsequenzen: Ein Linux-Runner startet in Sekunden, ein macOS-Runner
kann länger brauchen. Windows-Systeme haben andere vorinstallierte Tools
als Linux. Matrix-Strategien, die wir später betrachten werden, nutzen
diese Flexibilität, um Code auf mehreren Plattformen gleichzeitig zu
testen.
Steps sind die kleinsten Ausführungseinheiten und das Werkzeug, mit
dem wir konkrete Aufgaben umsetzen. Ein Step kann entweder Shell-Befehle
ausführen oder eine Action aufrufen – oder beides kombinieren. Die
Entscheidung, wann wir ein run: und wann ein
uses: einsetzen, folgt pragmatischen Überlegungen.
Ein run:-Step ist direkt und transparent. Wir sehen im
Workflow-File, was passiert:
- name: Install dependencies
run: npm ciFür einfache Kommandos ist das perfekt. Wird die Logik komplexer – etwa ein mehrzeiliges Setup-Script oder eine Interaktion mit externen APIs – kann der Workflow schnell unübersichtlich werden. Hier kommen Actions ins Spiel.
Eine Action ist eine gekapseltes Stück Funktionalität, das wir per
uses: einbinden. Der Klassiker ist
actions/checkout@v4, der den Repository-Code in den
Workspace kopiert. Der Step wird dadurch zur Deklaration:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0Das with:-Mapping übergibt Parameter an die Action. Was
im Hintergrund passiert – Git-Operationen, Token-Handling, Submodule –
ist abstrahiert. Wir arbeiten auf einer höheren Ebene.
Actions stammen aus drei Quellen: dem offiziellen
actions/-Namespace von GitHub, dem Community-Marketplace
oder aus unserem eigenen Repository. Die Versionierung über Tags oder
SHAs stellt sicher, dass Workflows reproduzierbar bleiben.
@v4 referenziert eine Major-Version, @v4.1.2
eine exakte Version, @abc123def einen konkreten Commit.
Der gemeinsame Workspace aller Steps eines Jobs ist das verbindende Element. Der erste Step checkt Code aus, der zweite baut ihn, der dritte führt Tests aus – jeder arbeitet im selben Verzeichnis. Diese Kontinuität macht Jobs zu kohärenten Einheiten, während die Isolation zwischen Jobs saubere Trennungen erzwingt.
Die Workflow-Anatomie ist keine abstrakte Theorie, sondern ein Werkzeug zum Denken. Wenn wir einen neuen Workflow planen, fragen wir uns:
Diese Fragen strukturieren unsere Arbeit. Ein einfacher CI-Workflow könnte aus einem Job mit fünf Steps bestehen. Ein komplexes CD-System hat vielleicht fünf Jobs mit je zehn Steps und nutzt dutzende Actions. Die Hierarchie skaliert mit der Komplexität.
Ein letzter Punkt, der oft übersehen wird: Workflows sind versioniert wie Code. Jeder Lauf verwendet die Workflow-Definition, die zum Zeitpunkt des Triggers im Repository lag. Das bedeutet auch: Experimente mit Workflows können wir in Feature-Branches durchführen, ohne Produktions-Workflows zu beeinflussen. Erst mit dem Merge gehen Änderungen live. Diese Git-basierte Versionierung macht Workflows wartbar, nachvollziehbar und kollaborativ – wie allen Code, den wir schreiben.