Alle vorherigen Kapitel haben eines gemeinsam: Sie bereiten Code für das Deployment vor. Tests stellen sicher, dass nichts kaputt ist. Caching beschleunigt den Prozess. Reusable Workflows standardisieren die Pipeline. Aber irgendwann muss der Code tatsächlich irgendwo hin.
Dieses Kapitel behandelt das eigentliche Deployment: Wie Workflows Code sicher in verschiedene Umgebungen bringen, welche Schutzmechanismen GitHub bietet und wie badge-gen sowohl auf GitHub Pages als auch auf PyPI landet.
Ein Environment in GitHub Actions ist eine benannte Deployment-Zielumgebung mit eigenen:
Via UI: Repository → Settings → Environments → New environment
Via Workflow: Beim ersten Referenzieren wird ein Environment automatisch erstellt:
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Erstellt "production" falls nicht vorhandenWichtig: Automatisch erstellte Environments haben keine Protection Rules. Für produktive Umgebungen sollten diese immer manuell konfiguriert werden.
jobs:
deploy-staging:
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com # Optional: Link im UI
steps:
- run: echo "Deploying to staging"
deploy-production:
runs-on: ubuntu-latest
needs: deploy-staging
environment:
name: production
url: https://example.com
steps:
- run: echo "Deploying to production"Die url erscheint als klickbarer Link in der GitHub UI –
praktisch für schnellen Zugriff auf die deployten Anwendung.
Environments haben eigene Secrets und Variables, die nur in Jobs verfügbar sind, die das Environment referenzieren:
jobs:
deploy:
environment: production
runs-on: ubuntu-latest
steps:
- name: Deploy
env:
# Environment Secret (nur in diesem Job verfügbar)
API_KEY: ${{ secrets.PRODUCTION_API_KEY }}
# Environment Variable
API_URL: ${{ vars.PRODUCTION_API_URL }}
run: ./deploy.shHierarchie der Secrets:
| Ebene | Scope | Priorität |
|---|---|---|
| Environment | Nur Jobs mit diesem Environment | Höchste |
| Repository | Alle Jobs im Repository | Mittel |
| Organization | Alle Repos der Organisation | Niedrigste |
Bei Namenskonflikten gewinnt das Environment Secret.
Ein oder mehrere Personen/Teams müssen das Deployment genehmigen:
Settings → Environments → [environment] → Required reviewers
Konfiguration:
Im Workflow:
jobs:
deploy:
environment: production # Wartet auf Genehmigung
runs-on: ubuntu-latest
steps:
- run: ./deploy.shDer Job pausiert und zeigt “Waiting for review” bis ein berechtigter Reviewer im GitHub UI auf “Approve” klickt.
Automatische Verzögerung vor dem Deployment:
Settings → Environments → [environment] → Wait timer → [1-43200 Minuten]
Use-Cases:
Wichtig: Wait Timer zählt nicht gegen die Billing-Minuten – der Runner startet erst nach Ablauf.
Einschränkung, welche Branches in ein Environment deployen dürfen:
| Option | Beschreibung |
|---|---|
| All branches | Keine Einschränkung |
| Protected branches | Nur Branches mit Branch Protection |
| Selected branches | Explizite Liste (Patterns möglich) |
Beispiel: Nur main und
release/* dürfen nach Production:
Selected branches:
- main
- release/*
Ein Workflow von einem Feature-Branch würde bei
environment: production fehlschlagen.
Für komplexere Anforderungen: GitHub Apps als Protection Rules. Diese können externe Systeme abfragen:
| Partner | Use-Case |
|---|---|
| Datadog | Deployment nur wenn Error-Rate unter Schwellwert |
| Honeycomb | Performance-Metriken prüfen |
| ServiceNow | Change-Management-Approval |
| New Relic | System-Health-Check |
Ablauf:
approved oder
rejectedLimitierung: Maximal 6 Protection Rules pro Environment (inklusive Built-in Rules).
Das klassische Muster: Erst Staging, dann Production.
name: Deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
deploy-staging:
needs: test
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.badge-gen.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to Staging
run: ./deploy.sh staging
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment:
name: production
url: https://badge-gen.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to Production
run: ./deploy.sh productionDeployment auf mehrere Ziele parallel:
jobs:
deploy:
runs-on: ubuntu-latest
strategy:
matrix:
region: [eu-west-1, us-east-1, ap-southeast-1]
fail-fast: false # Andere Regionen weitermachen wenn eine fehlschlägt
environment:
name: production-${{ matrix.region }}
url: https://${{ matrix.region }}.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to ${{ matrix.region }}
env:
AWS_REGION: ${{ matrix.region }}
run: ./deploy.shHinweis: Jede Region ist ein separates Environment – Protection Rules müssen für jedes einzeln konfiguriert werden.
Bei fehlgeschlagenem Deployment: Automatischer Rollback auf vorherige Version.
jobs:
deploy:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- name: Get current deployment
id: current
run: |
CURRENT_SHA=$(curl -s https://example.com/version.txt)
echo "sha=$CURRENT_SHA" >> $GITHUB_OUTPUT
- name: Deploy new version
id: deploy
run: ./deploy.sh
continue-on-error: true
- name: Health check
id: health
if: steps.deploy.outcome == 'success'
run: |
for i in {1..5}; do
if curl -sf https://example.com/health; then
echo "Health check passed"
exit 0
fi
sleep 10
done
echo "Health check failed"
exit 1
continue-on-error: true
- name: Rollback if needed
if: steps.deploy.outcome == 'failure' || steps.health.outcome == 'failure'
env:
ROLLBACK_SHA: ${{ steps.current.outputs.sha }}
run: |
echo "Rolling back to $ROLLBACK_SHA"
git checkout $ROLLBACK_SHA
./deploy.sh
exit 1 # Workflow als fehlgeschlagen markierenGitHub Pages unterstützt zwei Deployment-Methoden: 1.
Branch-basiert: Inhalte aus einem Branch (z.B.
gh-pages) werden automatisch deployed 2.
Actions-basiert: Workflow uploaded Artefakte, die dann
deployed werden
Für badge-gen nutzen wir die Actions-basierte Methode – mehr Kontrolle, keine zusätzlichen Branches.
Settings → Pages → Build and deployment → Source → GitHub Actions
# .github/workflows/deploy-pages.yml
name: Deploy to GitHub Pages
on:
push:
branches: [main]
workflow_dispatch: # Manuelles Triggern erlauben
permissions:
contents: read
pages: write
id-token: write
concurrency:
group: pages
cancel-in-progress: false # Laufende Deployments nicht abbrechen
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
cache: 'pip'
- name: Install dependencies
run: pip install -e .
- name: Generate badges
run: |
mkdir -p _site/badges
# Repository-Metriken sammeln (vereinfacht)
badge-gen create \
--name "build" \
--value "passing" \
--color "green" \
--output _site/badges/build.svg
badge-gen create \
--name "coverage" \
--value "87%" \
--color "yellow" \
--output _site/badges/coverage.svg
badge-gen create \
--name "python" \
--value "3.12" \
--color "blue" \
--output _site/badges/python.svg
- name: Create index page
run: |
cat > _site/index.html << 'EOF'
<!DOCTYPE html>
<html>
<head><title>badge-gen Badges</title></head>
<body>
<h1>Project Badges</h1>
<p><img src="badges/build.svg" alt="Build Status"></p>
<p><img src="badges/coverage.svg" alt="Coverage"></p>
<p><img src="badges/python.svg" alt="Python Version"></p>
</body>
</html>
EOF
- name: Setup Pages
uses: actions/configure-pages@v5
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: _site
deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4Permissions:
permissions:
contents: read # Repository lesen
pages: write # Pages-Artefakte schreiben
id-token: write # OIDC-Token für sichere AuthentifizierungConcurrency:
concurrency:
group: pages
cancel-in-progress: falseWarum false? Ein Deployment sollte vollständig
abgeschlossen werden. Bei true könnte ein neuer Push ein
laufendes Deployment abbrechen und die Seite in einem inkonsistenten
Zustand hinterlassen.
Zwei Jobs:
build: Generiert die statischen Dateien und uploaded
sie als Artefaktdeploy: Nimmt das Artefakt und deployed es zu GitHub
PagesDiese Trennung ermöglicht, dass deploy ein separates
Environment mit Protection Rules nutzen kann.
Das github-pages Environment: GitHub erstellt
automatisch ein Environment namens github-pages für
Pages-Deployments. Protection Rules können hier konfiguriert werden.
Für Branch-basiertes Deployment (älterer Ansatz, aber immer noch populär):
- name: Deploy
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./_siteDiese Action pushed die Dateien in den gh-pages Branch.
GitHub Pages muss dann auf diesen Branch konfiguriert sein.
PyPI unterstützt Trusted Publishing via OpenID Connect (OIDC). Der Vorteil: Keine API-Tokens im Repository nötig. GitHub Actions authentifiziert sich direkt bei PyPI.
Konto erstellen: https://pypi.org/account/register/
Trusted Publisher hinzufügen: https://pypi.org/manage/account/publishing/
Formular ausfüllen:
badge-genyour-github-usernamebadge-genpublish.ymlpypi (empfohlen)Repository → Settings → Environments → New environment → "pypi"
Empfohlene Protection Rules für pypi:
main# .github/workflows/publish.yml
name: Publish to PyPI
on:
release:
types: [published]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install build tools
run: pip install build
- name: Build distribution
run: python -m build
- name: Upload dist artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish:
needs: build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/project/badge-gen/
permissions:
id-token: write # Erforderlich für Trusted Publishing
steps:
- name: Download dist artifacts
uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1Security: Der Build-Job hat keinen Zugriff auf
id-token: write. Nur der Publish-Job erhält diese
Permission – und nur nach Genehmigung durch das Environment.
Atomic Uploads: Alle Distribution-Dateien werden in einem Job gebaut und als Artefakt gespeichert. Der Publish-Job uploaded dann alle auf einmal zu PyPI.
Troubleshooting: Wenn das Publishing fehlschlägt, kann man den Publish-Job erneut starten, ohne neu zu bauen.
Vor dem echten Release: Erst auf TestPyPI testen.
jobs:
publish-testpypi:
runs-on: ubuntu-latest
environment: testpypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/Für TestPyPI muss ein separater Trusted Publisher konfiguriert werden.
Ein vollständiger Workflow der testet, baut, und veröffentlicht:
name: Release
on:
push:
tags:
- 'v*'
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python: ['3.10', '3.11', '3.12']
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
cache: 'pip'
- run: pip install -e .[dev]
- run: pytest
build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install build
- run: python -m build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
publish-testpypi:
needs: build
runs-on: ubuntu-latest
environment: testpypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
publish-pypi:
needs: publish-testpypi
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- uses: pypa/gh-action-pypi-publish@release/v1
github-release:
needs: publish-pypi
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Create GitHub Release
uses: ncipollo/release-action@v1
with:
artifacts: "dist/*"
generateReleaseNotes: trueGitHub trackt automatisch alle Deployments:
Repository → Deployments
Hier sieht man:
Der einfachste Rollback: Den Workflow einer älteren Version erneut ausführen.
Einschränkung: Der Workflow verwendet den Code zum Zeitpunkt des ursprünglichen Runs, aber die aktuellen Workflow-Definitionen (außer bei Re-run von failed jobs).
Sauberer Ansatz: Einen Revert-Commit erstellen.
git revert HEAD
git push origin mainDer Revert-Commit triggert den normalen Deployment-Workflow und deployed effektiv die vorherige Version.
Symptom: secrets.PRODUCTION_API_KEY ist
leer.
Mögliche Ursachen: 1. Job referenziert kein Environment: ```yaml # ❌ Kein Environment jobs: deploy: runs-on: ubuntu-latest
# ✅ Mit Environment jobs: deploy: environment: production runs-on: ubuntu-latest ```
Branch darf nicht in Environment deployen (Deployment Branches Regel)
Secret existiert auf Repository-Ebene, nicht Environment-Ebene
Symptom: Job zeigt “Waiting for review” aber niemand kann genehmigen.
Mögliche Ursachen:
Symptom:
Error: No uploaded artifact was found!
Lösung: Build- und Deploy-Jobs müssen das gleiche Artefakt nutzen:
# Build-Job
- uses: actions/upload-pages-artifact@v3
with:
path: _site
# Deploy-Job
- uses: actions/deploy-pages@v4
# Kein Download nötig – deploy-pages holt das Artefakt automatischWichtig: actions/upload-pages-artifact
erstellt ein Artefakt mit dem festen Namen github-pages.
Dieses wird von actions/deploy-pages automatisch
verwendet.
Symptom: HTTPError: 403 Forbidden
Checkliste:
permissions: id-token: write ist gesetztDebug:
- name: Debug OIDC
run: |
echo "Repository: ${{ github.repository }}"
echo "Workflow: ${{ github.workflow }}"
echo "Environment: ${{ github.environment }}"Symptom: Deployment wird cancelled obwohl es lief.
Ursache: cancel-in-progress: true bei
schnellen Push-Folgen.
Lösung für Deployments:
concurrency:
group: deploy-${{ github.ref }}
cancel-in-progress: false # Deployments nie abbrechendevelopment → Automatisch, keine Approvals
staging → Automatisch nach Tests
production → Required Reviewers + Wait Timer
production-eu → Region-spezifisch
production-us
# Nur was nötig ist
permissions:
contents: read
pages: write
id-token: write
# NICHT
permissions: write-all| Secret | Ebene | Grund |
|---|---|---|
CODECOV_TOKEN |
Repository | Alle Branches brauchen es |
STAGING_API_KEY |
Environment staging | Nur Staging-Deployments |
PRODUCTION_API_KEY |
Environment production | Maximale Isolation |
Deployments nur während der Geschäftszeiten:
on:
push:
branches: [main]
schedule:
# Nur Mo-Fr 9-17 Uhr UTC
- cron: '0 9-17 * * 1-5'Kombiniert mit Wait Timer: Push triggert Workflow, Wait Timer verzögert bis zum nächsten Deployment-Fenster.
- name: Deploy
run: ./deploy.sh
- name: Wait for rollout
run: sleep 60
- name: Smoke test
run: |
curl -sf https://example.com/health || exit 1
curl -sf https://example.com/api/status || exit 1
- name: Notify on failure
if: failure()
uses: slackapi/slack-github-action@v1
with:
channel-id: 'deployments'
slack-message: "Deployment failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
env:
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}