26 Deployment-Strategien – von der Pipeline in die Produktion

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.

26.1 Environments – mehr als nur Labels

26.1.1 Das Konzept

Ein Environment in GitHub Actions ist eine benannte Deployment-Zielumgebung mit eigenen:

26.1.2 Environment erstellen

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 vorhanden

Wichtig: Automatisch erstellte Environments haben keine Protection Rules. Für produktive Umgebungen sollten diese immer manuell konfiguriert werden.

26.1.3 Environment in Jobs referenzieren

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.

26.1.4 Environment Secrets und Variables

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.sh

Hierarchie 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.

26.2 Protection Rules – Deployments absichern

26.2.1 Required Reviewers

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.sh

Der Job pausiert und zeigt “Waiting for review” bis ein berechtigter Reviewer im GitHub UI auf “Approve” klickt.

26.2.2 Wait Timer

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.

26.2.3 Deployment Branches

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.

26.2.4 Custom Deployment Protection Rules

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:

  1. GitHub App auf Repository installieren
  2. App in Environment aktivieren
  3. Bei Deployment: GitHub sendet Webhook an App
  4. App antwortet mit approved oder rejected
  5. Workflow wartet bis zu 30 Tage auf Antwort

Limitierung: Maximal 6 Protection Rules pro Environment (inklusive Built-in Rules).

26.3 Deployment-Muster

26.3.1 Sequentielles Multi-Environment Deployment

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 production

26.3.2 Matrix Deployment

Deployment 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.sh

Hinweis: Jede Region ist ein separates Environment – Protection Rules müssen für jedes einzeln konfiguriert werden.

26.3.3 Rollback-Strategie

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 markieren

26.4 Praxis: badge-gen auf GitHub Pages deployen

26.4.1 GitHub Pages mit Actions

GitHub 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.

26.4.2 Repository konfigurieren

Settings → Pages → Build and deployment → Source → GitHub Actions

26.4.3 Der Deployment-Workflow

# .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@v4

26.4.4 Erklärung der Schlüsselkomponenten

Permissions:

permissions:
  contents: read      # Repository lesen
  pages: write        # Pages-Artefakte schreiben
  id-token: write     # OIDC-Token für sichere Authentifizierung

Concurrency:

concurrency:
  group: pages
  cancel-in-progress: false

Warum 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:

  1. build: Generiert die statischen Dateien und uploaded sie als Artefakt
  2. deploy: Nimmt das Artefakt und deployed es zu GitHub Pages

Diese 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.

26.4.5 Alternative: peaceiris/actions-gh-pages

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: ./_site

Diese Action pushed die Dateien in den gh-pages Branch. GitHub Pages muss dann auf diesen Branch konfiguriert sein.

26.5 Praxis: badge-gen auf PyPI veröffentlichen

26.5.1 Trusted Publishing – der moderne Weg

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.

26.5.2 PyPI konfigurieren

  1. Konto erstellen: https://pypi.org/account/register/

  2. Trusted Publisher hinzufügen: https://pypi.org/manage/account/publishing/

  3. Formular ausfüllen:

26.5.3 GitHub Environment erstellen

Repository → Settings → Environments → New environment → "pypi"

Empfohlene Protection Rules für pypi:

26.5.4 Der Publish-Workflow

# .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/v1

26.5.5 Warum zwei separate Jobs?

Security: 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.

26.5.6 TestPyPI für Testläufe

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.

26.5.7 Release-Workflow komplett

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: true

26.6 Deployment-Historie und Rollbacks

26.6.1 Deployment-Historie einsehen

GitHub trackt automatisch alle Deployments:

Repository → Deployments

Hier sieht man:

26.6.2 Manueller Rollback via Re-run

Der einfachste Rollback: Den Workflow einer älteren Version erneut ausführen.

  1. Repository → Actions → [Workflow]
  2. Erfolgreichen Run einer älteren Version finden
  3. “Re-run all jobs”

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).

26.6.3 Rollback via Revert-Commit

Sauberer Ansatz: Einen Revert-Commit erstellen.

git revert HEAD
git push origin main

Der Revert-Commit triggert den normalen Deployment-Workflow und deployed effektiv die vorherige Version.

26.7 Edge-Cases und Troubleshooting

26.7.1 Problem: Environment Secrets nicht verfügbar

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 ```

  1. Branch darf nicht in Environment deployen (Deployment Branches Regel)

  2. Secret existiert auf Repository-Ebene, nicht Environment-Ebene

26.7.2 Problem: Deployment wartet ewig

Symptom: Job zeigt “Waiting for review” aber niemand kann genehmigen.

Mögliche Ursachen:

  1. Reviewer haben keinen Zugriff auf das Repository
  2. “Prevent self-review” ist aktiv und der Workflow-Auslöser ist der einzige Reviewer
  3. Custom Protection Rule antwortet nicht (Timeout nach 30 Tagen)

26.7.3 Problem: Pages-Deployment schlägt fehl

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 automatisch

Wichtig: actions/upload-pages-artifact erstellt ein Artefakt mit dem festen Namen github-pages. Dieses wird von actions/deploy-pages automatisch verwendet.

26.7.4 Problem: PyPI Trusted Publishing funktioniert nicht

Symptom: HTTPError: 403 Forbidden

Checkliste:

  1. Environment-Name im Workflow stimmt mit PyPI-Konfiguration überein
  2. Workflow-Dateiname stimmt mit PyPI-Konfiguration überein
  3. permissions: id-token: write ist gesetzt
  4. Repository Owner/Name stimmen

Debug:

- name: Debug OIDC
  run: |
    echo "Repository: ${{ github.repository }}"
    echo "Workflow: ${{ github.workflow }}"
    echo "Environment: ${{ github.environment }}"

26.7.5 Problem: Concurrency bricht Deployments ab

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 abbrechen

26.8 Best Practices

26.8.1 Environment-Namenskonvention

development    → Automatisch, keine Approvals
staging        → Automatisch nach Tests
production     → Required Reviewers + Wait Timer
production-eu  → Region-spezifisch
production-us

26.8.2 Permissions minimieren

# Nur was nötig ist
permissions:
  contents: read
  pages: write
  id-token: write

# NICHT
permissions: write-all

26.8.3 Secrets isolieren

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

26.8.4 Deployment-Fenster via Scheduled Workflows

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.

26.8.5 Monitoring nach Deployment

- 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 }}