6 Daten im Workflow

Workflows ohne Daten sind wie Programme ohne Variablen – technisch möglich, praktisch nutzlos. Jeder Workflow benötigt Informationen: Wohin deployen? Welchen Branch bauen? Welche Credentials verwenden? GitHub Actions bietet ein durchdachtes, mehrschichtiges System für den Umgang mit Daten. Es unterscheidet zwischen öffentlichen und vertraulichen Informationen, zwischen statischer Konfiguration und dynamischen Laufzeitwerten, zwischen workflow-lokalem und repository-übergreifendem Scope.

Die Herausforderung besteht nicht darin, Daten verfügbar zu machen – GitHub stellt dutzende Variablen automatisch bereit. Die Kunst liegt darin, die richtigen Daten am richtigen Ort auf die richtige Weise zu nutzen. Ein Deployment-Workflow, der hardcodierte Server-URLs enthält, ist wartungsfeindlich. Ein CI-Build, der Secrets in Logs ausgibt, ist ein Sicherheitsrisiko. Ein Job, der nicht auf den aktuellen Branch zugreift, arbeitet womöglich mit veralteten Daten.

Dieses Kapitel behandelt die Mechanismen, mit denen Workflows Informationen handhaben: Umgebungsvariablen für Konfiguration, Kontexte für Laufzeitdaten, Ausdrücke für dynamische Berechnungen und Secrets für sensible Credentials. Wir betrachten nicht nur die Syntax, sondern auch die Designentscheidungen dahinter – wann nutze ich welchen Mechanismus und warum?

6.1 Drei Arten von Variablen

GitHub Actions unterscheidet drei Kategorien von Variablen, die sich in Scope, Lebensdauer und Verwendungszweck unterscheiden. Diese Unterscheidung ist nicht akademisch – sie bestimmt, wie wartbar und sicher unsere Workflows werden.

Default Environment Variables setzt GitHub automatisch bei jedem Workflow-Lauf. Sie existieren als Umgebungsvariablen auf dem Runner und tragen Namen wie GITHUB_SHA, GITHUB_REF oder RUNNER_OS. Diese Variablen beschreiben den Kontext des aktuellen Laufs: Welcher Commit hat ihn ausgelöst? Auf welchem Betriebssystem läuft er? Welcher Actor hat ihn gestartet? Sie sind read-only und konsistent über alle Workflows hinweg – ein verlässliches Fundament.

Environment Variables definieren wir selbst in der Workflow-Datei mit dem env:-Schlüssel. Sie können auf Workflow-, Job- oder Step-Ebene gesetzt werden und folgen einer klaren Scoping-Regel: je spezifischer, desto vorrangig. Eine Variable auf Step-Ebene überschreibt die gleichnamige Variable auf Job-Ebene. Umgebungsvariablen eignen sich für workflow-interne Konfiguration – Build-Flags, temporäre Pfade, berechnete Werte:

env:
  NODE_ENV: production
  BUILD_DIR: ./dist

jobs:
  build:
    env:
      ARTIFACT_NAME: app-${{ github.sha }}
    steps:
      - run: echo "Building in $NODE_ENV mode"
      - run: npm run build
        env:
          VERBOSE: true

Configuration Variables (verfügbar über den vars-Kontext) sind das Enterprise-Feature für größere Setups. Sie werden auf Repository-, Organisations- oder Umgebungsebene in GitHubs UI definiert und sind über mehrere Workflows hinweg nutzbar. Das macht sie ideal für Daten, die sich selten ändern, aber an vielen Stellen gebraucht werden: API-Endpoints, Feature-Flags, Deployment-Targets. Anders als Umgebungsvariablen in der YAML-Datei können sie ohne Code-Änderung aktualisiert werden – ein Ops-Team kann Production-URLs anpassen, ohne Workflows zu editieren.

Variablenart Definition Scope Typischer Einsatz Änderbarkeit
Default Variables Automatisch von GitHub Pro Workflow-Run Metadaten, Kontext Read-only
Environment Variables In YAML (env:) Workflow/Job/Step Build-Config, Flags Per Code-Change
Configuration Variables In GitHub UI Repo/Org/Environment URLs, Features, Ziele Ohne Code-Change

6.2 Das Kontext-System

Während Umgebungsvariablen zur Laufzeit auf dem Runner existieren, sind Kontexte GitHubs Antwort auf die Frage: Wie greife ich vor der Runner-Ausführung auf Daten zu? Ein if-Statement, das einen Job überspringt, wird ausgewertet, bevor der Runner überhaupt gestartet ist. Ein runs-on, das sich dynamisch entscheidet, braucht Informationen, bevor die Maschine allokiert wird. Kontexte sind das Werkzeug für diese Szenarien.

Ein Kontext ist ein JSON-artiges Objekt, das über die Syntax ${{ context.property }} zugänglich wird. GitHub bietet verschiedene Kontexte für verschiedene Informationstypen. Das Timing ihrer Verfügbarkeit ist entscheidend.

6.2.1 Der github-Kontext: Workflow-Metadaten

Der github-Kontext ist die umfassendste Datenquelle. Er enthält alles, was GitHub über den aktuellen Workflow-Lauf weiß: von welchem Event er ausgelöst wurde (github.event_name), welcher Commit involviert ist (github.sha), auf welchem Branch wir operieren (github.ref_name), wer ihn gestartet hat (github.actor). Ein Blick in die Struktur zeigt die Breite:

- name: Debug workflow context
  run: |
    echo "Event: ${{ github.event_name }}"
    echo "Branch: ${{ github.ref_name }}"
    echo "Actor: ${{ github.actor }}"
    echo "SHA: ${{ github.sha }}"

Besonders wertvoll ist github.event – ein Objekt, das die komplette Webhook-Payload des auslösenden Events enthält. Bei einem pull_request-Trigger enthält es Details über PR-Nummer, Titel, Labels, und den kompletten Diff-Kontext. Bei push sind es Commit-Messages und geänderte Dateien. Diese Granularität erlaubt hochspezifische Workflow-Logik: “Deploye nur, wenn das Label ‘deploy-ready’ gesetzt ist” oder “Sende Slack-Notification mit der Commit-Message des Pushes”.

Ein praktischer Hinweis: github.ref vs. github.ref_name. Ersteres liefert den vollqualifizierten Ref (refs/heads/main), letzteres nur den Namen (main). Für die meisten Zwecke – etwa um Branch-Namen in Artefakt-Pfade einzubauen – ist ref_name das, was wir wollen.

6.2.2 Der env-Kontext: Umgebungsvariablen im Ausdruck

Der env-Kontext macht unsere selbst definierten Umgebungsvariablen in Ausdrücken zugänglich. Der Unterschied zur direkten Shell-Variable ist subtil, aber wichtig: ${{ env.MY_VAR }} wird von GitHub vor der Übergabe an den Runner ausgewertet, $MY_VAR wird im Shell-Kontext des Runners ausgewertet.

env:
  DEPLOY_ENV: staging
  
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        if: ${{ env.DEPLOY_ENV == 'production' }}
        run: ./deploy.sh

Die if-Bedingung wird verarbeitet, bevor der Step überhaupt zum Runner geschickt wird. Das spart Ressourcen – übersprungene Steps belegen keine Runner-Zeit.

Der env-Kontext folgt dem Scoping: In einem Step sieht man workflow-weite, job-weite und step-weite Variablen. Die innerste Definition gewinnt. Das erlaubt elegante Overrides für Spezialfälle.

6.2.3 Der job-Kontext: Laufzeitinformationen

Sobald ein Job auf einem Runner startet, wird der job-Kontext aktiv. Er enthält Laufzeitdetails wie die Container-ID (falls der Job in einem Container läuft), Service-Container-Ports (für Datenbanken oder Redis) und den Job-Status. Besonders nützlich bei Service-Containern:

services:
  postgres:
    image: postgres:14
    ports:
      - 5432

steps:
  - name: Run tests
    run: |
      npm test -- --db-port=${{ job.services.postgres.ports[5432] }}

GitHub mappt Port 5432 des Containers auf einen zufälligen Host-Port. Der job-Kontext verrät uns, welcher. Ohne diese Information könnten Tests nicht auf die Datenbank zugreifen.

6.2.4 Der steps-Kontext: Daten zwischen Steps teilen

Während Jobs isoliert sind und nicht direkt Daten austauschen, können Steps innerhalb eines Jobs über den steps-Kontext kommunizieren. Ein Step kann Outputs definieren, die nachfolgende Steps lesen:

steps:
  - name: Generate version
    id: versioning
    run: echo "version=1.2.${{ github.run_number }}" >> $GITHUB_OUTPUT
    
  - name: Build with version
    run: |
      echo "Building version ${{ steps.versioning.outputs.version }}"
      docker build -t app:${{ steps.versioning.outputs.version }} .

Die Mechanik: Ein Step schreibt key=value-Paare in die Datei $GITHUB_OUTPUT. GitHub parsed diese Datei und macht die Werte im steps.<id>.outputs-Kontext verfügbar. Wichtig ist das id:-Attribut – ohne ID ist der Step nicht referenzierbar.

Der steps-Kontext enthält auch outcome und conclusion – den Erfolg oder Misserfolg vorheriger Steps. Der Unterschied: outcome ist das unmittelbare Ergebnis, conclusion berücksichtigt continue-on-error. Wenn ein Step fehlschlägt, aber continue-on-error: true hat, ist sein outcome “failure”, aber seine conclusion “success”.

6.2.5 Der needs-Kontext: Job-Abhängigkeiten nutzen

Jobs sind isoliert, aber nicht inkommunikativ. Über den needs-Kontext kann ein Job die Outputs abhängiger Jobs lesen:

jobs:
  build:
    outputs:
      artifact_url: ${{ steps.upload.outputs.url }}
    steps:
      - name: Upload
        id: upload
        run: echo "url=https://artifacts.example.com/build-${{ github.sha }}" >> $GITHUB_OUTPUT
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Download artifact
        run: curl -O ${{ needs.build.outputs.artifact_url }}

Diese Pattern ist fundamental für Pipeline-Architekturen: Build erstellt ein Artefakt, Test prüft es, Deploy veröffentlicht es. Ohne needs und Outputs müssten wir Artefakte über externe Speicher koordinieren – umständlich und fehleranfällig.

6.3 Ausdrücke: Daten dynamisch verarbeiten

Die ${{ }}-Syntax ist mehr als nur Variablen-Interpolation. Sie definiert einen eigenen Auswertungsraum mit Literalen, Operatoren, Funktionen und Kontexten. Ausdrücke machen Workflows programmatisch.

steps:
  - name: Conditional step
    if: ${{ github.ref == 'refs/heads/main' && github.event_name == 'push' }}
    run: echo "Main branch push detected"

Die if-Bedingung kombiniert zwei Kontextwerte mit logischen Operatoren. GitHub wertet den Ausdruck zu einem Boolean aus. Nur wenn true, wird der Step ausgeführt.

Ein Sonderfall: In if-Klauseln können die ${{ }}-Wrapper weggelassen werden – GitHub erkennt den Kontext. if: github.ref == 'refs/heads/main' ist identisch zur expliziten Form. Das ist Syntax-Zucker, aber bei langen Bedingungen hilfreich.

6.3.1 Operatoren und Funktionen

Ausdrücke unterstützen die üblichen Verdächtigen: Vergleichsoperatoren (==, !=, <, >), logische Operatoren (&&, ||, !), und arithmetische Operatoren (+, -, *, /). Dazu kommen spezielle Funktionen:

Funktion Zweck Beispiel
contains() Substring/Array-Check contains(github.ref, 'feature')
startsWith() Präfix-Check startsWith(github.ref, 'refs/tags/')
endsWith() Suffix-Check endsWith(matrix.os, 'latest')
format() String-Formatierung format('v{0}.{1}', major, minor)
join() Array zu String join(matrix.node, ', ')
toJSON() Kontext zu JSON toJSON(github)
fromJSON() JSON zu Objekt fromJSON(steps.meta.outputs.json)
hashFiles() Datei-Hash für Caching hashFiles('**/package-lock.json')

Die contains()-Funktion ist ein Arbeitspferd für Branch- und Label-Checks. startsWith() und endsWith() eignen sich für Präfix/Suffix-Logik, etwa “führe aus für alle Tags, die mit ‘release-’ beginnen”.

Besonders mächtig ist fromJSON(): Es erlaubt, dynamische Konfiguration aus JSON-Strings zu bauen. Ein Step kann eine Matrix-Konfiguration berechnen und als JSON-Output ausgeben, ein anderer Job konsumiert sie via fromJSON() als Matrix-Strategie. Das ermöglicht Meta-Programmierung auf Workflow-Ebene.

6.3.2 Status-Funktionen

Vier spezielle Funktionen prüfen den Workflow-Status:

steps:
  - name: Always run
    if: always()
    run: echo "I run even if previous steps failed"
    
  - name: On success
    if: success()
    run: echo "All previous steps succeeded"
    
  - name: On failure
    if: failure()
    run: echo "A previous step failed"
    
  - name: If cancelled
    if: cancelled()
    run: echo "Workflow was cancelled"

Diese Funktionen verändern das Default-Verhalten: Normalerweise überspringt GitHub nachfolgende Steps, wenn ein Step fehlschlägt. Mit if: always() erzwingen wir Ausführung – nützlich für Cleanup-Tasks oder Benachrichtigungen. Mit if: failure() können wir auf Fehler reagieren, etwa Logs hochladen oder Alerts senden.

6.4 Secrets: Vertrauliche Daten schützen

Credentials, API-Keys, Tokens – sensible Daten gehören nicht in Workflow-Dateien. GitHub bietet Secrets als verschlüsselte Speichermechanismus auf Repository-, Organisations- und Umgebungsebene. Secrets sind write-only in der UI: Man kann sie setzen, aber nicht auslesen. Im Workflow sind sie über den secrets-Kontext verfügbar.

steps:
  - name: Deploy
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
    run: |
      aws s3 sync ./dist s3://my-bucket

GitHub redacted Secrets automatisch in Logs. Wenn ein Secret-Wert in einem Log auftaucht – ob durch echo, console.log oder Fehler-Output – ersetzt GitHub ihn durch ***. Das ist ein Sicherheitsnetz, aber kein Freifahrtschein: Strukturierte Daten (JSON mit Secrets) oder Base64-kodierte Secrets können durch das Raster fallen. Best Practice: Secrets explizit nur dort übergeben, wo sie gebraucht werden, nicht breit streuen.

6.4.1 Secret-Scoping und Precedence

Secrets folgen demselben Precedence-Modell wie Konfigurationsvariablen: Umgebungs-Secrets überschreiben Repository-Secrets, diese wiederum Organisations-Secrets. Das erlaubt Default-Credentials auf Org-Ebene mit Overrides für spezielle Repos oder Environments.

Ein wichtiger Unterschied zu Variablen: Environment-Level Secrets werden erst gelesen, wenn der Job startet, nicht bei Workflow-Queue-Zeit. Das hat Konsequenzen für if-Bedingungen auf Job-Ebene – sie können keine Environment-Secrets referenzieren, da diese noch nicht verfügbar sind.

jobs:
  deploy:
    environment: production
    steps:
      - name: Use production secret
        run: echo "Deploying with ${{ secrets.PROD_API_KEY }}"
        # Das funktioniert - Job läuft bereits
  
  check:
    if: ${{ secrets.PROD_API_KEY != '' }}  # Das funktioniert NICHT
    # Job-Level if kann keine Environment-Secrets sehen

6.4.2 Das GITHUB_TOKEN-Secret

Jeder Workflow bekommt automatisch ein GITHUB_TOKEN-Secret. Dieses Token authentifiziert den Workflow gegenüber der GitHub API und erlaubt Operationen wie Kommentare auf Issues, Releases erstellen, Packages publishen. Die Permissions sind standardmäßig liberal, können aber per permissions:-Schlüssel eingeschränkt werden:

permissions:
  contents: read
  issues: write
  pull-requests: write

jobs:
  comment:
    runs-on: ubuntu-latest
    steps:
      - name: Comment on PR
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          gh pr comment ${{ github.event.number }} --body "Build completed!"

Das Token ist kurzlebig (Lebensdauer des Workflow-Runs) und scope-limitiert (nur auf das auslösende Repository, es sei denn, man konfiguriert anders). Für Operationen außerhalb des Repos – etwa Cross-Repo-Triggers oder Package-Registry-Zugriffe in anderen Orgs – braucht es PATs (Personal Access Tokens), die als reguläre Secrets hinterlegt werden.

6.5 Naming Conventions und Limits

GitHub erzwingt Regeln für Variablen- und Secret-Namen. Sie mögen pedantisch wirken, dienen aber der Konsistenz und Kompatibilität.

Für alle Variablen und Secrets:

Size Limits:

Diese Limits sind großzügig für normale Nutzung, können aber bei exzessiver Verwendung von JSON-Configs oder Multi-Megabyte-Secrets zum Problem werden. Für große Datenmengen – etwa ML-Modell-Weights oder große Config-Files – ist ein externer Speicher (S3, Artifact Storage) der bessere Weg.

6.6 Praktische Muster

Environment-Matrix aus JSON-Output: Ein häufiges Pattern für dynamische Workflows – ein Job berechnet, gegen welche Environments deployed werden soll, und gibt das als JSON-Array aus. Der Deploy-Job nutzt es als Matrix:

jobs:
  determine-targets:
    runs-on: ubuntu-latest
    outputs:
      matrix: ${{ steps.set-matrix.outputs.matrix }}
    steps:
      - id: set-matrix
        run: |
          if [ "${{ github.ref_name }}" == "main" ]; then
            echo 'matrix=["staging","production"]' >> $GITHUB_OUTPUT
          else
            echo 'matrix=["development"]' >> $GITHUB_OUTPUT
          fi
  
  deploy:
    needs: determine-targets
    strategy:
      matrix:
        environment: ${{ fromJSON(needs.determine-targets.outputs.matrix) }}
    runs-on: ubuntu-latest
    environment: ${{ matrix.environment }}
    steps:
      - name: Deploy to ${{ matrix.environment }}
        run: echo "Deploying..."

Composite Secrets für Multi-Value-Credentials: AWS braucht Access Key ID und Secret Key. Statt zwei separate Secrets in jedem Step zu übergeben, definieren wir sie einmal im Job-env:

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_DEFAULT_REGION: us-east-1
    steps:
      - name: Upload to S3
        run: aws s3 sync ./dist s3://bucket
      - name: Invalidate CloudFront
        run: aws cloudfront create-invalidation --distribution-id ${{ vars.CF_DIST_ID }}

Alle Steps erben die AWS-Credentials, ohne sie explizit zu deklarieren. Das reduziert Boilerplate und Fehlerquellen.

Version-Strings aus Context-Daten: Semantische Versionen lassen sich aus Workflow-Metadaten konstruieren:

steps:
  - name: Generate version
    id: version
    run: |
      if [[ "${{ github.ref }}" =~ ^refs/tags/v(.*)$ ]]; then
        echo "version=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT
      else
        echo "version=0.0.0-dev.${{ github.run_number }}" >> $GITHUB_OUTPUT
      fi
  
  - name: Build with version
    run: |
      echo "Building version ${{ steps.version.outputs.version }}"

Für Tags nutzen wir die Tag-Version, für Development-Builds eine synthetische Version aus der Run-Number. Das hält Versionen eindeutig und nachvollziehbar.

6.7 Anti-Patterns vermeiden

Secrets in Logs debuggen: Der Klassiker – ein verzweifelter echo ${{ secrets.MY_SECRET }}, um zu prüfen, ob das Secret korrekt ist. GitHub redacted es im Log, aber das Muster ist gefährlich. Besser: Prüfen, ob das Secret existiert (nicht-leer ist), nicht seinen Wert ausgeben.

# Schlecht:
- run: echo "Secret is: ${{ secrets.MY_SECRET }}"

# Gut:
- name: Verify secret exists
  if: ${{ secrets.MY_SECRET == '' }}
  run: |
    echo "ERROR: MY_SECRET is not set"
    exit 1

Kontext-Missbrauch in Shell-Variablen: Ein subtiler Fehler – Context-Properties in Strings einbetten, die dann in der Shell evaluiert werden:

# Unsicher - Command Injection möglich:
- run: echo "Processing ${{ github.event.head_commit.message }}"

# Sicher - via env-Variable übergeben:
- env:
    COMMIT_MSG: ${{ github.event.head_commit.message }}
  run: echo "Processing $COMMIT_MSG"

Im ersten Beispiel wird die Commit-Message direkt in den Shell-Befehl interpoliert. Enthält sie Shell-Metacharacters (;, |, $()), können sie ausgeführt werden. Im zweiten Beispiel landet sie als Umgebungsvariable im Shell-Environment – sicher vor Injection.

Overengineering mit Ausdrücken: Komplexe Logik gehört in Scripts, nicht in YAML-Ausdrücke. Wenn eine if-Bedingung drei Zeilen lang wird und fünf Operatoren verknüpft, ist es Zeit für ein dediziertes Script:

# Überkompliziert:
- if: |
    github.ref == 'refs/heads/main' && 
    (github.event_name == 'push' || github.event_name == 'workflow_dispatch') &&
    !contains(github.event.head_commit.message, '[skip ci]') &&
    success()

# Besser:
- name: Check deployment conditions
  id: check
  run: |
    # Logik in Shell mit Kommentaren und Debuggability
    if [ "${{ github.ref }}" == "refs/heads/main" ]; then
      # ... komplexe Prüflogik ...
      echo "should_deploy=true" >> $GITHUB_OUTPUT
    fi

- if: steps.check.outputs.should_deploy == 'true'

Lesbarkeit und Wartbarkeit schlagen kompakte YAML-Ausdrücke. Komplexe Bedingungen profitieren von expliziter Programmlogik mit Kommentaren, Logging und Testbarkeit – alles Dinge, die in YAML-Expressions fehlen.