GitHub Actions Grundlagen: Workflow, Job und Step

1. Übersicht

GitHub Actions ist ein in GitHub integriertes CI/CD-Tool, mit dem du Build, Test und Deployment automatisieren kannst, ohne einen separaten CI-Server zu benötigen. Dieser Artikel erklärt drei Kernkonzepte: workflow, job und step, mit praktischen Beispielen für ein Spring-Boot-Projekt mit Maven.

Nach dem Lesen wirst du in der Lage sein, eine Workflow-Datei zu schreiben, die automatisch Tests ausführt, wenn du Code pushst. Du wirst verstehen, wie Jobs parallel oder sequenziell ausgeführt werden, und wie du fehlgeschlagene Workflows debuggst.

2. Was ist ein Workflow?

Ein Workflow ist eine YAML-Datei, die sich im Verzeichnis .github/workflows/ deines Repositorys befindet. Jeder Workflow definiert, wann er ausgelöst wird (Trigger) und was er tut (Jobs).

# .github/workflows/ci.yml
name: CI Pipeline

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

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4
      - name: Set up JDK 21
        uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Build with Maven
        run: mvn -B package

Erklärung:

  • name am Anfang der Datei ist der Name, der auf der Registerkarte Actions von GitHub angezeigt wird.
  • on deklariert den Trigger: hier wird der Workflow ausgelöst, wenn ein push oder pull_request zum Branch main erfolgt.
  • jobs.build ist ein einzelner Job namens build, der auf der virtuellen Maschine ubuntu-latest läuft.

Hinweis: Die Datei muss im Pfad .github/workflows/*.yml liegen. Wenn das Verzeichnis falsch ist, erkennt GitHub den Workflow nicht.

3. Job: unabhängige Ausführungseinheit

Ein Workflow kann mehrere Jobs haben. Jeder Job läuft auf einem separaten Runner (virtuelle Maschine). Standardmäßig werden Jobs parallel ausgeführt, es sei denn, du deklarierst needs.

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: mvn -B test

  deploy:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - run: echo "Deploying application..."

Erklärung:

  • test und deploy sind zwei separate Jobs; jeder Job hat eine saubere Runner-Umgebung und teilt keine Dateien mit anderen.
  • needs: test zwingt deploy dazu, auf die erfolgreiche Ausführung von test zu warten — dadurch entsteht eine sequenzielle Abhängigkeit.
  • Ohne needs würde GitHub Actions test und deploy gleichzeitig ausführen.

3.1. Datenaustausch zwischen Jobs

Da jeder Job seinen eigenen Runner hat, musst du artifacts verwenden, um Dateien zwischen Jobs zu übertragen.

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: mvn -B package
      - uses: actions/upload-artifact@v4
        with:
          name: app-jar
          path: target/*.jar

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - uses: actions/download-artifact@v4
        with:
          name: app-jar

Erklärung:

  • upload-artifact speichert die .jar-Datei vom Job build im temporären GitHub-Speicher.
  • download-artifact im Job deploy ruft die Datei auf den neuen Runner ab.
  • Dies ist der einzige Weg, um Daten zwischen Jobs zu übertragen (normale Umgebungsvariablen funktionieren nicht).

4. Step: die kleinste Handlungseinheit

Ein Step ist eine spezifische Aktion innerhalb eines Jobs, die sequenziell von oben nach unten ausgeführt wird. Es gibt zwei Haupttypen von Steps: uses (um vordefinierte Actions aufzurufen) oder run (um Shell-Befehle auszuführen).

steps:
  - name: Checkout code
    uses: actions/checkout@v4

  - name: Cache Maven dependencies
    uses: actions/cache@v4
    with:
      path: ~/.m2/repository
      key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}

  - name: Run tests
    run: mvn -B test

  - name: Print result
    run: echo "Test step finished with status ${{ job.status }}"

Erklärung:

  • Der Step Checkout code verwendet die vordefinierte Action actions/checkout, um den Quellcode auf den Runner zu bringen.
  • Der Step Cache Maven dependencies cached das Verzeichnis ~/.m2/repository, wodurch nachfolgende Builds schneller werden, da die Abhängigkeiten nicht erneut heruntergeladen werden müssen.
  • Der key des Caches basiert auf dem Hash der Datei pom.xml — wenn sich pom.xml ändert, wird der alte Cache verworfen und ein neuer erstellt.
  • Der letzte Step verwendet run, um normale Shell-Befehle auszuführen. Es können Kontextreferenzen wie ${{ job.status }} verwendet werden.

5. Umgebungsvariablen und Secrets

Oft musst du vertrauliche Konfigurationen (API-Schlüssel, Tokens) an den Workflow übergeben, ohne sie hartcodiert in der YAML-Datei zu speichern.

jobs:
  deploy:
    runs-on: ubuntu-latest
    env:
      SPRING_PROFILES_ACTIVE: prod
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to server
        env:
          DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
        run: |
          echo "Deploying with profile $SPRING_PROFILES_ACTIVE"
          curl -H "Authorization: Bearer $DEPLOY_TOKEN" https://api.example.com/deploy

Erklärung:

  • env auf Job-Ebene gilt für alle Steps in diesem Job (SPRING_PROFILES_ACTIVE).
  • env auf Step-Ebene gilt nur für diesen Step (DEPLOY_TOKEN).
  • secrets.DEPLOY_TOKEN wird aus Settings → Secrets and variables → Actions des Repositorys abgerufen und zeigt niemals den tatsächlichen Wert im Log.

Hinweis: Gib einen Secret-Wert nie direkt mit echo $DEPLOY_TOKEN ins Log aus — GitHub wird ihn zwar automatisch maskieren, aber es ist besser, Secrets gar nicht erst auszugeben.

6. Matrix: mehrere Versionen gleichzeitig ausführen

Wenn du auf mehreren Java-Versionen oder Betriebssystemen testen musst, hilft dir matrix, wiederholte Job-Definitionen zu vermeiden.

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        java: [17, 21]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: ${{ matrix.java }}
          distribution: 'temurin'
      - run: mvn -B test

Erklärung:

  • strategy.matrix.java deklariert eine Liste von Werten [17, 21].
  • GitHub Actions erstellt automatisch 2 Jobs parallel, jeder mit einer entsprechenden Java-Version.
  • Dies spart wiederholtes YAML-Schreiben und ist ideal, um sicherzustellen, dass der Code in mehreren Umgebungen funktioniert.

7. Workflow-Tests mit einfachen Tests

Workflows werden nicht direkt mit JUnit getestet, aber du solltest sicherstellen, dass der Schritt mvn test im Workflow die tatsächliche Test-Suite des Projekts ausführt. Hier ist ein einfaches Test-Beispiel für OrderService, das von CI ausgeführt wird:

class OrderServiceTest {

    private final OrderService orderService = new OrderService();

    @Test
    void shouldCalculateTotalPriceCorrectly() {
        Order order = new Order("ORD-001", List.of(
                new OrderItem("Keyboard", 2, BigDecimal.valueOf(50)),
                new OrderItem("Mouse", 1, BigDecimal.valueOf(20))
        ));

        BigDecimal total = orderService.calculateTotal(order);

        assertThat(total).isEqualByComparingTo(BigDecimal.valueOf(120));
    }
}

Erklärung:

  • Dieser Test befindet sich in src/test/java und wird von mvn -B test im Workflow-Step automatisch ausgeführt.
  • Wenn ein Test fehlschlägt, signalisiert der Step Run tests einen Fehler (Exit-Code ≠ 0), der Job stoppt und abhängige Jobs (needs) werden nicht ausgeführt.
  • Dies ist, warum CI wertvoll ist: fehlerhafter Code wird blockiert, bevor er in main gemergt wird.

Ergebnis bei Ausführung in der GitHub Actions UI (Registerkarte Actions):

✓ Checkout code
✓ Set up JDK 21
✓ Build with Maven
✓ Run tests (shouldCalculateTotalPriceCorrectly PASSED)

8. Häufige Fehler

  • Falsches YAML-Indent: GitHub Actions verwendet YAML. Ein falsches Einrückungsmuster um ein Leerzeichen kann dazu führen, dass Jobs oder Steps nicht erkannt werden – typischerweise mit der Fehlermeldung “Invalid workflow file”.
  • Vergessenes runs-on: Jeder Job muss runs-on haben. Ohne dieses Feld schlägt der Workflow beim Parsen fehl.
  • Verwendung von Umgebungsvariablen eines Jobs in einem anderen Job: env und Ausgabevariablen werden nicht automatisch zwischen Jobs übertragen – du musst outputs oder artifact verwenden.
  • Cache-Schlüssel ändert sich nicht, wenn sich Abhängigkeiten ändern: Wenn der key zu allgemein ist (z.B. nur maven-cache), verwendest du versehentlich einen alten Cache mit veralteten Abhängigkeiten.
  • Ausführung von schweren Workflows bei jedem Push: Wenn du paths oder branches nicht einschränkst, triggert jeder kleine Commit (z.B. README-Änderung) die gesamte Build- und Test-Pipeline – das verschlingt kostenlose Actions-Minuten (es gibt ein Limit).

Hinweis: Wenn dein Projekt nur einen einfachen Build benötigt und nicht auf mehreren Umgebungen laufen muss, brauchst du weder matrix noch artifact — unnötige Komplexität macht den Workflow schwer zu warten.

9. Fazit

Ein Workflow ist eine YAML-Datei, die die gesamte Pipeline definiert. Jobs sind unabhängige Ausführungsblöcke, die auf separaten Runnern laufen, und Steps sind konkrete Aktionen innerhalb eines Jobs. Wenn du diese drei Ebenen verstehst, kannst du eine klare CI/CD-Pipeline organisieren: welche Jobs parallel laufen, welche auf andere warten müssen (needs), und wann du artifact zum Datenaustausch verwendest.

Wenn du neu anfängst, solltest du den Workflow einfach halten (ein Job mit wenigen grundlegenden Steps: checkout, Java-Setup, Test). Wenn dein Projekt wächst, füge matrix, cache hinzu und teile build/test/deploy in separate Jobs auf – das macht die Verwaltung und das Debugging einfacher.

github-actionsci-cddevopsjavaautomatisierung