19 Eigene Actions entwickeln: Composite Actions

Composite Actions sind der einfachste Einstieg in die Action-Entwicklung. Sie bestehen aus reinem YAML, erfordern keine Programmiersprache und können trotzdem mächtige Abstraktionen schaffen. In diesem Kapitel entwickeln wir Composite Actions von Grund auf – von der action.yml-Struktur über Input-Handling bis zur Fehlerbehandlung.

19.1 Wann Composite Actions?

Composite Actions eignen sich für Szenarien, in denen man Step-Sequenzen kapseln möchte, die aus Shell-Befehlen und/oder anderen Actions bestehen. Sie sind das Mittel der Wahl, wenn:

Der entscheidende Unterschied zu JavaScript/Docker Actions: Composite Actions haben keinen eigenen Runtime-Kontext. Sie werden zur Laufzeit “entfaltet” und ihre Steps laufen direkt im Job-Kontext – mit Zugriff auf denselben Workspace, dieselben Environment Variables.

19.2 Anatomie der action.yml

Jede Action – egal welchen Typs – benötigt eine action.yml (oder action.yaml) im Root-Verzeichnis. Diese Datei definiert Metadaten, Inputs, Outputs und die eigentliche Ausführungslogik.

19.2.1 Minimale Struktur

name: 'My Composite Action'
description: 'Eine kurze Beschreibung der Action'

runs:
  using: 'composite'
  steps:
    - run: echo "Hello from Composite Action"
      shell: bash

Das using: 'composite' ist der Marker, der diese Action als Composite Action identifiziert. Die steps folgen derselben Syntax wie Workflow-Steps – mit einem wichtigen Unterschied: Bei run-Steps muss das shell immer explizit angegeben werden.

19.2.2 Vollständige Struktur mit allen Optionen

name: 'Setup and Build'
description: 'Installiert Dependencies und baut das Projekt'
author: 'DevOps Team'

branding:
  icon: 'package'
  color: 'blue'

inputs:
  node-version:
    description: 'Node.js Version'
    required: false
    default: '20'
  working-directory:
    description: 'Arbeitsverzeichnis für npm-Befehle'
    required: false
    default: '.'
  build-command:
    description: 'Build-Befehl'
    required: false
    default: 'npm run build'

outputs:
  build-path:
    description: 'Pfad zum Build-Output'
    value: ${{ steps.build.outputs.path }}
  artifact-name:
    description: 'Name des erzeugten Artifacts'
    value: ${{ steps.meta.outputs.name }}

runs:
  using: 'composite'
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
        cache-dependency-path: ${{ inputs.working-directory }}/package-lock.json
    
    - name: Install dependencies
      run: npm ci
      shell: bash
      working-directory: ${{ inputs.working-directory }}
    
    - name: Build
      id: build
      run: |
        ${{ inputs.build-command }}
        echo "path=${{ inputs.working-directory }}/dist" >> $GITHUB_OUTPUT
      shell: bash
      working-directory: ${{ inputs.working-directory }}
    
    - name: Generate metadata
      id: meta
      run: |
        NAME="build-$(date +%Y%m%d-%H%M%S)"
        echo "name=$NAME" >> $GITHUB_OUTPUT
      shell: bash

19.3 Inputs: Parameter für Flexibilität

19.3.1 Input-Definition

Inputs machen Composite Actions konfigurierbar. Jeder Input hat:

Eigenschaft Beschreibung Pflicht
description Dokumentation für Nutzer Ja
required Muss der Input angegeben werden? Nein (default: false)
default Standardwert wenn nicht angegeben Nein
deprecationMessage Warnung für veraltete Inputs Nein
inputs:
  environment:
    description: 'Zielumgebung (dev, staging, prod)'
    required: true
  
  timeout:
    description: 'Timeout in Sekunden'
    required: false
    default: '300'
  
  legacy-mode:
    description: 'Aktiviert Legacy-Kompatibilität'
    deprecationMessage: 'legacy-mode wird in v3 entfernt. Bitte migrieren.'
    required: false
    default: 'false'

19.3.2 Input-Zugriff in Steps

Inputs sind über den inputs-Kontext verfügbar:

steps:
  - run: |
      echo "Environment: ${{ inputs.environment }}"
      echo "Timeout: ${{ inputs.timeout }}"
    shell: bash

Wichtig: Der inputs-Kontext ist nur innerhalb der Composite Action verfügbar. Im aufrufenden Workflow existiert er nicht.

19.3.3 Input-Validierung

Composite Actions haben keine eingebaute Validierung. Man muss sie selbst implementieren:

steps:
  - name: Validate inputs
    run: |
      # Environment validieren
      case "${{ inputs.environment }}" in
        dev|staging|prod)
          echo "✓ Environment '${{ inputs.environment }}' ist gültig"
          ;;
        *)
          echo "✗ Ungültiges Environment: '${{ inputs.environment }}'"
          echo "  Erlaubt: dev, staging, prod"
          exit 1
          ;;
      esac
      
      # Timeout als Zahl prüfen
      if ! [[ "${{ inputs.timeout }}" =~ ^[0-9]+$ ]]; then
        echo "✗ Timeout muss eine Zahl sein: '${{ inputs.timeout }}'"
        exit 1
      fi
    shell: bash

Ein exit 1 in einem Step bricht die Action ab – und damit auch den aufrufenden Job (sofern nicht continue-on-error gesetzt ist).

19.3.4 Boolean-Inputs: Die Tücken

YAML-Booleans werden in Composite Actions als Strings behandelt. Das führt zu Überraschungen:

# Im aufrufenden Workflow
- uses: ./my-action
  with:
    debug: true  # Wird zu String "true"

# In der Action – FALSCH
- if: ${{ inputs.debug }}  # Immer true, weil "true" und "false" beide truthy Strings sind!
  run: echo "Debug mode"
  shell: bash

# In der Action – RICHTIG
- if: ${{ inputs.debug == 'true' }}
  run: echo "Debug mode"
  shell: bash

Die explizite String-Vergleich == 'true' ist der sichere Weg.

19.4 Outputs: Daten zurückgeben

19.4.1 Output-Definition und -Erzeugung

Outputs erlauben es, Daten aus der Composite Action an den aufrufenden Workflow zurückzugeben. Der Mechanismus ist zweistufig:

  1. Ein Step schreibt in $GITHUB_OUTPUT
  2. Die action.yml mappt den Step-Output auf einen Action-Output
outputs:
  version:
    description: 'Ermittelte Version'
    value: ${{ steps.version.outputs.value }}
  changelog:
    description: 'Changelog seit letztem Release'
    value: ${{ steps.changelog.outputs.content }}

runs:
  using: 'composite'
  steps:
    - name: Determine version
      id: version
      run: |
        VERSION=$(cat package.json | jq -r '.version')
        echo "value=$VERSION" >> $GITHUB_OUTPUT
      shell: bash
    
    - name: Generate changelog
      id: changelog
      run: |
        # Multiline-Output mit Delimiter
        EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)
        echo "content<<$EOF" >> $GITHUB_OUTPUT
        git log --oneline HEAD~10..HEAD >> $GITHUB_OUTPUT
        echo "$EOF" >> $GITHUB_OUTPUT
      shell: bash

19.4.2 Multiline-Outputs

Für mehrzeilige Werte verwendet man das Heredoc-Pattern mit zufälligem Delimiter:

- name: Capture multiline output
  id: report
  run: |
    # Zufälliger Delimiter verhindert Injection
    DELIMITER=$(openssl rand -hex 16)
    
    echo "report<<$DELIMITER" >> $GITHUB_OUTPUT
    cat test-results.txt >> $GITHUB_OUTPUT
    echo "$DELIMITER" >> $GITHUB_OUTPUT
  shell: bash

Der zufällige Delimiter ist eine Sicherheitsmaßnahme: Wenn der Output selbst den Delimiter enthielte, würde er das Output vorzeitig beenden.

19.4.3 Output-Verwendung im Workflow

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build with custom action
        id: build
        uses: ./actions/setup-and-build
        with:
          node-version: '20'
      
      - name: Use outputs
        run: |
          echo "Version: ${{ steps.build.outputs.version }}"
          echo "Build path: ${{ steps.build.outputs.build-path }}"

19.5 Shell-Handling

19.5.1 Verfügbare Shells

In Composite Actions muss für jeden run-Step explizit ein Shell angegeben werden:

Shell Plattform Befehl
bash Linux, macOS, Windows (Git Bash) bash --noprofile --norc -eo pipefail {0}
sh Linux, macOS sh -e {0}
pwsh Alle (PowerShell Core) pwsh -command ". '{0}'"
powershell Windows powershell -command ". '{0}'"
cmd Windows cmd /D /E:ON /V:OFF /S /C "CALL "{0}""
python Alle (wenn installiert) python {0}

19.5.2 Cross-Platform-Actions

Für Actions, die auf allen Plattformen laufen sollen, hat man zwei Optionen:

Option 1: Bash überall (einfach)

steps:
  - run: |
      echo "Works on Linux, macOS, and Windows (Git Bash)"
    shell: bash

Windows-Runner haben Git Bash vorinstalliert. Für die meisten Fälle reicht das.

Option 2: Plattform-spezifische Steps (robust)

steps:
  - name: Setup (Linux/macOS)
    if: runner.os != 'Windows'
    run: |
      chmod +x ./scripts/setup.sh
      ./scripts/setup.sh
    shell: bash
  
  - name: Setup (Windows)
    if: runner.os == 'Windows'
    run: |
      .\scripts\setup.ps1
    shell: pwsh

19.5.3 Error-Handling mit Shell-Optionen

Bash mit -eo pipefail (der Default) bricht bei Fehlern ab. Das ist meist gewünscht, aber manchmal hinderlich:

steps:
  # Befehl darf fehlschlagen
  - name: Optional cleanup
    run: |
      rm -rf ./temp 2>/dev/null || true
    shell: bash
  
  # Fehler explizit behandeln
  - name: Check with fallback
    run: |
      if ! command -v docker &> /dev/null; then
        echo "::warning::Docker nicht gefunden, überspringe Container-Tests"
        echo "docker_available=false" >> $GITHUB_OUTPUT
      else
        echo "docker_available=true" >> $GITHUB_OUTPUT
      fi
    shell: bash
    id: check

19.6 Andere Actions einbinden

Composite Actions können selbst andere Actions aufrufen – das ist ihre Stärke:

runs:
  using: 'composite'
  steps:
    # Marketplace Action
    - uses: actions/setup-python@v5
      with:
        python-version: '3.11'
    
    # Andere Composite Action aus demselben Repo
    - uses: ./.github/actions/lint
    
    # Action aus anderem Repository
    - uses: owner/repo/.github/actions/shared@v1
    
    # Shell-Befehl
    - run: pip install -r requirements.txt
      shell: bash

Wichtig: Der uses-Pfad in einer Composite Action ist relativ zum Repository-Root, nicht zur Action selbst.

19.6.1 Wrapper-Pattern

Ein häufiges Pattern: Eine existierende Action mit eigenen Defaults wrappen:

name: 'Org Node Setup'
description: 'Setup Node.js mit Organisations-Defaults'

inputs:
  node-version:
    description: 'Node.js Version'
    required: false
    default: '20'

runs:
  using: 'composite'
  steps:
    - uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}
        cache: 'npm'
        registry-url: 'https://npm.pkg.github.com'
        scope: '@myorg'
    
    - name: Configure npm
      run: |
        npm config set //npm.pkg.github.com/:_authToken ${{ env.NPM_TOKEN }}
        npm config set @myorg:registry https://npm.pkg.github.com
      shell: bash
      env:
        NPM_TOKEN: ${{ env.NPM_TOKEN }}

Teams verwenden dann uses: ./.github/actions/org-node-setup statt die Konfiguration in jedem Workflow zu wiederholen.

19.7 Dateien und der Workspace

19.7.1 Zugriff auf Repository-Dateien

Composite Actions laufen im selben Workspace wie der Job. Nach einem actions/checkout hat die Action Zugriff auf alle Repository-Dateien:

runs:
  using: 'composite'
  steps:
    - name: Read config
      run: |
        if [[ -f ".github/config.yml" ]]; then
          CONFIG=$(cat .github/config.yml)
          echo "Config gefunden"
        else
          echo "::warning::Keine Config gefunden, verwende Defaults"
        fi
      shell: bash

19.7.2 Mitgelieferte Dateien in der Action

Actions können eigene Dateien mitbringen. Der Pfad zur Action selbst ist über ${{ github.action_path }} verfügbar:

.github/actions/my-action/
├── action.yml
├── scripts/
│   ├── setup.sh
│   └── validate.py
└── templates/
    └── config.template.json
runs:
  using: 'composite'
  steps:
    - name: Run bundled script
      run: |
        chmod +x "${{ github.action_path }}/scripts/setup.sh"
        "${{ github.action_path }}/scripts/setup.sh"
      shell: bash
    
    - name: Copy template
      run: |
        cp "${{ github.action_path }}/templates/config.template.json" ./config.json
      shell: bash

Achtung: github.action_path zeigt auf das Verzeichnis, in dem die action.yml liegt – nicht auf das Repository-Root.

19.8 Logging und Debugging

19.8.1 Workflow Commands

GitHub Actions interpretiert bestimmte Echo-Patterns als Befehle:

steps:
  - name: Logging examples
    run: |
      # Debug-Message (nur sichtbar wenn ACTIONS_STEP_DEBUG=true)
      echo "::debug::Detaillierte Debug-Info"
      
      # Normale Logs mit Leveln
      echo "::notice::Informative Nachricht"
      echo "::warning::Warnung, aber kein Fehler"
      echo "::error::Fehler aufgetreten"
      
      # Mit Datei-Annotation
      echo "::warning file=src/app.js,line=10,col=5::Deprecated function used"
      
      # Gruppen für bessere Lesbarkeit
      echo "::group::Installing dependencies"
      npm ci
      echo "::endgroup::"
      
      # Secret masken (z.B. dynamisch generierte Werte)
      TOKEN=$(generate-token)
      echo "::add-mask::$TOKEN"
      echo "Token ist jetzt maskiert: $TOKEN"  # Zeigt ***
    shell: bash

19.8.2 Debug-Mode in Actions

Für ausführliches Debugging kann man einen Debug-Input anbieten:

inputs:
  debug:
    description: 'Aktiviert Debug-Ausgaben'
    required: false
    default: 'false'

runs:
  using: 'composite'
  steps:
    - name: Debug info
      if: ${{ inputs.debug == 'true' }}
      run: |
        echo "::group::Debug Information"
        echo "Runner OS: ${{ runner.os }}"
        echo "Workspace: ${{ github.workspace }}"
        echo "Action path: ${{ github.action_path }}"
        echo "Event: ${{ github.event_name }}"
        env
        echo "::endgroup::"
      shell: bash

19.8.3 Fehler mit Kontext

Statt nur exit 1 sollte man hilfreiche Fehlermeldungen ausgeben:

steps:
  - name: Validate environment
    run: |
      ERRORS=()
      
      if [[ -z "${{ inputs.api-url }}" ]]; then
        ERRORS+=("api-url ist erforderlich")
      fi
      
      if [[ ! -f "package.json" ]]; then
        ERRORS+=("package.json nicht gefunden – ist actions/checkout gelaufen?")
      fi
      
      if [[ ${#ERRORS[@]} -gt 0 ]]; then
        echo "::error::Validierung fehlgeschlagen:"
        for err in "${ERRORS[@]}"; do
          echo "::error::  - $err"
        done
        exit 1
      fi
    shell: bash

19.9 Praktisches Beispiel: Python-Projekt-Setup

Eine vollständige Composite Action für Python-Projekte:

# .github/actions/python-setup/action.yml
name: 'Python Project Setup'
description: 'Richtet eine Python-Umgebung ein mit Caching und optionalem Linting'
author: 'Platform Team'

branding:
  icon: 'code'
  color: 'yellow'

inputs:
  python-version:
    description: 'Python-Version'
    required: false
    default: '3.11'
  requirements-file:
    description: 'Pfad zur Requirements-Datei'
    required: false
    default: 'requirements.txt'
  install-dev:
    description: 'Installiert auch dev-dependencies'
    required: false
    default: 'false'
  run-lint:
    description: 'Führt Linting mit ruff aus'
    required: false
    default: 'false'
  working-directory:
    description: 'Arbeitsverzeichnis'
    required: false
    default: '.'

outputs:
  python-path:
    description: 'Pfad zur Python-Installation'
    value: ${{ steps.setup.outputs.python-path }}
  cache-hit:
    description: 'Ob der Dependency-Cache getroffen wurde'
    value: ${{ steps.cache.outputs.cache-hit }}
  lint-status:
    description: 'Ergebnis des Linting (success/skipped/failed)'
    value: ${{ steps.lint-result.outputs.status }}

runs:
  using: 'composite'
  steps:
    - name: Validate inputs
      run: |
        cd "${{ inputs.working-directory }}"
        
        if [[ ! -f "${{ inputs.requirements-file }}" ]]; then
          echo "::error::Requirements-Datei nicht gefunden: ${{ inputs.requirements-file }}"
          echo "::error::Arbeitsverzeichnis: $(pwd)"
          exit 1
        fi
      shell: bash
    
    - name: Setup Python
      id: setup
      uses: actions/setup-python@v5
      with:
        python-version: ${{ inputs.python-version }}
    
    - name: Cache pip dependencies
      id: cache
      uses: actions/cache@v4
      with:
        path: ~/.cache/pip
        key: ${{ runner.os }}-pip-${{ inputs.python-version }}-${{ hashFiles(format('{0}/{1}', inputs.working-directory, inputs.requirements-file)) }}
        restore-keys: |
          ${{ runner.os }}-pip-${{ inputs.python-version }}-
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r "${{ inputs.requirements-file }}"
      shell: bash
      working-directory: ${{ inputs.working-directory }}
    
    - name: Install dev dependencies
      if: ${{ inputs.install-dev == 'true' }}
      run: |
        if [[ -f "requirements-dev.txt" ]]; then
          pip install -r requirements-dev.txt
        elif [[ -f "pyproject.toml" ]]; then
          pip install -e ".[dev]"
        else
          echo "::warning::Keine Dev-Dependencies gefunden"
        fi
      shell: bash
      working-directory: ${{ inputs.working-directory }}
    
    - name: Run linting
      id: lint
      if: ${{ inputs.run-lint == 'true' }}
      continue-on-error: true
      run: |
        if ! command -v ruff &> /dev/null; then
          pip install ruff
        fi
        ruff check .
      shell: bash
      working-directory: ${{ inputs.working-directory }}
    
    - name: Set lint result
      id: lint-result
      run: |
        if [[ "${{ inputs.run-lint }}" != "true" ]]; then
          echo "status=skipped" >> $GITHUB_OUTPUT
        elif [[ "${{ steps.lint.outcome }}" == "success" ]]; then
          echo "status=success" >> $GITHUB_OUTPUT
        else
          echo "status=failed" >> $GITHUB_OUTPUT
          echo "::warning::Linting hat Probleme gefunden"
        fi
      shell: bash
    
    - name: Summary
      run: |
        echo "### Python Setup Complete ✓" >> $GITHUB_STEP_SUMMARY
        echo "" >> $GITHUB_STEP_SUMMARY
        echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY
        echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY
        echo "| Python | ${{ inputs.python-version }} |" >> $GITHUB_STEP_SUMMARY
        echo "| Cache Hit | ${{ steps.cache.outputs.cache-hit || 'false' }} |" >> $GITHUB_STEP_SUMMARY
        echo "| Lint Status | ${{ steps.lint-result.outputs.status }} |" >> $GITHUB_STEP_SUMMARY
      shell: bash

Verwendung im Workflow:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Python environment
        id: python
        uses: ./.github/actions/python-setup
        with:
          python-version: '3.12'
          install-dev: 'true'
          run-lint: 'true'
      
      - name: Check lint status
        if: steps.python.outputs.lint-status == 'failed'
        run: echo "::warning::Bitte Lint-Fehler beheben"
      
      - name: Run tests
        run: pytest

19.10 Lokale Actions vs. Shared Actions

19.10.1 Lokale Actions (Repository-intern)

my-repo/
├── .github/
│   ├── actions/
│   │   ├── setup/
│   │   │   └── action.yml
│   │   └── deploy/
│   │       └── action.yml
│   └── workflows/
│       └── ci.yml
└── src/

Aufruf mit relativem Pfad:

steps:
  - uses: ./.github/actions/setup

Vorteile: Versionierung mit dem Code, einfaches Refactoring, keine externe Dependency.

19.10.2 Shared Actions (Repository-übergreifend)

Für organisationsweite Wiederverwendung legt man Actions in ein dediziertes Repository:

org/shared-actions/
├── setup-node/
│   └── action.yml
├── deploy-aws/
│   └── action.yml
└── notify-slack/
    └── action.yml

Aufruf mit vollem Pfad:

steps:
  - uses: org/shared-actions/setup-node@v1
  - uses: org/shared-actions/deploy-aws@v1

Wichtig: Bei Shared Actions muss eine Version (Tag, Branch, SHA) angegeben werden.

19.10.3 Versionierungsstrategie

Für Shared Actions empfiehlt sich Semantic Versioning mit Major-Tags:

# Nach Änderungen
git tag v1.2.3
git push origin v1.2.3

# Major-Tag aktualisieren (für uses: org/action@v1)
git tag -fa v1 -m "Update v1 to v1.2.3"
git push origin v1 --force

Nutzer können dann wählen: - @v1 – immer neuestes v1.x.x (bequem, minor Updates automatisch) - @v1.2.3 – exakte Version (stabil, keine Überraschungen) - @main – bleeding edge (nur für Entwicklung) - @abc1234 – SHA-Pinning (maximale Reproduzierbarkeit)

19.11 Grenzen von Composite Actions

Composite Actions haben architekturbedingte Limitierungen:

Feature Composite Action JavaScript/Docker Action
Shell-Befehle
Andere Actions aufrufen
Services (Container)
Eigene Matrix
runs-on definieren
secrets als Input ✗ (nur via env)
Post-Job Cleanup ✓ (post step)
Komplexe Logik Bash/Shell Programmiersprache

Wenn man Services, Matrix oder Post-Cleanup braucht, ist ein Reusable Workflow die bessere Wahl. Für komplexe Logik, API-Interaktionen oder Zustandsmanagement sind JavaScript Actions geeigneter.

19.11.1 Secrets-Handling: Der Workaround

Composite Actions können keine Secrets als Inputs empfangen. Der Workaround: Environment Variables.

# Im Workflow
- uses: ./.github/actions/deploy
  env:
    API_KEY: ${{ secrets.API_KEY }}
    
# In der Action
runs:
  using: 'composite'
  steps:
    - name: Deploy
      run: |
        curl -H "Authorization: Bearer $API_KEY" https://api.example.com/deploy
      shell: bash

Die Environment Variable API_KEY ist im Step verfügbar, ohne dass sie als Input definiert sein muss.

19.12 Testing von Composite Actions

19.12.1 Lokales Testing mit act

Das Tool act führt GitHub Actions lokal aus:

# Installation (macOS)
brew install act

# Workflow lokal ausführen
act -j test

# Mit spezifischem Event
act push

# Secrets übergeben
act -s API_KEY=test123

Einschränkung: act simuliert nicht alle GitHub-Features perfekt. Es eignet sich für Smoke Tests, ersetzt aber keine echten Workflow-Runs.

19.12.2 Test-Workflow für Actions

Ein dedizierter Test-Workflow validiert die Action bei Änderungen:

# .github/workflows/test-action.yml
name: Test Composite Action

on:
  push:
    paths:
      - '.github/actions/python-setup/**'
  pull_request:
    paths:
      - '.github/actions/python-setup/**'

jobs:
  test-defaults:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/python-setup
      - run: python --version
  
  test-custom-version:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/python-setup
        with:
          python-version: '3.10'
      - run: |
          VERSION=$(python --version)
          [[ "$VERSION" == *"3.10"* ]] || exit 1
  
  test-with-lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/python-setup
        id: setup
        with:
          run-lint: 'true'
      - run: |
          echo "Lint status: ${{ steps.setup.outputs.lint-status }}"
  
  test-matrix:
    strategy:
      matrix:
        os: [ubuntu-latest, macos-latest, windows-latest]
        python: ['3.10', '3.11', '3.12']
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: ./.github/actions/python-setup
        with:
          python-version: ${{ matrix.python }}
      - run: python --version

19.13 Checkliste für produktionsreife Composite Actions

Bevor eine Composite Action in der Organisation verbreitet wird:

Dokumentation - [ ] Aussagekräftige description in action.yml - [ ] Alle Inputs dokumentiert mit sinnvollen Defaults - [ ] README.md mit Beispielen (für Shared Actions) - [ ] Changelog bei Versionierung

Robustheit - [ ] Input-Validierung mit hilfreichen Fehlermeldungen - [ ] Sinnvolle Defaults für optionale Inputs - [ ] shell: bash bei allen run-Steps - [ ] Cross-Platform getestet (wenn relevant)

Observability - [ ] echo "::group::" für lange Outputs - [ ] Warnings statt Errors bei nicht-kritischen Problemen - [ ] Step Summary für wichtige Ergebnisse - [ ] Debug-Output bei debug: 'true'

Wartbarkeit - [ ] Test-Workflow vorhanden - [ ] Semantic Versioning (für Shared Actions) - [ ] Keine hardcodierten Pfade oder Werte - [ ] Abhängigkeiten (andere Actions) mit Version gepinnt