11 Deployment-Konzepte

Der Build ist grün, die Tests laufen durch – aber der Weg in die Production ist mehr als ein git push. Production-Deployments erfordern kontrollierte Freigabeprozesse, Vier-Augen-Prinzip, zeitliche Trennung zwischen Stages und die Fähigkeit, im Notfall schnell zurückzurollen. GitHub Actions bietet mit Environments, Protection Rules und OIDC ein ausgereiftes Framework für sichere Deployments. Dieses Kapitel zeigt, wie man es nutzt – ohne die Agilität zu opfern.

11.1 Environments: Logische Deployment-Targets

11.1.1 Das Konzept

Ein Environment ist eine logische Repräsentation eines Deployment-Ziels – typischerweise development, staging oder production. Jedes Environment kann eigene Secrets, Variables und Protection Rules haben. Ein Job referenziert ein Environment, und GitHub erzwingt alle konfigurierten Regeln, bevor der Job läuft.

Die Definition eines Environments passiert nicht im Workflow-Code, sondern in den Repository-Settings unter Settings → Environments. Das ist bewusst: Deployment-Policies sollten nicht von Entwicklern änderbar sein, sondern von Admins kontrolliert werden.

jobs:
  deploy-production:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://app.example.com
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to production
        run: ./deploy.sh
        env:
          API_KEY: ${{ secrets.PROD_API_KEY }}

In diesem Beispiel referenziert der Job das Environment production. Wenn dieses Environment Protection Rules hat (z.B. Required Reviewers), pausiert der Job und wartet auf Approval. Die url ist optional, aber nützlich: Sie erscheint in der Deployment-History und ermöglicht direktes Anspringen der deployed Applikation.

11.1.2 Environment-URLs: Statisch und dynamisch

Die URL kann statisch sein oder dynamisch aus dem Workflow berechnet werden:

environment:
  name: staging
  url: https://staging-pr-${{ github.event.pull_request.number }}.example.com

Für Preview-Deployments bei Pull Requests ist das unverzichtbar. Jeder PR bekommt seine eigene URL, die in der GitHub-UI verlinkt wird.

11.2 Protection Rules: Das Sicherheitsnetz

11.2.1 Required Reviewers

Die mächtigste Protection Rule: Required Reviewers. Man definiert bis zu 6 Personen oder Teams, von denen mindestens eine Person den Deployment approven muss.

Konfiguration (UI):

  1. Settings → Environments → production
  2. Required reviewers aktivieren
  3. User/Teams hinzufügen (z.B. @platform-team, @release-manager)
  4. Optional: Prevent self-review aktivieren

Die “Prevent self-review”-Option verhindert, dass der Workflow-Auslöser selbst approven kann. Das ist essenziell für Compliance: Ein Entwickler kann nicht seinen eigenen Code in Production deployen.

Der Approval-Flow im Workflow:

jobs:
  deploy-prod:
    environment: production
    runs-on: ubuntu-latest
    steps:
      - name: This step waits for approval
        run: echo "Deployment starts after approval"

Wenn dieser Job läuft, zeigt GitHub eine Approval-Anfrage in der Actions-UI. Reviewer erhalten eine Notification (je nach Settings) und können über einen Button approven oder rejecten. Bei Approval läuft der Job weiter, bei Reject wird er abgebrochen.

11.2.2 Wait Timer

Manchmal will man eine künstliche Verzögerung – etwa um Monitoring-Daten zu sammeln oder einen zeitlichen Abstand zwischen Staging und Production zu erzwingen.

Konfiguration (UI):

  1. Settings → Environments → production
  2. Wait timer aktivieren
  3. Minuten eingeben (z.B. 10)

Der Job wartet 10 Minuten, bevor er startet – unabhängig von Approvals. Man kann Wait Timer und Required Reviewers kombinieren: Der Job wartet erst 10 Minuten, dann muss er approved werden.

Sinnvolle Anwendung:

11.2.3 Deployment Branch Policies

Nicht jeder Branch soll in Production deployen dürfen. Branch Policies definieren, welche Branches/Tags ein Environment targeten dürfen.

Konfiguration (UI):

  1. Settings → Environments → production
  2. Deployment branches and tags → “Selected branches and tags”
  3. Pattern hinzufügen: main, release/*, v*.*.*

Mit diesen Patterns kann nur der main-Branch, Release-Branches (release/1.0) und Version-Tags (v1.2.3) nach Production deployen. Feature-Branches sind blockiert.

Das Pattern-Matching folgt den üblichen Glob-Regeln:

11.2.4 Custom Deployment Protection Rules

Fortgeschrittene Organisationen können eigene Protection Rules als GitHub Apps bauen. Beispiele:

Diese Custom Rules erscheinen dann als Option in den Environment-Settings und werden von GitHub automatisch evaluiert.

11.3 Environment Secrets und Variables

11.3.1 Der Unterschied zu Repository Secrets

Environment Secrets sind nur für Jobs verfügbar, die das entsprechende Environment referenzieren. Das ist wichtig für Separation:

jobs:
  deploy-dev:
    environment: development
    steps:
      - run: echo ${{ secrets.API_KEY }}  # DEV_API_KEY
  
  deploy-prod:
    environment: production
    steps:
      - run: echo ${{ secrets.API_KEY }}  # PROD_API_KEY

Beide Jobs verwenden secrets.API_KEY, aber GitHub liefert unterschiedliche Werte je nach Environment. Man muss nicht secrets.DEV_API_KEY vs. secrets.PROD_API_KEY unterscheiden – das Environment ist der Scope.

Wichtig: Environment Secrets sind erst zugänglich, nachdem alle Protection Rules erfüllt sind. Wenn ein Job auf Approval wartet, hat er noch keinen Zugriff auf die Secrets. Das verhindert, dass unapproved Code Secrets extrahieren kann.

11.3.2 Environment Variables für Non-Secret-Config

Nicht alles ist ein Secret. Für Environment-spezifische Config, die nicht vertraulich ist, gibt es Environment Variables:

jobs:
  deploy:
    environment: production
    steps:
      - run: |
          echo "Region: ${{ vars.AWS_REGION }}"
          echo "Instance Type: ${{ vars.INSTANCE_TYPE }}"

Variables sind über den vars-Kontext zugänglich. Der Vorteil gegenüber Secrets: Sie sind in der UI sichtbar (Secrets sind immer redacted) und können ohne Admin-Rechte gelesen werden.

Feature Repository Secret Environment Secret Environment Variable
Scope Alle Jobs Nur Environment-Jobs Nur Environment-Jobs
Sichtbarkeit Redacted Redacted Plain Text
Zugriff nach Approval Sofort Erst nach Approval Erst nach Approval
Verwendung secrets.* secrets.* vars.*

11.4 Concurrency für serielle Deployments

11.4.1 Das Problem

Standardmäßig laufen Workflows parallel. Wenn zwei Commits schnell hintereinander gepusht werden, könnten beide in Production deployen – gleichzeitig. Das führt zu Race Conditions: Welche Version ist jetzt deployed?

11.4.2 Die Lösung: Deployment Concurrency

Mit concurrency auf Job-Ebene erzwingt man, dass maximal ein Deployment gleichzeitig läuft:

jobs:
  deploy-production:
    environment: production
    runs-on: ubuntu-latest
    concurrency:
      group: production-deployment
      cancel-in-progress: false
    steps:
      - run: ./deploy.sh

Das cancel-in-progress: false ist hier entscheidend. Bei Deployments will man normalerweise nicht laufende Deployments abbrechen, sondern in eine Queue stellen. Commit A deployt, Commit B wartet. Wenn A fertig ist, startet B.

11.4.3 Environment-spezifische Concurrency Groups

Für Multi-Environment-Setups braucht man pro Environment eine eigene Concurrency Group:

jobs:
  deploy-staging:
    environment: staging
    concurrency:
      group: deploy-staging
      cancel-in-progress: false
    steps:
      - run: ./deploy.sh staging
  
  deploy-production:
    environment: production
    needs: deploy-staging
    concurrency:
      group: deploy-production
      cancel-in-progress: false
    steps:
      - run: ./deploy.sh production

Staging und Production können parallel deployen (verschiedene Groups), aber innerhalb jedes Environments ist es seriell. Das ist das gewünschte Verhalten.

11.4.4 Cancel-in-Progress für Non-Production

Für Development/Staging macht cancel-in-progress: true oft Sinn:

jobs:
  deploy-dev:
    environment: development
    concurrency:
      group: deploy-dev
      cancel-in-progress: true
    steps:
      - run: ./deploy.sh dev

Wenn Commit B gepusht wird, während Commit A noch deployed, wird A abgebrochen und B startet. In Dev-Environments will man typischerweise die neueste Version, nicht alle Versionen der Reihe nach.

11.5 OIDC: Credential-freie Cloud-Authentifizierung

11.5.1 Das Problem mit Long-Lived Credentials

Traditionell speichert man Cloud-Credentials (AWS Access Keys, Azure Service Principal, GCP Service Account Keys) als Secrets in GitHub. Das hat Nachteile:

11.5.2 Die Lösung: OpenID Connect (OIDC)

OIDC erlaubt, dass GitHub Actions temporäre, kurzlebige Tokens direkt vom Cloud-Provider anfordert – ohne Secrets in GitHub zu speichern.

Das Flow:

  1. Workflow läuft, Job braucht Cloud-Zugriff
  2. GitHub erstellt ein OIDC-Token (JWT) mit Workflow-Identität
  3. Workflow sendet Token an Cloud-Provider
  4. Cloud-Provider validiert Token gegen Trust-Policy
  5. Cloud-Provider gibt temporäres Access Token (gültig für Job-Dauer)
  6. Workflow verwendet Access Token für Cloud-API-Calls

11.5.3 OIDC in der Praxis: AWS

Setup (einmalig):

  1. In AWS: IAM Identity Provider für GitHub erstellen

  2. IAM Role erstellen mit Trust Policy:

    {
      "Version": "2012-10-17",
      "Statement": [{
        "Effect": "Allow",
        "Principal": {
          "Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com"
        },
        "Action": "sts:AssumeRoleWithWebIdentity",
        "Condition": {
          "StringEquals": {
            "token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:environment:production"
          }
        }
      }]
    }

Diese Policy erlaubt nur dem production-Environment in myorg/myrepo, die Role zu assumen. Andere Workflows sind blockiert.

Workflow:

jobs:
  deploy-aws:
    environment: production
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # Benötigt für OIDC-Token-Request
      contents: read
    steps:
      - uses: actions/checkout@v4
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: eu-central-1
      
      - name: Deploy to S3
        run: aws s3 sync ./dist s3://my-bucket/

Das permissions.id-token: write ist der Trigger: GitHub generiert ein OIDC-Token für diesen Job. Die configure-aws-credentials Action tauscht es gegen AWS-Credentials.

Wichtig: Keine AWS Access Keys im Workflow oder in Secrets. Alles läuft über kurzlebige Tokens.

11.5.4 OIDC Token Claims

Das OIDC-Token enthält Claims, die die Workflow-Identität beschreiben:

{
  "sub": "repo:octo-org/octo-repo:environment:prod",
  "repository": "octo-org/octo-repo",
  "environment": "prod",
  "ref": "refs/heads/main",
  "actor": "octocat",
  "workflow": "deploy.yml",
  "job_workflow_ref": "octo-org/octo-repo/.github/workflows/deploy.yml@refs/heads/main"
}

Der sub (Subject) ist der wichtigste Claim. Er definiert die Identität, die der Cloud-Provider gegen seine Trust Policy prüft. Man kann granular steuern:

Das ist erheblich feingranularer als ein statischer Access Key.

11.6 Multi-Environment-Workflows

11.6.1 Das klassische Pattern: Dev → Staging → Production

Die gängigste Deployment-Pipeline führt Code durch mehrere Stages:

name: Progressive Deployment

on:
  push:
    branches: [main]

jobs:
  deploy-dev:
    environment: development
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-dev
      cancel-in-progress: true
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Dev
        run: ./deploy.sh dev
        env:
          API_KEY: ${{ secrets.API_KEY }}
  
  deploy-staging:
    needs: deploy-dev
    environment: staging
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-staging
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Staging
        run: ./deploy.sh staging
        env:
          API_KEY: ${{ secrets.API_KEY }}
  
  deploy-production:
    needs: deploy-staging
    environment: production
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-production
      cancel-in-progress: false
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to Production
        run: ./deploy.sh production
        env:
          API_KEY: ${{ secrets.API_KEY }}

Flow:

  1. Push nach main triggert Workflow
  2. Dev-Deployment läuft sofort (keine Protection Rules)
  3. Staging-Deployment wartet auf Dev (via needs)
  4. Production-Deployment wartet auf Staging + Required Reviewers Approval + Wait Timer

Wenn Dev oder Staging fehlschlagen, stoppt die Pipeline automatisch. Production-Deployment passiert nur, wenn alle vorherigen Stages erfolgreich waren.

11.6.2 Conditional Production Deployment

Nicht jeder Commit soll in Production. Oft will man nur bei Tags oder nach manueller Bestätigung deployen:

jobs:
  deploy-production:
    if: startsWith(github.ref, 'refs/tags/v')
    environment: production
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh production

Dieses Deployment läuft nur, wenn ein Tag mit v gepusht wird (z.B. v1.2.3). Normale Commits gehen bis Staging, Production ist Tag-basiert.

Alternativ: Workflow mit workflow_dispatch manuell triggerbar machen:

on:
  workflow_dispatch:
    inputs:
      environment:
        description: 'Target environment'
        required: true
        type: choice
        options:
          - staging
          - production

jobs:
  deploy:
    environment: ${{ inputs.environment }}
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh ${{ inputs.environment }}

Der Workflow kann nur manuell gestartet werden, und der User wählt das Environment. Das ist sicherer als automatische Production-Deployments.

11.6.3 Matrix-Deployments für Multi-Region

Bei Multi-Region-Deployments hilft eine Matrix:

jobs:
  deploy-production:
    environment: production
    strategy:
      matrix:
        region: [us-east-1, eu-central-1, ap-southeast-1]
      max-parallel: 1  # Ein Region nach dem anderen
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to ${{ matrix.region }}
        run: ./deploy.sh production ${{ matrix.region }}

Das max-parallel: 1 ist wichtig: Es deployt erst us-east-1, dann eu-central-1, dann ap-southeast-1. Bei Fehler in der ersten Region wird nicht weiter deployed.

Ohne max-parallel: 1 würden alle drei parallel deployen – das ist riskant bei Breaking Changes.

11.7 Deployment Tracking und History

11.7.1 Die Deployments-UI

GitHub trackt alle Deployments automatisch. Unter Actions → Deployments (oder direkt über die rechte Sidebar) sieht man:

Diese UI ist unverzichtbar für Incident Response: “Welche Version ist gerade in Production?” – ein Blick auf Deployments zeigt es.

11.7.2 Deployment Status API

GitHub erstellt für jeden Deployment-Job ein Deployment-Objekt über die API. Das kann man nutzen für:

Webhook-Event: deployment und deployment_status

11.7.3 Rollback-Strategien

Rollback ist kein First-Class-Feature in GitHub Actions, aber machbar über:

1. Re-Deploy des letzten stabilen Tags:

on:
  workflow_dispatch:
    inputs:
      tag:
        description: 'Tag to deploy (for rollback)'
        required: true

jobs:
  rollback:
    environment: production
    steps:
      - uses: actions/checkout@v4
        with:
          ref: ${{ inputs.tag }}
      - run: ./deploy.sh production

2. Deployment-History nutzen:
In der Deployments-UI kann man sehen, welcher Commit zuletzt deployed war. Diesen Commit in einem neuen Workflow re-deployen.

3. Feature Flags:
Statt Code-Rollback ein Feature Flag toggeln. Das ist schneller und weniger riskant als ein komplettes Re-Deployment.

11.8 Praktische Patterns und Anti-Patterns

11.8.1 Pattern 1: Approval + Automated Smoke Tests

Kombination aus manueller Approval und automatischen Post-Deployment-Tests:

jobs:
  deploy-production:
    environment: production
    runs-on: ubuntu-latest
    steps:
      - name: Deploy
        run: ./deploy.sh
  
  smoke-tests:
    needs: deploy-production
    runs-on: ubuntu-latest
    steps:
      - name: Run smoke tests
        run: ./smoke-tests.sh
      
      - name: Notify on failure
        if: failure()
        run: |
          curl -X POST $SLACK_WEBHOOK \
            -d '{"text": "🚨 Production smoke tests failed!"}'

Der Reviewer approved den Deployment-Start, aber die Smoke Tests laufen automatisch danach. Bei Failure wird alarmiert.

11.8.2 Pattern 2: Canary mit Wait Timer

jobs:
  deploy-canary:
    environment: production-canary
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy-canary.sh  # 10% Traffic
  
  wait-and-observe:
    needs: deploy-canary
    runs-on: ubuntu-latest
    steps:
      - run: sleep 300  # 5 Minuten warten
      - name: Check error rate
        run: ./check-errors.sh
  
  deploy-full:
    needs: wait-and-observe
    environment: production
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy-full.sh  # 100% Traffic

Canary-Deployment mit automatischem Monitoring-Check. Nur wenn Error-Rate okay, geht’s weiter zu Full-Deployment.

11.8.3 Anti-Pattern 1: Production ohne Protection Rules

# ❌ SCHLECHT
jobs:
  deploy-production:
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh production

Kein Environment, keine Protection Rules – jeder Commit deployt direkt nach Production. Das ist ein Rezept für Desaster.

Lösung: Immer ein environment: production mit mindestens Required Reviewers oder Branch Policy.

11.8.4 Anti-Pattern 2: Secrets im Workflow-Code

# ❌ SCHLECHT
jobs:
  deploy:
    steps:
      - run: |
          export API_KEY="sk-prod-abc123..."
          ./deploy.sh

Secrets gehören in GitHub Secrets oder besser: OIDC. Niemals hardcoded im Workflow.

11.8.5 Anti-Pattern 3: Parallel Production Deployments

# ❌ PROBLEMATISCH
jobs:
  deploy-region-1:
    environment: production
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh us-east-1
  
  deploy-region-2:
    environment: production
    runs-on: ubuntu-latest
    steps:
      - run: ./deploy.sh eu-central-1

Ohne Concurrency-Control oder Matrix können beide gleichzeitig fehlschlagen. Besser: Matrix mit max-parallel: 1 oder sequential Jobs mit needs.

11.9 Ein vollständiges Beispiel

Zum Abschluss ein produktionsreifes Deployment-Setup, das alle Konzepte vereint:

name: Production Deployment

on:
  push:
    tags:
      - 'v*.*.*'
  workflow_dispatch:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Build and test
        run: |
          npm ci
          npm run build
          npm test
      - uses: actions/upload-artifact@v4
        with:
          name: build-artifact
          path: dist/
  
  deploy-staging:
    needs: build
    environment: staging
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-staging
      cancel-in-progress: false
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-artifact
          path: dist/
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/StagingDeployRole
          aws-region: eu-central-1
      
      - name: Deploy to Staging
        run: aws s3 sync ./dist s3://staging-bucket/
      
      - name: Run smoke tests
        run: ./smoke-tests.sh https://staging.example.com
  
  deploy-production:
    needs: deploy-staging
    environment:
      name: production
      url: https://app.example.com
    runs-on: ubuntu-latest
    concurrency:
      group: deploy-production
      cancel-in-progress: false
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: build-artifact
          path: dist/
      
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/ProductionDeployRole
          aws-region: eu-central-1
      
      - name: Deploy to Production
        run: aws s3 sync ./dist s3://production-bucket/
      
      - name: Invalidate CloudFront
        run: |
          aws cloudfront create-invalidation \
            --distribution-id E1234567890ABC \
            --paths "/*"
      
      - name: Post-deployment verification
        run: ./verify-deployment.sh https://app.example.com

Was macht dieses Setup richtig?

  1. Trigger-Control: Nur bei Version-Tags (plus manueller Trigger als Fallback)
  2. Artifact-Passing: Build einmal, deploy mehrfach – immutable Artifact
  3. Staging-Gate: Production deployt nur, wenn Staging erfolgreich
  4. Environment-Protection: production-Environment hat in Settings Required Reviewers + Wait Timer + Branch Policy
  5. OIDC: Keine AWS Access Keys als Secrets, temporäre Tokens via Role Assumption
  6. Concurrency: Serielle Deployments pro Environment
  7. Observability: Environment URL verlinkt, Smoke Tests + Verification
  8. Audit Trail: Jedes Deployment ist in der History, mit Commit, Timestamp, Approver

Das ist ein produktionsreifes Setup, das Balance zwischen Sicherheit und Geschwindigkeit schafft. Deployments sind kontrolliert, aber nicht bürokratisch – das Team kann schnell iterieren, ohne die Guardrails zu umgehen.

11.10 Monitoring und Observability

Ein Workflow ist nur so gut wie die Sichtbarkeit, die er bietet. GitHub Actions generiert für jeden Run umfangreiche Telemetriedaten – von einfachen Statusmeldungen bis zu detaillierten Execution-Traces. Diese Daten sind nicht nur für Debugging relevant, sondern bilden die Grundlage für kontinuierliche Prozessverbesserung. Wer versteht, wie lange bestimmte Jobs laufen, welche Steps häufig fehlschlagen und wo Engpässe entstehen, kann Workflows gezielt optimieren.

Die Observability-Strategie von GitHub Actions basiert auf mehreren Schichten: Die UI bietet visuelle Übersichten und Echtzeit-Logs, die REST API ermöglicht programmatischen Zugriff auf alle Metadaten, und das Checks-System stellt strukturierte Annotations bereit, die direkt im Code-Review-Kontext sichtbar sind. Zusammen bilden diese Mechanismen ein umfassendes Monitoring-Ökosystem.

11.10.1 Workflow-Runs in der UI beobachten

Die primäre Anlaufstelle für Monitoring ist der Actions-Tab eines Repositories. Hier werden alle Workflow-Runs chronologisch aufgelistet, gefiltert nach Workflows, Branches, Events und Status. Die Farbcodierung ist konsistent: Grün für Success, Rot für Failure, Gelb für Cancelled, Grau für Skipped.

Jeder Run-Eintrag zeigt auf einen Blick:

Ein Klick auf einen Run öffnet die Workflow-Summary-Seite. Diese zeigt zunächst einen Dependency-Graph: Eine visuelle Darstellung aller Jobs und ihrer Abhängigkeiten. Jobs, die parallel laufen, erscheinen nebeneinander. Sequenzielle Jobs sind durch Pfeile verbunden. Diese Visualisierung macht besonders bei komplexen Workflows mit needs-Dependencies sofort klar, wo der kritische Pfad liegt.

In diesem Beispiel ist sofort erkennbar: Integration Tests ist der Bottleneck. Alle drei parallelen Vorgänger-Jobs müssen abgeschlossen sein, bevor dieser Job starten kann. Ein Failure hier blockiert die gesamte nachfolgende Pipeline.

Unterhalb des Graphs listet die UI alle Jobs mit ihrem individuellen Status auf. Ein Klick auf einen Job öffnet dessen Logs. Seit 2024 unterstützt GitHub “Backscroll” – auch bei laufenden Jobs werden die letzten 1.000 Log-Zeilen sofort sichtbar, ohne auf Completion warten zu müssen. Dies ist essentiell für lange laufende Jobs wie umfangreiche Test-Suites oder Multi-Stage-Builds.

Die Logs sind hierarchisch strukturiert:

  1. Job-Level: Setup und Completion, einschließlich Runner-Informationen
  2. Step-Level: Jeder Step ist collapsible, fehlgeschlagene Steps sind automatisch expanded
  3. Command-Level: Innerhalb eines Steps werden alle Shell-Commands und deren Output angezeigt

Die Suchfunktion ist kontextabhängig: Sie durchsucht nur expanded Steps. Ein häufiges Pattern ist, alle Steps zu expandieren (Expand all logs), dann nach Fehlermustern zu suchen – etwa error:, failed, oder spezifischen Exception-Namen.

Nützlich ist auch das Pinning-Feature: Bis zu fünf Workflows lassen sich pro Repository an den Anfang der Liste pinnen. Dies hilft bei Repositories mit vielen Workflows, die wichtigsten (etwa Production-Deployments oder Security-Scans) prominent zu platzieren.

11.10.2 Log-Download und Retention

Logs bleiben nicht ewig verfügbar. Ihre Retention folgt den Artifact-Retention-Einstellungen des Repositories (Standard: 90 Tage). Danach werden sie automatisch gelöscht. Für Compliance oder langfristige Analyse-Zwecke bietet GitHub einen Download-Mechanismus.

Der Download erfolgt über die UI (Download log archive) oder die REST API. Das resultierende ZIP-Archiv enthält:

workflow-logs/
├── 1_Setup job/
│   ├── 1_Set up job.txt
│   └── 2_Complete job.txt
├── 2_Checkout/
│   └── 1_Run actions-checkout@v4.txt
├── 3_Build/
│   └── 1_Run npm run build.txt
└── 4_Test/
    └── 1_Run npm test.txt

Jeder Step ist eine separate Textdatei. Dies ermöglicht strukturierte Log-Analyse mit Standard-Unix-Tools:

# Alle Errors finden
grep -r "ERROR" workflow-logs/

# Durchschnittliche Build-Zeit ermitteln
grep "Elapsed time" workflow-logs/3_Build/*.txt | awk '{sum+=$3; count++} END {print sum/count}'

# Test-Failures aggregieren
grep -h "FAIL" workflow-logs/4_Test/*.txt | sort | uniq -c

Ein wichtiger Aspekt: Bei Re-Runs enthält das Log-Archiv nur die Logs des aktuellen Attempts. Logs vorheriger Attempts müssen separat heruntergeladen werden, indem man den entsprechenden Attempt in der UI auswählt und dort den Download triggert.

Die API bietet hier mehr Flexibilität:

# Logs für spezifischen Attempt herunterladen
curl -L \
  -H "Authorization: Bearer $GITHUB_TOKEN" \
  https://api.github.com/repos/owner/repo/actions/runs/$RUN_ID/attempts/$ATTEMPT_NUM/logs \
  -o logs-attempt-$ATTEMPT_NUM.zip

Dies ist relevant für Failure-Analyse: Wenn ein Workflow beim ersten Versuch fehlschlägt, der Re-Run aber erfolgreich ist, gehen die Failure-Logs verloren, wenn man sie nicht explizit sichert.

11.10.3 Debug-Logging aktivieren

Standard-Logs zeigen Commands und deren Output, aber nicht die internen Evaluierungen von GitHub Actions selbst. Debug-Logging behebt dies durch zwei zusätzliche Ebenen:

Step Debug Logging (ACTIONS_STEP_DEBUG=true) zeigt, wie Expressions evaluiert werden. Dies ist besonders wertvoll für komplexe Conditionals:

- name: Deploy to staging
  if: success() && github.ref == 'refs/heads/develop'
  run: ./deploy.sh staging

Mit Debug-Logging aktiviert, zeigen die Logs:

##[debug]Evaluating condition for step: 'Deploy to staging'
##[debug]Evaluating And:
##[debug]..Evaluating success:
##[debug]..=> true
##[debug]..Evaluating Equal:
##[debug]....Evaluating Index:
##[debug]......Evaluating github:
##[debug]......=> Object
##[debug]......Evaluating String:
##[debug]......=> 'ref'
##[debug]....=> 'refs/heads/main'
##[debug]....Evaluating String:
##[debug]....=> 'refs/heads/develop'
##[debug]..=> false
##[debug]=> false
##[debug]Result: false

Dies macht sofort klar: Der Step wurde übersprungen, weil github.ref auf main zeigt, nicht auf develop. Ohne Debug-Logging wäre nur sichtbar, dass der Step skipped wurde – nicht warum.

Runner Debug Logging (ACTIONS_RUNNER_DEBUG=true) exponiert Runner-Internals: Wie werden Secrets injiziert, wie läuft die Job-Initialisierung, welche Environment-Variables werden gesetzt. Dies hilft bei Runner-spezifischen Problemen, etwa bei Self-Hosted-Runnern mit Custom-Configuration.

Die Aktivierung erfolgt über Repository-Secrets oder -Variables:

  1. SettingsSecrets and variablesActions
  2. Neues Secret oder Variable anlegen: ACTIONS_STEP_DEBUGtrue
  3. Bei Bedarf auch ACTIONS_RUNNER_DEBUGtrue

Der Unterschied zwischen Secret und Variable ist rein organisatorisch – beide aktivieren Debug-Logging. Wichtig: Existiert sowohl Secret als auch Variable mit demselben Namen, hat das Secret Vorrang.

Alternativ lässt sich Debug-Logging on-demand beim Re-Run aktivieren: Re-run jobsRe-run failed jobs☑ Enable debug logging. Dies vermeidet, dass alle nachfolgenden Runs mit Debug-Logging laufen – nützlich für einmalige Diagnosen.

11.10.4 Workflow Commands und Annotations

Workflows können während ihrer Ausführung strukturierte Nachrichten an GitHub senden. Diese erscheinen nicht nur in den Logs, sondern erzeugen Annotations, die in der UI prominent dargestellt werden.

Die Syntax basiert auf echo-Statements mit speziellem Format:

- name: Validate code
  run: |
    echo "::notice::Validation started"
    echo "::warning file=src/app.js,line=42::Deprecated API usage detected"
    echo "::error file=src/utils.js,line=15,col=8::Null pointer exception possible"

Dies erzeugt drei verschiedene Annotation-Levels:

Level Symbol Verwendung Erscheinung
notice ℹ️ Informationen, Hinweise Blau, nicht kritisch
warning ⚠️ Potenzielle Probleme Gelb, erfordert Aufmerksamkeit
error Kritische Fehler Rot, blockiert oft Merge

Die Annotations erscheinen an mehreren Stellen:

  1. Im Workflow-Log: Direkt beim jeweiligen Step
  2. In der Job-Summary: Aggregiert über alle Steps
  3. Im Pull Request: Bei den Files-Changed, inline an der referenzierten Zeile

Der letzte Punkt ist der mächtigste: Ein error mit Datei- und Zeilen-Referenz erscheint direkt im Code-Review-Diff. Reviewer sehen sofort, welche Zeilen Probleme haben, ohne in die Actions-Logs wechseln zu müssen.

Weitere nützliche Commands:

::add-mask:: maskiert Secrets in Logs. Wenn ein Workflow dynamisch ein Secret generiert oder aus einer API lädt, sollte es maskiert werden, bevor es geloggt wird:

- name: Generate token
  run: |
    TOKEN=$(curl -s api.example.com/token)
    echo "::add-mask::$TOKEN"
    echo "Generated token: $TOKEN"  # Erscheint als "***"

::debug:: schreibt Debug-Messages, die nur sichtbar sind, wenn ACTIONS_STEP_DEBUG=true:

- name: Complex calculation
  run: |
    echo "::debug::Input value: $INPUT"
    RESULT=$((INPUT * 2))
    echo "::debug::Calculated result: $RESULT"
    echo "Result: $RESULT"

::group:: / ::endgroup:: gruppiert Log-Output, der standardmäßig collapsed ist:

- name: Install dependencies
  run: |
    echo "::group::npm install"
    npm install --verbose
    echo "::endgroup::"

Dies reduziert Log-Clutter bei verbose Commands, ohne Information zu verlieren – der Output ist nur einen Klick entfernt.

11.10.5 Checks API und programmatische Annotations

Während Workflow Commands von innerhalb eines Workflows gesendet werden, ermöglicht die Checks API externe Tools, Annotations zu erstellen. Dies ist das Fundament für GitHub Apps, die Code-Analyse, Security-Scans oder Custom-Validierungen durchführen.

Die Checks API arbeitet mit zwei Konzepten:

Check Suites sind Container für mehrere Check Runs. GitHub erstellt automatisch eine Check Suite pro Commit. Alle Workflows, die für diesen Commit laufen, sind Check Runs innerhalb dieser Suite.

Check Runs repräsentieren einzelne Checks – typischerweise ein Job oder ein externer Service. Ein Check Run hat:

Der Lifecycle eines Check Runs:

  1. Create: Check Run mit Status queued oder in_progress anlegen
  2. Update: Status und Output aktualisieren, Annotations hinzufügen
  3. Complete: Finalen Status und Conclusion setzen

Ein Beispiel: Eine GitHub App, die Markdown-Files auf kaputte Links prüft:

// 1. Check Run erstellen
await octokit.checks.create({
  owner: 'org',
  repo: 'repo',
  name: 'Link Checker',
  head_sha: commit_sha,
  status: 'in_progress',
  output: {
    title: 'Checking links...',
    summary: 'Scanning all markdown files'
  }
});

// 2. Analyse durchführen, kaputte Links finden
const brokenLinks = findBrokenLinks();

// 3. Check Run mit Annotations abschließen
await octokit.checks.update({
  owner: 'org',
  repo: 'repo',
  check_run_id: checkRunId,
  status: 'completed',
  conclusion: brokenLinks.length > 0 ? 'failure' : 'success',
  output: {
    title: brokenLinks.length > 0 
      ? `Found ${brokenLinks.length} broken links`
      : 'All links valid',
    summary: `Checked ${totalLinks} links in ${fileCount} files`,
    annotations: brokenLinks.map(link => ({
      path: link.file,
      start_line: link.line,
      end_line: link.line,
      annotation_level: 'error',
      message: `Broken link: ${link.url}`,
      title: 'Dead Link'
    }))
  }
});

Die resultierenden Annotations erscheinen im PR-Diff wie native GitHub-Actions-Errors. Reviewer sehen inline, welche Links kaputt sind, und können diese direkt fixen.

Die Annotations-Struktur unterstützt auch mehrzeilige Bereiche:

{
  path: 'src/component.js',
  start_line: 10,
  end_line: 25,
  annotation_level: 'warning',
  message: 'This entire function should be refactored for performance',
  title: 'Performance Issue',
  raw_details: 'Detailed explanation with metrics...'
}

Der raw_details-Feld kann umfangreichen Markdown-Content enthalten – ideal für ausführliche Erklärungen, die im Inline-Comment zu viel wären.

Ein wichtiges Limit: Pro Check Run sind maximal 50 Annotations pro API-Call erlaubt. Bei mehr Annotations müssen mehrere Update-Calls erfolgen. Zudem sollten Annotations sparsam eingesetzt werden – ein Check Run mit 500 Annotations wird unübersichtlich. Besser: Severe Issues als Annotations, der Rest im Summary oder raw_details.

11.10.6 Job Summaries

Seit 2022 können Workflows strukturierte Summaries generieren, die prominent auf der Workflow-Run-Seite angezeigt werden. Dies ist ideal für Test-Reports, Coverage-Statistiken oder Deployment-Informationen.

Summaries werden durch Schreiben in die Datei $GITHUB_STEP_SUMMARY erstellt:

- name: Run tests
  run: |
    npm test -- --json > test-results.json
    
- name: Generate summary
  if: always()
  run: |
    cat >> $GITHUB_STEP_SUMMARY << 'EOF'
    ## Test Results
    
    | Status | Count |
    |--------|-------|
    | ✅ Passed | $(jq '.numPassedTests' test-results.json) |
    | ❌ Failed | $(jq '.numFailedTests' test-results.json) |
    | ⏭️ Skipped | $(jq '.numPendingTests' test-results.json) |
    
    **Total Duration:** $(jq '.testDuration' test-results.json)ms
    EOF

Die Summary erscheint direkt auf der Run-Summary-Page, ohne in die Logs klicken zu müssen. Dies ist besonders nützlich für Pull Requests: Contributors sehen sofort, wie viele Tests fehlschlagen, ohne detaillierte Logs durchsuchen zu müssen.

Summaries unterstützen vollständiges GitHub-Flavored-Markdown: Tabellen, Code-Blöcke, Bilder, Links. Ein fortgeschrittenes Pattern:

- name: Coverage report
  run: |
    cat >> $GITHUB_STEP_SUMMARY << 'EOF'
    ## Code Coverage
    
    ```mermaid
    pie
        "Covered" : 85
        "Uncovered" : 15
    ```
    
    ### Details by Package
    
    $(generate_coverage_table)
    
    [Full HTML Report](https://ci-artifacts.example.com/${{ github.run_id }}/coverage)
    EOF

Dies generiert eine visuell ansprechende Coverage-Summary inklusive Mermaid-Diagramm und Link zum vollständigen Report.

Summaries sind auch über mehrere Steps hinweg akkumulierend: Jeder Step, der in $GITHUB_STEP_SUMMARY schreibt, fügt Content hinzu. Am Ende des Jobs werden alle Summaries kombiniert und dargestellt. Dies erlaubt modularen Aufbau – etwa separate Steps für Unit-Tests, Integration-Tests und Linting, die jeweils ihre eigene Section zur Summary beitragen.

11.10.7 REST API für Monitoring-Automation

Die GitHub REST API bietet programmatischen Zugriff auf alle Monitoring-Daten. Dies ermöglicht Custom-Dashboards, Alerting-Systeme oder Trend-Analysen über Zeit.

Zentrale Endpoints:

GET /repos/{owner}/{repo}/actions/runs – Liste aller Workflow-Runs mit Filterung:

# Alle fehlgeschlagenen Runs der letzten Woche
curl -H "Authorization: Bearer $TOKEN" \
  "https://api.github.com/repos/org/repo/actions/runs?status=failure&created=>2024-12-05"

GET /repos/{owner}/{repo}/actions/runs/{run_id} – Details eines spezifischen Runs:

{
  "id": 1234567890,
  "name": "CI Pipeline",
  "status": "completed",
  "conclusion": "failure",
  "run_started_at": "2024-12-12T10:00:00Z",
  "run_attempt": 2,
  "run_number": 42,
  "event": "pull_request",
  "workflow_id": 9876543,
  "jobs_url": "https://api.github.com/repos/org/repo/actions/runs/1234567890/jobs"
}

GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs – Alle Jobs eines Runs:

{
  "jobs": [
    {
      "id": 111,
      "name": "Build",
      "status": "completed",
      "conclusion": "success",
      "started_at": "2024-12-12T10:01:00Z",
      "completed_at": "2024-12-12T10:05:23Z",
      "steps": [
        {
          "name": "Checkout",
          "status": "completed",
          "conclusion": "success",
          "number": 1,
          "started_at": "2024-12-12T10:01:05Z",
          "completed_at": "2024-12-12T10:01:15Z"
        }
      ]
    }
  ]
}

Ein praktisches Monitoring-Script: Durchschnittliche Job-Dauer über die letzten 100 Runs ermitteln:

import requests
from datetime import datetime

def get_avg_job_duration(owner, repo, job_name):
    runs = requests.get(
        f"https://api.github.com/repos/{owner}/{repo}/actions/runs",
        params={"per_page": 100},
        headers={"Authorization": f"Bearer {token}"}
    ).json()["workflow_runs"]
    
    durations = []
    for run in runs:
        jobs = requests.get(run["jobs_url"]).json()["jobs"]
        for job in jobs:
            if job["name"] == job_name and job["conclusion"] == "success":
                start = datetime.fromisoformat(job["started_at"].rstrip("Z"))
                end = datetime.fromisoformat(job["completed_at"].rstrip("Z"))
                durations.append((end - start).total_seconds())
    
    return sum(durations) / len(durations) if durations else 0

avg = get_avg_job_duration("org", "repo", "Build")
print(f"Average build time: {avg/60:.1f} minutes")

Dies lässt sich erweitern für Trend-Analysen: Wie entwickelt sich die Build-Zeit über Wochen hinweg? Gibt es Korrelationen zwischen Commit-Häufigkeit und Failure-Rate?

Webhooks für Echtzeit-Monitoring: Statt die API zu pollen, können Workflows auch Events triggern, auf die externe Systeme reagieren:

on:
  workflow_run:
    workflows: ["CI Pipeline"]
    types: [completed]
    
jobs:
  notify:
    runs-on: ubuntu-latest
    if: ${{ github.event.workflow_run.conclusion == 'failure' }}
    steps:
      - name: Send alert
        run: |
          curl -X POST $SLACK_WEBHOOK \
            -d "{\"text\": \"CI failed on ${{ github.ref }}\"}"

Dies pusht Failure-Notifications sofort, ohne dass ein externes System die API abfragen muss.

Die Kombination aus UI, Logs, Annotations, Checks API und REST API bildet ein leistungsfähiges Monitoring-Ökosystem. Die Kunst liegt darin, die richtige Granularität zu finden: Genug Observability für schnelles Debugging, aber nicht so viel, dass Signal in Noise untergeht.