Zum Inhalt springen

Entwickler-Workflows mit GitHub Actions automatisieren

Praktische GitHub-Actions-Muster für CI/CD, Code-Qualitätsprüfungen, Dependency-Updates und Release-Workflows — mit wiederverwendbaren Workflows.

4 Min. Lesezeit
GitHub-Actions-Workflow-Diagramm mit parallelen CI-Jobs und Deployment-Stufen

GitHub Actions begann als einfaches CI-Tool, ist aber inzwischen eine vollwertige Plattform zur Workflow-Automatisierung. Über das Ausführen von Tests hinaus nutzen Teams es, um Codequalität durchzusetzen, Releases zu automatisieren, Dependencies aktuell zu halten und Multi-Service-Deployments zu koordinieren. Entscheidend ist, die Muster zu verstehen, die Workflows auch beim Wachsen wartbar halten.

Die meisten Teams starten mit einer einzigen Workflow-Datei, die alles erledigt. Das funktioniert — bis es nicht mehr funktioniert. Dieser Guide behandelt die Muster, die skalieren.

Basis-CI-Pipeline

Jedes Repository braucht eine CI-Pipeline, die bei Pull Requests läuft. Fang einfach an und füge Komplexität nur hinzu, wenn sie nötig ist.

ymlyaml
# .github/workflows/ci.yml
name: CI
 
on:
  pull_request:
    branches: [main]
  push:
    branches: [main]
 
jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm run lint
 
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
      - run: npm test -- --coverage
      - uses: actions/upload-artifact@v4
        with:
          name: coverage-report
          path: coverage/

Lint und Test laufen parallel — schlägt das Linting fehl, siehst du das sofort, ohne auf die Tests zu warten. Beide Jobs cachen node_modules über den eingebauten Cache der setup-node-Action.

Matrix-Builds für Tests über mehrere Umgebungen

Teste mit Matrix-Strategien über mehrere Node-Versionen, Betriebssysteme oder Datenbankversionen hinweg.

ymlyaml
# ❌ Separate jobs for each environment — repetitive and hard to maintain
jobs:
  test-node-18:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 18
      - run: npm ci && npm test
 
  test-node-20:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm test
ymlyaml
# ✅ Matrix strategy — one job definition, multiple environments
jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        node-version: [18, 20, 22]
        os: [ubuntu-latest, windows-latest]
      fail-fast: false  # Don't cancel other jobs if one fails
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npm test

fail-fast: false ist wichtig — ohne diese Option bricht ein Fehler in einer Kombination alle laufenden Jobs ab. Du willst alle Fehler auf einmal sehen, nicht einen nach dem anderen beheben.

Wiederverwendbare Workflows

Wenn mehrere Repositories dieselbe CI-Pipeline brauchen, verteilt sich dupliziertes YAML über die Repos und läuft auseinander. Wiederverwendbare Workflows lösen dieses Problem.

ymlyaml
# .github/workflows/reusable-node-ci.yml (in a shared repository)
name: Reusable Node CI
 
on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'
      run-e2e:
        required: false
        type: boolean
        default: false
    secrets:
      NPM_TOKEN:
        required: false
 
jobs:
  build-and-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
          registry-url: 'https://registry.npmjs.org'
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
      - run: npm ci
      - run: npm run build
      - run: npm test
 
  e2e:
    if: ${{ inputs.run-e2e }}
    runs-on: ubuntu-latest
    needs: build-and-test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}
          cache: 'npm'
      - run: npm ci
      - run: npx playwright install --with-deps
      - run: npm run test:e2e
ymlyaml
# .github/workflows/ci.yml (in consuming repositories)
name: CI
 
on:
  pull_request:
    branches: [main]
 
jobs:
  ci:
    uses: my-org/shared-workflows/.github/workflows/reusable-node-ci.yml@main
    with:
      node-version: '20'
      run-e2e: true
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Änderungen am gemeinsamen Workflow werden an alle Repositories weitergegeben, die ihn referenzieren. Pinne ihn in Produktions-Repos für Stabilität auf einen bestimmten SHA oder Tag.

Automatisierter Release-Workflow

Automatisiere Version-Bumps und Changelog-Generierung auf Basis von Conventional Commits.

ymlyaml
# .github/workflows/release.yml
name: Release
 
on:
  push:
    branches: [main]
 
permissions:
  contents: write
  pull-requests: write
 
jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0  # Need full history for changelog
 
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
          registry-url: 'https://registry.npmjs.org'
 
      - run: npm ci
      - run: npm run build
      - run: npm test
 
      - name: Determine version bump
        id: version
        run: |
          # Check commit messages since last tag
          LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0")
          COMMITS=$(git log ${LAST_TAG}..HEAD --pretty=format:"%s")
 
          if echo "$COMMITS" | grep -q "^feat!:\|^BREAKING CHANGE:"; then
            echo "bump=major" >> "$GITHUB_OUTPUT"
          elif echo "$COMMITS" | grep -q "^feat:"; then
            echo "bump=minor" >> "$GITHUB_OUTPUT"
          else
            echo "bump=patch" >> "$GITHUB_OUTPUT"
          fi
 
      - name: Bump version and create tag
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          npm version ${{ steps.version.outputs.bump }} -m "chore: release v%s"
          git push --follow-tags
 
      - name: Publish to npm
        run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Bedingte Workflows mit Pfad-Filtern

Große Monorepos sollten nicht bei jeder Änderung alle Checks ausführen. Pfad-Filter sorgen dafür, dass nur relevante Workflows ausgelöst werden.

ymlyaml
# ❌ Every PR runs the full pipeline regardless of what changed
on:
  pull_request:
    branches: [main]
ymlyaml
# ✅ Only run when relevant files change
on:
  pull_request:
    branches: [main]
    paths:
      - 'src/**'
      - 'package.json'
      - 'package-lock.json'
      - '.github/workflows/ci.yml'
 
# Also useful: ignore paths
on:
  pull_request:
    branches: [main]
    paths-ignore:
      - 'docs/**'
      - '*.md'
      - '.vscode/**'

Verwende für Monorepo-Services separate Workflow-Dateien mit Pfad-Filtern pro Service. Eine Änderung an services/auth/** löst nur die Pipeline des Auth-Services aus.

Caching und Performance

CI-Geschwindigkeit wirkt sich direkt auf die Entwicklerproduktivität aus. Cache aggressiv und parallelisiere alles, was möglich ist.

ymlyaml
jobs:
  install:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'
      - run: npm ci
 
      # Cache the entire node_modules for downstream jobs
      - uses: actions/cache/save@v4
        with:
          path: node_modules
          key: modules-${{ hashFiles('package-lock.json') }}
 
  lint:
    needs: install
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache/restore@v4
        with:
          path: node_modules
          key: modules-${{ hashFiles('package-lock.json') }}
      - run: npm run lint
 
  test-unit:
    needs: install
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache/restore@v4
        with:
          path: node_modules
          key: modules-${{ hashFiles('package-lock.json') }}
      - run: npm run test:unit
 
  test-integration:
    needs: install
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache/restore@v4
        with:
          path: node_modules
          key: modules-${{ hashFiles('package-lock.json') }}
      - run: npm run test:integration

Einmal installieren, dann parallel auf Lint, Unit-Tests und Integrationstests verteilen. Die Aufteilung in cache/save und cache/restore vermeidet, die Dependencies dreimal herunterzuladen.

Wichtigste Erkenntnisse

  1. Trenne Zuständigkeiten in parallele Jobs — Lint, Test und Build sollten parallel laufen, nicht sequenziell
  2. Nutze Matrix-Builds für Tests über mehrere Umgebungen und setze fail-fast: false, um alle Fehler zu sehen
  3. Extrahiere wiederverwendbare Workflows, wenn mehrere Repos dieselbe Pipeline-Logik teilen
  4. Nutze Pfad-Filter, um in Monorepos keine irrelevanten Workflows auszuführen
  5. Cache aggressiv — installiere Dependencies einmal und teile sie über den Cache zwischen parallelen Jobs
  6. Automatisiere Releases mit Conventional Commits und Version-Bump-Workflows
Wilfredo Rujel

Wilfredo Rujel

Full-Stack-Softwareentwickler

Diesen Beitrag teilenX