Das DRY-Prinzip (Don’t Repeat Yourself) gilt in der Softwareentwicklung als Grundregel – und CI/CD-Pipelines sind da keine Ausnahme. Wer dieselben Build-, Test- und Deployment-Schritte über dutzende Repositories hinweg dupliziert, schafft eine Wartungshölle. GitHub Actions bietet drei komplementäre Mechanismen zur Wiederverwendung: Reusable Workflows für vollständige Pipeline-Templates, Composite Actions für Step-Sequenzen und lokale Actions für repository-spezifische Bausteine.
Die Kunst besteht darin zu wissen, wann man welchen Ansatz verwendet – und wie man sie kombiniert, um maximale Flexibilität bei minimaler Komplexität zu erreichen.
Ein Reusable Workflow ist eine YAML-Datei, die nicht durch Events wie
push oder pull_request getriggert wird,
sondern durch workflow_call. Sie definiert einen oder
mehrere Jobs, die von anderen Workflows aufgerufen werden können –
ähnlich einer Funktion, die von anderen Funktionen aufgerufen wird.
Der Unterschied zu normalen Workflows: Reusable Workflows leben in
.github/workflows/ (genau wie normale Workflows), werden
aber nicht automatisch ausgeführt. Sie warten darauf, von einem
Caller-Workflow referenziert zu werden.
# .github/workflows/build-and-test.yml
name: Build and Test Template
on:
workflow_call:
inputs:
node-version:
required: true
type: string
run-integration-tests:
required: false
type: boolean
default: false
secrets:
npm-token:
required: true
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- run: npm ci
env:
NPM_TOKEN: ${{ secrets.npm-token }}
- run: npm run build
- run: npm test
integration-test:
if: ${{ inputs.run-integration-tests }}
needs: build
runs-on: ubuntu-latest
steps:
- run: npm run test:integrationDieser Reusable Workflow definiert zwei Inputs
(node-version, run-integration-tests) und ein
Secret (npm-token). Er führt immer Unit-Tests aus,
Integrationstests nur optional.
Ein Caller-Workflow referenziert den Reusable Workflow auf Job-Ebene
mit uses:
# .github/workflows/ci.yml
name: CI Pipeline
on:
pull_request:
push:
branches: [main]
jobs:
test-node-18:
uses: ./.github/workflows/build-and-test.yml
with:
node-version: '18'
run-integration-tests: true
secrets:
npm-token: ${{ secrets.NPM_TOKEN }}Die Syntax ist kompakt: uses auf Job-Ebene (nicht
Step-Ebene!), with für Inputs, secrets für
Secrets. Der Pfad ./.github/workflows/build-and-test.yml
referenziert eine Datei im selben Repository. Für andere
Repositories:
uses: myorg/shared-workflows/.github/workflows/build-and-test.yml@v2Das @v2 ist eine Git-Referenz – ein Tag, Branch oder
SHA. Best Practice: SHA-Pinning für kritische
Workflows, Tags für einfachere Updates.
Secrets können explizit übergeben werden (wie oben) oder mit
secrets: inherit alle verfügbaren Secrets
weitergegeben:
jobs:
test:
uses: ./.github/workflows/build-and-test.yml
with:
node-version: '20'
secrets: inheritDas ist bequem, birgt aber ein Risiko: Der Reusable Workflow erhält Zugriff auf alle Secrets des Callers, auch solche, die er gar nicht braucht. In Organizations mit strikten Security-Policies sollte man Secrets explizit übergeben.
Wichtig bei Nesting: Secrets werden nicht automatisch transitiv weitergegeben. In einer Kette A → B → C muss A die Secrets an B übergeben, und B muss sie explizit an C weiterleiten:
# Workflow A
jobs:
call-b:
uses: org/repo/.github/workflows/b.yml@main
secrets: inherit
# Workflow B
jobs:
call-c:
uses: org/repo/.github/workflows/c.yml@main
secrets:
token: ${{ secrets.SOME_TOKEN }} # explizite WeiterleitungReusable Workflows können Outputs definieren, die der Caller nutzen kann. Das erfordert ein zweistufiges Mapping: Step-Output → Job-Output → Workflow-Output.
# Reusable Workflow
on:
workflow_call:
outputs:
build-version:
description: "Version of the build artifact"
value: ${{ jobs.build.outputs.version }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.get-version.outputs.version }}
steps:
- id: get-version
run: echo "version=$(cat package.json | jq -r .version)" >> $GITHUB_OUTPUTDer Caller greift darauf zu wie auf Job-Outputs:
jobs:
build:
uses: ./.github/workflows/build.yml
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- run: echo "Deploying version ${{ needs.build.outputs.build-version }}"Matrix-Jobs können Reusable Workflows aufrufen. Das ist mächtig für Cross-Platform-Tests:
jobs:
test-matrix:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [18, 20, 22]
uses: ./.github/workflows/test.yml
with:
os: ${{ matrix.os }}
node-version: ${{ matrix.node }}Jede Kombination von OS und Node-Version führt den Reusable Workflow einmal aus. Das Resultat: 9 Jobs aus einer Zeile YAML.
Reusable Workflows können andere Reusable Workflows aufrufen – bis zu 10 Ebenen tief. Das ist mehr als ausreichend für die meisten Szenarien, aber es gibt eine wichtige Einschränkung: Permissions können nur reduziert, nie erhöht werden.
Workflow C kann nicht mehr Permissions haben als Workflow B. Die Chain kann nur restriktiver werden, nie permissiver.
Während Reusable Workflows auf Job-Ebene operieren und mehrere Jobs enthalten können, arbeiten Composite Actions auf Step-Ebene. Eine Composite Action ist eine Sequenz von Steps, die als einzelner Step in einem Job aufgerufen wird.
Die action.yml einer Composite Action sieht so aus:
name: Setup Node Environment
description: Installs Node, restores cache, and installs dependencies
inputs:
node-version:
description: Node.js version
required: true
cache-dependency-path:
description: Path to package-lock.json
required: false
default: '**/package-lock.json'
runs:
using: composite
steps:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
cache-dependency-path: ${{ inputs.cache-dependency-path }}
- name: Install dependencies
shell: bash
run: npm ci
- name: Verify installation
shell: bash
run: |
echo "Node version: $(node --version)"
echo "NPM version: $(npm --version)"Das runs.using: composite ist der entscheidende Marker.
Composite Actions können run-Commands oder andere Actions
aufrufen, aber keine Jobs definieren.
Composite Actions können lokal im selben Repository oder shared in einem separaten Repository leben.
Lokale Action (im selben Repo):
my-repo/
├── .github/
│ ├── actions/
│ │ └── setup-env/
│ │ └── action.yml
│ └── workflows/
│ └── ci.yml
Aufruf im Workflow:
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-env
with:
node-version: '20'Shared Action (in anderem Repo):
steps:
- uses: myorg/shared-actions/setup-env@v1
with:
node-version: '20'Der Vorteil lokaler Actions: Sie versionieren automatisch mit dem Repository. Kein Dependency-Management notwendig. Der Nachteil: Keine Wiederverwendung über Repository-Grenzen hinweg.
Composite Actions können Inputs empfangen und Outputs zurückgeben:
name: Build and Extract Version
inputs:
build-command:
required: true
outputs:
version:
description: Extracted version
value: ${{ steps.extract.outputs.version }}
runs:
using: composite
steps:
- name: Build
shell: bash
run: ${{ inputs.build-command }}
- name: Extract version
id: extract
shell: bash
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUTWichtig: Bei run-Steps muss immer
shell angegeben werden. Das ist eine Besonderheit von
Composite Actions – normale Steps erben die Shell vom Job, Composite
Actions nicht.
Im Vergleich zu Reusable Workflows haben Composite Actions Limitierungen:
| Feature | Reusable Workflow | Composite Action |
|---|---|---|
| Mehrere Jobs | ✓ | – |
Runner-Auswahl (runs-on) |
✓ | – |
| Services (z.B. Datenbanken) | ✓ | – |
| Matrix-Strategien | ✓ | – |
if-Bedingungen auf Job-Ebene |
✓ | – |
| Secrets als Input | ✓ | – (nur via Env) |
Composite Actions sind einfacher, aber weniger mächtig. Sie sind ideal für Step-Sequenzen, die sich oft wiederholen, aber keine Job-Level-Features brauchen.
Wann verwendet man was? Die folgende Tabelle gibt Orientierung:
| Szenario | Empfehlung | Begründung |
|---|---|---|
| Komplette CI/CD-Pipeline über Repos hinweg | Reusable Workflow | Job-Orchestrierung, Services, Matrix |
| Setup-Schritte (Node, Cache, Install) | Composite Action | Step-Sequenz, lokal oder shared |
| Repository-spezifische Helfer | Lokale Composite Action | Versioniert mit Repo, kein Ext-Dependency |
| Cross-Platform-Tests | Reusable Workflow + Matrix | Braucht runs-on-Kontrolle |
| Deployment-Template | Reusable Workflow | Environment-Schutz, Secrets |
| Code-Linting/Formatting | Composite Action | Reine Step-Sequenz |
Eine Faustregel: Sobald du runs-on,
services oder mehrere Jobs brauchst, nutze einen Reusable
Workflow. Für alles andere sind Composite Actions
einfacher.
Ein häufiges Pattern: Ein Reusable Workflow für die Pipeline-Struktur, eine lokale Composite Action für projekt-spezifische Setup-Schritte.
# Shared: org/workflows/.github/workflows/test-suite.yml
on:
workflow_call:
inputs:
language:
required: true
type: string
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/setup-project # lokale Action!
with:
language: ${{ inputs.language }}
- run: make testDer Reusable Workflow ist generisch, die lokale Action projekt-spezifisch. Das Beste aus beiden Welten.
Manchmal will man eine bestehende Action mit org-spezifischen Defaults wrappen:
# .github/actions/setup-node-with-defaults/action.yml
name: Setup Node (Org Defaults)
inputs:
node-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
env:
NODE_AUTH_TOKEN: ${{ github.token }}
- shell: bash
run: |
npm config set @myorg:registry https://npm.pkg.github.com
npm ciJedes Projekt kann jetzt einfach
uses: ./.github/actions/setup-node-with-defaults nutzen,
ohne sich um Registry-Config zu kümmern.
Für komplexe Pipelines: Mehrere Reusable Workflows, die aufeinander aufbauen.
jobs:
build:
uses: org/workflows/.github/workflows/build.yml@v2
with:
artifact-name: app-${{ github.sha }}
test:
needs: build
uses: org/workflows/.github/workflows/test.yml@v2
with:
artifact-name: app-${{ github.sha }}
deploy-staging:
needs: test
if: github.ref == 'refs/heads/main'
uses: org/workflows/.github/workflows/deploy.yml@v2
with:
environment: staging
artifact-name: app-${{ github.sha }}Jeder Stage ist ein separater Reusable Workflow. Die Orchestrierung passiert im Caller. Das ist wartbar, testbar und erlaubt flexibles Überschreiben.
Mit Conditional Logic kann man verschiedene Reusable Workflows je nach Kontext aufrufen:
jobs:
determine-deployment:
runs-on: ubuntu-latest
outputs:
workflow: ${{ steps.decide.outputs.workflow }}
steps:
- id: decide
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "workflow=deploy-prod.yml" >> $GITHUB_OUTPUT
else
echo "workflow=deploy-preview.yml" >> $GITHUB_OUTPUT
fi
deploy:
needs: determine-deployment
uses: org/workflows/.github/workflows/${{ needs.determine-deployment.outputs.workflow }}@v2Achtung: Der uses-Pfad kann keine
Expression mit Runtime-Werten enthalten – nur Kontext-Variablen, die zur
Parse-Zeit bekannt sind. Das obige Beispiel funktioniert
nicht. Stattdessen:
jobs:
deploy-prod:
if: github.ref == 'refs/heads/main'
uses: org/workflows/.github/workflows/deploy-prod.yml@v2
deploy-preview:
if: github.ref != 'refs/heads/main'
uses: org/workflows/.github/workflows/deploy-preview.yml@v2Conditional Jobs, nicht Dynamic Paths.
Shared Composite Actions und Reusable Workflows sollten nach Semantic Versioning versioniert werden. Das bedeutet:
v1, v2) für Breaking
Changesv1.2) für neue Featuresv1.2.3) für BugfixesDer Workflow:
main mit konventionellen Commitsv1.2.3v1 auf denselben CommitNutzer können dann entscheiden:
@v1 – immer die neueste v1.x.x (auto-update)@v1.2 – immer die neueste v1.2.x (minor-update)@v1.2.3 – fixe Version (kein auto-update)@abc123 – SHA-Pinning (maximal stabil)Wenn eine Action oder ein Reusable Workflow deprecated wird:
Dokumentiere in der README und im Changelog
Füge Warnings hinzu mit
::warning::-Commands:
steps:
- name: Deprecation Warning
run: |
echo "::warning::This action is deprecated. Use new-action@v2 instead."Setze ein EOL-Datum (z.B. 6 Monate)
Kommuniziere über Issues, Slack, Internal Docs
Bei Breaking Changes (Major-Version-Bump):
# CHANGELOG.md
## v2.0.0 (2025-01-15)
### BREAKING CHANGES
- Removed `legacy-mode` input (deprecated since v1.5)
- Changed default `node-version` from '16' to '20'
### Migration Guide
**Before (v1.x):**
```yaml
uses: org/action@v1
with:
legacy-mode: true
node-version: '16'After (v2.x):
uses: org/action@v2
with:
node-version: '20'Ein guter Changelog ist die halbe Miete. Die andere Hälfte: Automatisierte Tests für alle Major-Versionen.
Viele Organizations zentralisieren ihre Reusable Workflows in einem Monorepo:
shared-workflows/
├── .github/
│ ├── workflows/
│ │ ├── build-node.yml
│ │ ├── build-python.yml
│ │ ├── deploy.yml
│ │ └── security-scan.yml
│ └── actions/
│ ├── setup-cloud-cli/
│ └── notify-slack/
├── docs/
└── README.md
Das ermöglicht:
Reusable Workflows und Composite Actions sollten getestet werden, bevor sie released werden:
# .github/workflows/test-reusable.yml
name: Test Reusable Workflows
on:
pull_request:
paths:
- '.github/workflows/**'
jobs:
test-build-workflow:
uses: ./.github/workflows/build-node.yml
with:
node-version: '20'
secrets: inheritJede Änderung an einem Reusable Workflow triggert einen Test-Run. Das verhindert Breaking Changes.
Composite Actions haben oft externe Dependencies (andere Actions). Diese sollten gepinnt werden:
# ❌ Schlecht: Unpinned
steps:
- uses: actions/checkout@v4
# ✓ Besser: SHA-Pinned
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1Tools wie Dependabot können Actions automatisch updaten:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"Die drei Mechanismen – Reusable Workflows, Composite Actions, lokale Actions – sind keine Konkurrenten, sondern Werkzeuge für unterschiedliche Abstraktionsebenen.
Reusable Workflows sind die Makro-Ebene: vollständige Pipeline-Templates, die Job-Orchestrierung, Runner-Auswahl und Service-Container ermöglichen. Sie sind ideal für standardisierte CI/CD-Flows über Teams und Repositories hinweg.
Composite Actions sind die Mikro-Ebene: wiederverwendbare Step-Sequenzen, die komplexe Setup-Prozeduren kapseln. Sie sind leichtgewichtiger als Reusable Workflows und können lokal oder shared sein.
Lokale Actions sind die Projekt-Ebene: repository-spezifische Helfer, die mit dem Code versionieren. Sie vermeiden externe Dependencies und sind perfekt für Projektbesonderheiten.
Die Kunst liegt in der Kombination: Ein Reusable Workflow orchestriert die Pipeline, ruft Composite Actions für Standard-Setups auf und erlaubt lokale Actions für Projekt-Spezifika. Das Ergebnis: DRY, wartbar, skalierbar – und trotzdem flexibel genug für individuelle Anforderungen.