Zurück zum Blog
Juli 2026·3 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

Deine erste GitHub Action schreiben

#ci-cd#github#devops

Die meisten Teams starten mit GitHub Actions, indem sie einen Workflow aus einem anderen Repo kopieren und hoffen, dass er noch passt. Das funktioniert, bis es nicht mehr funktioniert und man tatsächlich verstehen muss, was einen Lauf auslöst, was ein Job und ein Step sind und wie man Daten zwischen ihnen weiterreicht. Hier wird ein echter Workflow von Grund auf gebaut – Tests bei jedem Pull Request ausführen und ein Paket bei getaggten Releases veröffentlichen.

Die Anatomie eines Workflows

Ein Workflow ist eine YAML-Datei in .github/workflows/. Er hat drei Kernbestandteile: Trigger (on) bestimmen, wann er läuft, Jobs sind Gruppen von Steps, die jeweils auf einer eigenen virtuellen Maschine laufen, und Steps sind die einzelnen Befehle oder Actions innerhalb eines Jobs.

name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm test

actions/checkout@v4 klont dein Repo auf den Runner – ohne es ist der Runner eine leere VM ohne Zugriff auf deinen Code. actions/setup-node installiert eine bestimmte Node-Version. npm ci (nicht npm install) ist in CI wichtig: Es installiert genau das, was in package-lock.json steht, und scheitert, wenn die Lockfile nicht synchron ist, statt sie stillschweigend zu aktualisieren.

Jobs parallel mit einer Matrix ausführen

Muss man gegen mehrere Node-Versionen oder Betriebssysteme testen, erweitert eine Matrix eine Job-Definition zu mehreren parallelen Läufen:

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        node-version: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
      - run: npm ci
      - run: npm test

Das führt drei unabhängige Jobs aus, einen pro Node-Version, alle parallel. ${{ matrix.node-version }} ist die Ausdruckssyntax von GitHub Actions – sie setzt den aktuellen Matrixwert in den Step ein.

Abhängigkeiten cachen

Jeder Job startet auf einer frischen VM, daher lädt npm ci bei jedem Lauf jedes Paket neu herunter, sofern man sie nicht cached:

- uses: actions/setup-node@v4
  with:
    node-version: '20'
    cache: 'npm'

Die eingebaute cache-Option von setup-node erledigt das für npm, yarn und pnpm – sie bindet den Cache an den Hash deiner Lockfile, sodass ein Cache-Treffer nur entsteht, wenn sich Abhängigkeiten nicht geändert haben.

Daten zwischen Steps weiterreichen

Steps im selben Job teilen ein Dateisystem, aber keine Variablen – um einen Wert von einem Step an den nächsten weiterzugeben, schreibt man in eine spezielle Ausgabedatei:

steps:
  - id: get_version
    run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT"

  - run: echo "Building version ${{ steps.get_version.outputs.version }}"

Das id: get_version gibt dem Step einen Namen, auf den man später als steps.get_version.outputs.version verweisen kann. Dieses Muster – etwas lesen, es in $GITHUB_OUTPUT schreiben, weiter unten darauf verweisen – ist, wie die meisten nichttrivialen Workflows Daten bewegen.

Secrets

Hinterlege niemals Zugangsdaten direkt in einer Workflow-Datei – sie ist im Repo eingecheckt und für jeden mit Lesezugriff sichtbar. Speichere sie unter Repo Settings → Secrets and variables → Actions und verweise über den secrets-Kontext darauf:

- name: Publish package
  run: npm publish
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Secrets werden in Logs automatisch maskiert – gibt ein Step versehentlich eines aus, ersetzt GitHub es in der Ausgabe durch ***.

Nur bei getaggten Releases auslösen

Kombiniere on.push.tags mit einem bedingten Job, um nur dann zu veröffentlichen, wenn du einen Versions-Tag pushst, während Tests weiterhin bei jedem Push laufen:

name: Release

on:
  push:
    tags:
      - 'v*'

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
          registry-url: 'https://registry.npmjs.org'
          cache: 'npm'
      - run: npm ci
      - run: npm test
      - run: npm publish
        env:
          NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

Das Pushen eines Tags wie v1.2.0 führt diesen Workflow aus; ein normaler Commit auf main nicht, da der Trigger auf tags begrenzt ist, nicht auf branches.

if:-Bedingungen für feinere Kontrolle

Manchmal ist ein Trigger allein nicht präzise genug. Mit if: lässt sich ein Step oder Job an einen Ausdruck knüpfen:

- name: Notify on failure
  if: failure()
  run: echo "Build failed on ${{ github.ref }}"

failure() ist eine von mehreren eingebauten Funktionen (neben success(), always(), cancelled()), die das Ergebnis vorheriger Steps im Job prüfen – nützlich, um eine Benachrichtigung nur dann zu senden, wenn etwas kaputtgeht, ohne einen separaten Workflow.

Einen Workflow debuggen

Zwei Dinge sparen am meisten Zeit, wenn ein Workflow sich daneben benimmt:

  • act – führt GitHub-Actions-Workflows lokal in Docker aus, sodass man iterieren kann, ohne für jeden Tippfehler einen Commit zu pushen.
  • Der workflow_dispatch-Trigger – füge on: workflow_dispatch zu jedem Workflow hinzu, um in der GitHub-Oberfläche einen „Run workflow"-Button zu bekommen, mit dem man ihn manuell ohne neuen Push erneut ausführen kann:
on:
  workflow_dispatch:
  push:
    branches: [main]

Fazit

Ein GitHub-Actions-Workflow ist nur YAML, das Trigger, Jobs und Steps beschreibt, ausgeführt auf kurzlebigen VMs, die mit nichts starten außer dem, was actions/checkout ihnen gibt. Sobald man on:, jobs:, steps: und die ${{ }}-Ausdruckssyntax flüssig lesen kann, sieht jeder online gefundene Workflow nicht mehr nach Magie aus, sondern nach einer Konfigurationsdatei, die man tatsächlich ändern kann.

NeuerDatenbank-Connection-Pooling verstehenÄlterDie JavaScript-Event-Loop verstehen