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.
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.
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.comFür Preview-Deployments bei Pull Requests ist das unverzichtbar. Jeder PR bekommt seine eigene URL, die in der GitHub-UI verlinkt wird.
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):
production@platform-team,
@release-manager)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.
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):
productionDer 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:
Nicht jeder Branch soll in Production deployen dürfen. Branch Policies definieren, welche Branches/Tags ein Environment targeten dürfen.
Konfiguration (UI):
productionmain, 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:
* matcht beliebige Zeichen außer /** matcht beliebige Zeichen inklusive
/release/* matcht release/1.0, aber nicht
release/1.0/hotfixrelease/** matcht beideFortgeschrittene 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.
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_KEYBeide 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.
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.* |
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?
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.shDas 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.
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 productionStaging und Production können parallel deployen (verschiedene Groups), aber innerhalb jedes Environments ist es seriell. Das ist das gewünschte Verhalten.
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 devWenn 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.
Traditionell speichert man Cloud-Credentials (AWS Access Keys, Azure Service Principal, GCP Service Account Keys) als Secrets in GitHub. Das hat Nachteile:
OIDC erlaubt, dass GitHub Actions temporäre, kurzlebige Tokens direkt vom Cloud-Provider anfordert – ohne Secrets in GitHub zu speichern.
Das Flow:
Setup (einmalig):
In AWS: IAM Identity Provider für GitHub erstellen
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.
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:
repo:myorg/myrepo:*repo:myorg/myrepo:environment:productionrepo:myorg/myrepo:ref:refs/heads/mainDas ist erheblich feingranularer als ein statischer Access Key.
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:
main triggert Workflowneeds)Wenn Dev oder Staging fehlschlagen, stoppt die Pipeline automatisch. Production-Deployment passiert nur, wenn alle vorherigen Stages erfolgreich waren.
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 productionDieses 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.
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.
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.
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
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 production2. 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.
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.
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% TrafficCanary-Deployment mit automatischem Monitoring-Check. Nur wenn Error-Rate okay, geht’s weiter zu Full-Deployment.
# ❌ SCHLECHT
jobs:
deploy-production:
runs-on: ubuntu-latest
steps:
- run: ./deploy.sh productionKein 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.
# ❌ SCHLECHT
jobs:
deploy:
steps:
- run: |
export API_KEY="sk-prod-abc123..."
./deploy.shSecrets gehören in GitHub Secrets oder besser: OIDC. Niemals hardcoded im Workflow.
# ❌ 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-1Ohne Concurrency-Control oder Matrix können beide gleichzeitig
fehlschlagen. Besser: Matrix mit max-parallel: 1 oder
sequential Jobs mit needs.
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.comWas macht dieses Setup richtig?
production-Environment hat in Settings Required Reviewers +
Wait Timer + Branch PolicyDas 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.
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.
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:
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.
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 -cEin 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.zipDies 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.
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 stagingMit 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:
Settings → Secrets and variables →
ActionsACTIONS_STEP_DEBUG
→ trueACTIONS_RUNNER_DEBUG →
trueDer 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 jobs → Re-run failed jobs →
☑ Enable debug logging. Dies vermeidet, dass alle
nachfolgenden Runs mit Debug-Logging laufen – nützlich für einmalige
Diagnosen.
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:
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.
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:
queued,
in_progress, completedsuccess,
failure, neutral, cancelled,
skipped, timed_out,
action_requiredDer Lifecycle eines Check Runs:
queued
oder in_progress anlegenEin 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.
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
EOFDie 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)
EOFDies 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.
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.