Die folgende Abbildung veranschaulicht die einzelnen Schritte, die in diesem Tutorial der Reihe nach durchgeführt werden, um das angestrebte Ziel zu erreichen.

NOx13S8m34NlcS8Bi01SYX22OoKcIDiF3NLSbDXCn1YzPJ5eom5niQp tfT bTKdjSXkUPpRrccyOr2ANWpCv2DTshvCZBIf3zHuWv19HzxojKnzWkpMMiE8MS5iSYEaWSlh2qz1pzRGljKmIgRdc1PlXSSWe3 Z3W mdEF7X7Ik fnvxHi0

1. Manuelle Ausführung der Schritte

Zu Beginn bauen und starten wir das Projekt manuell, um ein besseres Verständnis dafür zu bekommen, was später automatisiert durch GitHub Actions ablaufen wird.

NOun3e9044NxFSKarbv06umnMf9uWWM i38xaDaTJ GnwBYOWXR6PZBvx ta5obxpHRPhOzpcz4pMWLaL fmHvyfUZ4mauzqhRjvOkYsF5Bb3K8c3NbBjtdg5B9tRgbLZ9Woc4rQWlOyFx m7FB5AZhKEGoIw37ewdtWAGZepoZ325vo 9YGnXNli n 0G00

1.1. Build des Backend-Images

Dieses Shell-Skript dient dazu, das Backend-Projekt zu bauen und anschließend ein Docker-Image daraus zu erstellen. Es ist ein vorbereitender Schritt, bevor das Image z.B. in einer CI/CD-Pipeline weiterverarbeitet wird.

Erklärung des Dockerfiles hier.

classroom/build-image.sh
#!/usr/bin/env bash

mvn clean package (1)

(2)
cp ./src/main/docker/Dockerfile ./target
cp ./src/main/resources/import.sql ./target

cd target
(3)
docker build -t classroom-backend:latest .
1 Führt einen vollständigen Maven-Build durch. Dabei wird das Projekt bereinigt und neu kompiliert. Das erzeugt ein lauffähiges JAR im target-Verzeichnis.
2 Kopiert die für den Docker-Build benötigte Dockerfile sowie die import.sql Datei ins target-Verzeichnis. Dadurch befinden sich alle relevanten Dateien am gleichen Ort→Dies ist wichtig, weil Docker den Kontext des Builds auf das aktuelle Verzeichnis beschränkt.
3 Wechselt ins target-Verzeichnis und baut dort das Docker-Image mit dem Tag classroom-backend:latest, basierend auf der kopierten Dockerfile und dem erstellten JAR.

1.2. Build des DB-Images

Nun machen wir das gleiche für die Datenbank. Auch hier wird ein Docker-Image erstellt.

Erklärung des Dockerfiles hier.

docker build -t classroom-db:latest .

1.3. Start des Docker-Compose zur lokalen Ausführung der Anwendung

Wenn man nun also das docker-compose.yaml File ausführt, wird die Anwendung lokal gestartet. Dabei wird das Backend-Image und das DB-Image verwendet, die wir zuvor erstellt haben.

Erklärung des docker-compose.yaml-Files hier.

docker-compose --env-file .env.dev up --build

Nun kann man unter http://localhost auf die Endpoints seiner Anwendung zugreifen.

Im nächsten Schritt würde man sich per SSH mit der VM verbinden, um die Anwendung dort manuell zu starten. Für diesen Vergleich ist das jedoch nicht zwingend erforderlich, da der Ablauf identisch ist, sobald man sich mit folgendem Befehl auf der VM befindet:

ssh -i $SSH_KEY_PATH $SSH_USER@$SSH_HOST
  • $SSH_KEY_PATH: Pfad zu deinem Private-SSH-Key für den Zugriff auf die VM

  • $SSH_USER: Benutzername für den Zugriff auf die VM (der Benutzer, der auf der sich auf der VM befindet)

  • $SSH_HOST: Hostname oder IP-Adresse der VM

Wer möchte, kann dies gerne selbst versuchen. Es müssen jedoch natürlich Docker, GitHub sowie Nginx installiert und initialisiert sein.

2. Problemstellung von GitHub Actions

Nun da wir unsere Anwendung lokal erfolgreich gestartet haben und verstehen, was dafür notwendig ist, gehen wir nun zum nächsten Punkt.

Wofür brauchen wir den GitHub Actions nun eigentlich?

NSv1pe9030NW PoYoR  Bu0RZ36w9F440pnXf5D8P g9F0OxBgR0nhXfclRBoojJzfejijiMvufAnmU6JD6BWPbyf5lJBpD3 wiJrTuWPDAIjtHVvbuX1 V 9i e590re6R4bC5iqX5quEMri 8IyjLgEZOvZ9BeEUhczkSv2EX7L642htTyIKZhoizOUxy1

2.1. Gebrauch von GitHub Actions

GitHub Actions ist ein CI/CD-Tool, das automatisierte Workflows direkt in GitHub ermöglicht, um Entwicklungsprozesse wie Tests, Builds und Bereitstellungen, wie wir es gerade gemacht haben, zu beschleunigen und zu zentralisieren. Ohne GitHub Actions sind diese Prozesse oft manuell und fehleranfälliger, was mehr Zeit und Ressourcen beansprucht.

Man möchte also den aufwändigen Prozess von eben nicht jedes Mal manuell durchführen müssen, sondern möchte dies automatisiert haben. Das ist der Grund, warum wir GitHub Actions verwenden.

Mehr Informationen zu CI/CD hier.

2.2. Alternativen zu GitHub Actions

  • Azure Pipelines

  • Jenkins

  • Circle CI

2.2.1. Kriterienkatalog: GitHub Actions vs. Jenkins, Azure Pipelines und CircleCI

criteria catalog

3. GitHub Actions Einstieg

Als Nächstes möchte ich nun mit euch ein erstes GitHub-Actions Script erstellen und die grundlegende Funktionsweise von GitHub Actions erklären und näher bringen.

NOv1pe9038NtSuec  g 06umnkYIn1CCy8IHJY6TgIVo6Eouc08RuwPfskzxUMNANhD5TjlYN57f C2GANenC9EFT6lwPMReVtMYsXk4J5hobkxBt2lai6ir8wOCPfEEe0Clh tvDtb6fu0Kbv2lLjEnoM6KHCzHDsFFE0S1 H1LcC3h7Lyaf7NbPwoztm00

Unter diesem Link befindet sich eine ausführliche Dokumentation zu GitHub Actions, die auch den strukturellen Aufbau von Workflow-YAML-Dateien behandelt.

Das folgende Skript erstellt zwei Docker-Images - eines für eine H2-Datenbank und eines für ein Maven-basiertes Java-Projekt - und pusht diese in die Container Registry ghcr.io.

Alle Workflows müssen im Repo unter dem Verzeichnis .github/workflows/ gespeichert werden.
Pipeline des GitHub Actions Workflows

lP4xRW8n48Pxd ANUwT8YH2YoK6CFRiu B4Pso0KSVViOYq92BHTkj7 aByPRtFHN6hmmbDNK1BOzVi2wvXCSIcAc2oXTuLfIFWMGESyGRvzwgEMq1bc38XBoybTTTvAhDTrLsEfcysKwFUP 4XyhGDVbIgr6O3kAHQO50RdwKwJWpQ7SPHtQdi2Dfa3CLmOrImVUZNlKTASOUfTBdnkgSkDrTBWq aLh7h Bzp6qJnqdC8IE5D8XLJZVEHlvjt1hws1awtjIo 2J8hoQZDlWXLaf1C ZXDIYWZQHIbcLut9t rkKzEVRlXCRVdNt FIWUAfk7meh6QSayx8rHZAkQjUYkLoe jpfYr5MuF 0G00

build-docker.yaml
name: Build docker images

(1)
on:
  workflow_dispatch:
  workflow_call:

jobs:
  build-classroom-demo-image:
    permissions: write-all
    runs-on: ubuntu-24.04
    env:
      JAVA_VERSION: '24'
      JAVA_DISTRIBUTION: 'temurin'
    steps:
      (2)
      - name: Check out repository code
        uses: actions/checkout@v4

      (3)
      - name: Set up docker buildx
        uses: docker/setup-buildx-action@v3

      (4)
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      (5)
      - name: Find and set image tag
        run: |
          if test "${{ github.ref_name }}" = "main"; then
            export tag=":stable";
          elif test "${{ github.ref_name }}" = "develop"; then
            export tag=":dev";
          elif echo "${{ github.ref_name }}" | grep -q "^release/"; then
            export tag=:$(echo "${{ github.ref_name }}" | sed "s/release\///");
          elif echo "${{ github.ref_name }}" | grep -q "^rc/"; then
            export tag=:$(echo "${{ github.ref_name }}" | sed "s/rc\///")-rc;
          else
            export tag="";
          fi
          echo "IMAGE_TAG=${tag}" >> "$GITHUB_ENV"

      (6)
      - uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: ${{ env.JAVA_DISTRIBUTION }}

      (7)
      - name: Build project
        working-directory: ./classroom
        run: |
          mvn clean package
          cp ./src/main/docker/Dockerfile ./target
          cp ./src/main/resources/import.sql ./target

      (8)
      - name: Set repository name in lowercase
        run: echo "REPOSITORY=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      (9)
      - name: Build and push classroom-demo
        uses: docker/build-push-action@v5
        with:
          context: ./classroom/target
          file: ./classroom/target/Dockerfile
          platforms: linux/amd64, linux/arm64
          push: true
          tags: ghcr.io/${{ env.REPOSITORY }}/classroom-backend${{ env.IMAGE_TAG }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  build-h2-db-image:
    permissions: write-all
    runs-on: ubuntu-24.04
    steps:
      - name: Check out repository code
        uses: actions/checkout@v4

      - name: Set up docker buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Find and set image tag
        run: |
          if test "${{ github.ref_name }}" = "main"; then
            export tag=":stable";
          elif test "${{ github.ref_name }}" = "develop"; then
            export tag=":dev";
          elif echo "${{ github.ref_name }}" | grep -q "^release/"; then
            export tag=:$(echo "${{ github.ref_name }}" | sed "s/release\///");
          elif echo "${{ github.ref_name }}" | grep -q "^rc/"; then
            export tag=:$(echo "${{ github.ref_name }}" | sed "s/rc\///")-rc;
          else
            export tag="";
          fi
          echo "IMAGE_TAG=${tag}" >> "$GITHUB_ENV"

      - name: Set repository name in lowercase
        run: echo "REPOSITORY=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> $GITHUB_ENV

      - name: Build and push h2-db image
        uses: docker/build-push-action@v5
        with:
          context: ./h2
          file: ./h2/Dockerfile
          platforms: linux/amd64, linux/arm64
          push: true
          tags: ghcr.io/${{ env.REPOSITORY }}/h2-db${{ env.IMAGE_TAG }}
          cache-from: type=gha
          cache-to: type=gha,mode=max
1 Der Workflow wird manuell (workflow_dispatch) oder durch einen anderen Workflow (workflow_call) gestartet.
2 Der Quellcode des Repositories wird für die weiteren Schritte ausgecheckt.
3 docker/setup-buildx-action richtet Docker Buildx ein, um Multi-Architekturen und Caching zu unterstützen.
4 Loggt sich im GitHub Container Registry (GHCR) mit den bereitgestellten Zugangsdaten ein.
5 Setzt dynamisch den Tag für das Docker-Image je nach Branch-Namen (main, develop, etc.)
6 Installiert Java mit der über Umgebungsvariablen definierten Version und Distribution (temurin, Version 24).
7 Baut das Maven-Projekt und kopiert die Dockerfile und die SQL-Datei in das Zielverzeichnis für den Docker-Build.
8 Erstellt aus dem Repository-Namen (z.B. User/Repo) eine kleingeschriebene Variante, da GHCR nur lowercase akzeptiert.
9 Erstellt und veröffentlicht das Docker-Image für das Backend-Projekt classroom auf GHCR.

Ein wichtiger Aspekt, den man beim Verständnis von GitHub Actions beachten sollte, sind die sogenannten uses-Steps.

3.1. GitHub Actions – Ausführen von Workflows auf einem Runner

GitHub Actions ist im Kern ein System, das es ermöglicht, automatisierte Workflows auf sogenannten "Runnern" auszuführen. Ein Runner ist dabei eine virtuelle Maschine (z.B. Ubuntu), auf der die im Workflow definierten Befehle und Aktionen ausgeführt werden. Der Ablauf ist vergleichbar mit dem Abarbeiten eines Skripts – Schritt für Schritt werden die definierten Anweisungen verarbeitet.

Diese Workflows werden in YAML-Dateien definiert und können sowohl einfache Shell-Kommandos enthalten (run) als auch vordefinierte Aktionen (uses), die häufige Aufgaben übernehmen.

3.2. Was bedeutet uses?

Der Schlüsselbegriff uses bezieht sich auf die Verwendung von wiederverwendbaren GitHub Actions, also kleinen, gekapselten Funktionsblöcken, die bereits eine bestimmte Aufgabe erfüllen. Statt beispielsweise ein Docker-Login manuell mit Shell-Befehlen durchzuführen, kann man einfach eine bestehende Aktion wie docker/login-action@v3 verwenden – was nicht nur komfortabler ist, sondern auch Fehlerquellen reduziert.

3.2.1. Der GitHub Actions Marketplace

Viele dieser wiederverwendbaren Aktionen stammen aus dem GitHub Actions Marketplace, einer öffentlichen Sammlung an geprüften und gepflegten Actions, die von der GitHub-Community oder offiziellen Anbietern bereitgestellt werden. Dort findet man Aktionen für unterschiedlichste Anwendungsfälle, z.B.:

  • Setup von Programmiersprachen (actions/setup-java, actions/setup-node)

  • Interaktion mit Cloud-Plattformen (z.B. AWS, Azure, DockerHub)

  • Automatisiertes Testen, Bauen und Veröffentlichen

  • Tools für Codeanalyse, Formatierung oder Sicherheitsscans

3.2.2. Unterschied zu normalen Skripten

Während run-Schritte einfach Shell-Kommandos ausführen, sind uses-Schritte modulare Bausteine mit eigener Logik, die oft mehrere Aufgaben auf einmal erledigen. Man kann sie sich wie Bibliotheken oder Plugins vorstellen, die in den Workflow integriert werden.

Beispiel:

- name: Login to GHCR
  uses: docker/login-action@v3
  with:
    registry: ghcr.io
    username: ${{ github.actor }}
    password: ${{ secrets.GITHUB_TOKEN }}

Anstatt selbst docker login zu skripten, kümmert sich diese Action um alles – inklusive Fehlerbehandlung, Sicherheitsvorgaben und Parameterhandling.

3.3. Fazit

Die Kombination aus einfachen Skripten (run) und wiederverwendbaren Modulen (uses) macht GitHub Actions so mächtig und flexibel. Insbesondere Actions aus dem Marketplace erlauben es, komplexe Workflows mit minimalem Aufwand und hoher Wartbarkeit zu erstellen, wie bei meinem oberen Workflow zu sehen ist.

4. Die GitHub Actions Pipeline fertigstellen

Abschließend wollen wir nun unsere CI/CD-Pipeline fertigstellen. Wir haben schon die Docker-Images, die uns jetzt auch auf unserer VM mittels der Docker-Registry zur Verfügung stehen. Uns fehlt aber noch das Deployment der Anwendung auf einer virtuellen Maschine und das Erstellen von GitHub Releases. Diese Aufgaben werden wir in zwei weiteren Workflows abarbeiten.

NOv1pe9034RtSuec  g 06umnkYIn1CCy0aZdK8wKq aCTXnCGLT63TDq TUqZ9bhzaYksrnhgXql6785BqOcCb7kZJzCZFqlpfGxGr29YrvIzTbxXMoUsSr8wOCPfEEe0ClZmyyXNosclPD3gCake wUHlycpoZKq2AOn3GZrk52RpUyTL0MbU nlht4m00

4.1. Deployment auf der VM

Das Deployment auf der VM erfolgt in einem separaten Workflow, der die Docker-Images von GitHub Container Registry abruft und auf der VM bereitstellt. Hierbei wird SSH verwendet, um sich mit der VM zu verbinden und die notwendigen Befehle auszuführen.

Bei der Remote-Ausführung von Befehlen auf einer VM mittels SSH muss man sehr auf Strings und so weiter aufpassen. (Ich hatte viel zu viel zu viele Probleme deswegen, weil ich irgendwo wieder ein '\' vor einem Sonderzeichen vergaß.)
Pipeline des GitHub Actions Workflows

lP4xRW8n48Pxd ANUwT8YH2YoK6CFRiu B4Pso0KSVViOYq92BHTkj7 aByPRtFHN6hmmbDNK1BOzVi2wvXCSIcAc2oXTuLfIFWMGESyGRvzwgEMq1bc38XBoybTTTvAhDTrLsEfcysKwFUP 4XyhGDVbIgr6O3kAHQO50RdwKwJWpQ7SPHtQdi2Dfa3CLmOrImVUZNlKTASOUfTBdnkgSkDrTBWq aLh7h Bzp6qJnqdC8IE5D8XLJZVEHlvjt1hws1awtjIo 2J8hoQZDlWXLaf1C ZXDIYWZQHIbcLut9t rkKzEVRlXCRVdNt FIWUAfk7meh6QSayx8rHZAkQjUYkLoe jpfYr5MuF 0G00

build-and-deploy.yaml
name: Build and deploy

on: (1)
  push:
    branches:
      - develop
      - main
    paths:
      - '.github/workflows/build-and-deploy.yaml'
      - '.github/workflows/build-docker.yaml'
      - 'compose/**'
      - 'classroom/**'
      - 'nginx/**'
  pull_request:
    branches:
      - main
  workflow_dispatch:

jobs:
  app-docker-images: (2)
    permissions: write-all
    name: Build Docker images
    uses: ./.github/workflows/build-docker.yaml

  deploy-to-vm: (3)
    concurrency:
      group: deploy-to-oravm-group
      cancel-in-progress: true
    needs: [ app-docker-images ]
    name: Deploy Classroom-Demo
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@v4

      (4)
      - name: Setup ssh
        shell: bash
        env:
          SSH_PRIVATE_KEY: ${{secrets.SSH_PRIVATE_KEY}}
          SSH_KNOWN_HOSTS: ${{secrets.SSH_KNOWN_HOSTS}}
        run: source ./cicd/setup-ssh.sh ${{github.workspace}} ${{secrets.SSH_USERNAME}} ${{secrets.SSH_HOST}}

      (5)
      - name: SetUp of GitHub Repo
        shell: bash
        run: |
          ssh -i "$SSH_KEY_PATH" $SSH_USER@$SSH_HOST "rm -rf project-classroom"

          ssh -i "$SSH_KEY_PATH" $SSH_USER@$SSH_HOST "eval \$(ssh-agent -s); ssh-add ~/.ssh/github-key; git clone git@github.com:2425-5bhif-wmc/03-tutorials-HikariTempest.git ~/project-classroom "

      (6)
      - name: SetUp of Docker-Compose
        shell: bash
        run: |
          ssh -i "$SSH_KEY_PATH" $SSH_USER@$SSH_HOST 'docker compose -f project-classroom/compose/docker-compose.yaml --env-file project-classroom/compose/.env.prod down'

          ssh -i "$SSH_KEY_PATH" $SSH_USER@$SSH_HOST "docker compose -f project-classroom/compose/docker-compose.yaml --env-file project-classroom/compose/.env.prod pull"

          ssh -i "$SSH_KEY_PATH" $SSH_USER@$SSH_HOST "docker compose -f project-classroom/compose/docker-compose.yaml --env-file project-classroom/compose/.env.prod up -d"

      (7)
      - name: SetUp of Nginx
        shell: bash
        run: |
          ssh -i "$SSH_KEY_PATH" $SSH_USER@$SSH_HOST "sudo cp -f project-classroom/nginx/nginx.conf /etc/nginx/nginx.conf.template"

          ssh -i "$SSH_KEY_PATH" $SSH_USER@$SSH_HOST "export SSH_HOST=${{secrets.SSH_HOST}} && envsubst </etc/nginx/nginx.conf.template | sudo tee /etc/nginx/nginx.conf"

          ssh -i "$SSH_KEY_PATH" $SSH_USER@$SSH_HOST "sudo cp -f project-classroom/nginx/index.html /var/www/html/"

          ssh -i "$SSH_KEY_PATH" $SSH_USER@$SSH_HOST "sudo systemctl restart nginx.service"
1 Dieser on:-Block definiert, wann das Workflow-File ausgeführt wird: bei Pushes auf main oder develop, bei Änderungen an bestimmten Pfaden oder bei einem manuellen Trigger (workflow_dispatch). Auch Pull Requests auf den main-Branch lösen den Workflow aus.
2 Dieser Job verwendet unser Workflow-File von oben (build-docker.yaml) mittels uses, um die Docker-Images zu bauen. Durch das permissions: write-all werden nötige Rechte zum Pushen auf die Registry gesetzt.
3 Der zweite Job hängt von unserem vorherigen Job app-docker-images ab (needs:). Damit wird sichergestellt, dass der Deploy-Job erst startet, wenn die Docker-Images erfolgreich gebaut wurden. concurrency verhindert parallele Ausführungen dieses Jobs und stellt sicher, dass bei einem neuen Trigger ein laufender vorheriger Deploy abgebrochen wird, um Zeit zu sparen, da wir sowieso nur die aktuellsten Daten auf unserer VM wollen.
4 Der SSH-Zugriff wird eingerichtet. Ein Bash-Skript wird verwendet, um einen privaten SSH-Key und bekannte Hosts aus den secrets zu konfigurieren. Dies ermöglicht die sichere Verbindung zur Zielmaschine.
5 Auf der Zielmaschine wird das alte Projektverzeichnis gelöscht und das aktuelle Repository neu geklont. Dadurch reduziert man den Speicher, da git auch die alten Commits speichert. Ein SSH-Agent wird initialisiert, der den GitHub-Deploy-Key lädt, damit der private Klon-Vorgang via SSH funktioniert.
Dies ist für SSH-Keys zu GitHub nötig, da man bei einer VM oft nicht die nötigen Rechte besitzt, um SSH-Keys dauerhaft zu adden (aus Sicherheitsgründen). Deswegen müssen wir den Key jedes Mal von neuem adden. (.bashrz oder anderes funktioniert hierbei nicht, da wir ja in einem anderen Kontext sind und nicht in einer Shell arbeiten, die den Key speichert.)
6 Hier wird Docker Compose auf der Zielmaschine verwendet, um den alten Stand zu stoppen (down), neue Images zu ziehen (pull) und die Anwendung im Hintergrund zu starten (up -d). Das --env-file gibt dabei die Umgebungsvariablen für docker-compose vor.
  • Erklärung des docker-compose.yaml-Files hier.

7 Dieser Schritt richtet den NGINX-Webserver ein. Die Konfigurationsdatei wird kopiert, dynamisch mit Umgebungswerten ersetzt (envsubst), die statische Startseite (index.html) aktualisiert und der NGINX-Dienst wird neu gestartet.
Wenn alles korrekt eingerichtet wurde, sollte die GitHub-Website wie im folgenden Bild aussehen.

build and deploy workflow github website

Damit ist nun ein automatisches Deployment auf unserem Server eingerichtet: Sobald ein Push auf den main-Branch erfolgt, wird die Anwendung automatisch auf der VM aktualisiert. Doch wie sieht es mit Releases aus?

4.2. Release

Releases auf GitHub sind veröffentlichte Versionen eines Projekts, die mit einem bestimmten Stand des Codes (einem Git-Tag) verknüpft sind. Sie dienen dazu, fertige, stabile Versionen bereitzustellen. Sie sind z.B. für Nutzer, die das Projekt herunterladen oder einsetzen möchten.

Ein Release kann zusätzlich Dateien (Assets) enthalten, etwa eine ausführbare .jar-Datei, und ist meist mit einem Changelog versehen, der zeigt, was sich seit dem letzten Release geändert hat.

Auch das können wir automatisieren. Hierzu verwenden wir den release-artifacts.yaml-Workflow. Dieses Skript erstellt ein Prerelease auf GitHub. Dabei werden alle Commits seit dem letzten Release zusammengefasst und mit den Commit-Nachrichten sowie dem .jar-Artefakt der Maven-Java-Anwendung veröffentlicht.

release-artifacts.yaml
name: Release changelog
run-name: ${{ github.actor }} is releasing the newest changelog.

on: (1)
  push:
    branches: [main]
jobs:
  build-and-release-classroom:
    runs-on: ubuntu-24.04
    permissions:
      contents: write (2)
    env:
      JAVA_VERSION: '24'
      JAVA_DISTRIBUTION: 'temurin'
      RUNNER_LOCATION: './classroom/target/*-runner.jar'

    steps:
      - uses: actions/checkout@v4
      (3)
      - name: Get the git commits since the last tag
        run: |
          git pull --unshallow
          echo "$(git log $(git describe --tags --abbrev=0)..HEAD | git shortlog)" | sed 's/^ \+/- /'  > release-notes.txt
          cat release-notes.txt

      (4)
      - uses: actions/setup-java@v4
        with:
          java-version: ${{ env.JAVA_VERSION }}
          distribution: ${{ env.JAVA_DISTRIBUTION }}

      (5)
      - name: Build java project with maven
        working-directory: ./classroom
        run: mvn clean package

      (6)
      - name: Release the build
        run: |
          tag="$(date +"%Y.%m.%d")-$(git rev-parse --short HEAD)"
          gh release create $tag \
            --title "classroom $tag" \
            --notes-file release-notes.txt \
            --prerelease \
            ${{ env.RUNNER_LOCATION }}
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
1 Dieser on:-Block bestimmt, dass der Workflow nur bei einem Push auf den main-Branch ausgeführt wird. Das ist üblich für Releases, da hier die produktionsreifen Änderungen landen.
2 Hier werden die nötigen Permissions für den Workflow festgelegt. Um Releases erstellen zu können benötigt man write-permissions.
3 In diesem Schritt werden alle Git-Commits seit dem letzten Tag ausgelesen. git shortlog gruppiert und formatiert sie übersichtlich. Mit sed wird daraus eine Markdown-taugliche Liste erstellt und in release-notes.txt gespeichert. Dieses File wird später als Changelog verwendet.
4 Java wird mit der gewünschten Version und Distribution installiert, damit das Projekt gebaut werden kann. Dies ist notwendig, da der Maven-Build später eine lauffähige .jar-Datei erzeugt.
5 Das Java-Projekt wird mithilfe von Maven im Unterordner classroom gebaut. Dabei entsteht eine *-runner.jar, die anschließend released wird.
6 Dieser Schritt erstellt mit der GitHub CLI (gh) einen neuen Release.
  • Der Tag besteht aus dem aktuellen Datum und einem Teil des Commit-Hashes, z.B. 2025.04.07-abc123.

  • Die zuvor generierte release-notes.txt-Datei wird als Beschreibung (--notes-file) hinzugefügt.

  • Das Build-Artefakt (*-runner.jar) wird als Release-Datei angehängt.

  • Da gh Zugriff auf das Repository benötigt, wird GH_TOKEN über ein GitHub Secret bereitgestellt.

Beispiel eines Releases welcher mit diesem Script erstellt wurde

example release

Doch was ist, wenn wir einen Fehler haben und das Release nicht funktioniert? Hierzu gibt es die Möglichkeit, den Workflow zu debuggen. Dies geschieht über Tmate.

4.3. Debugging mit Tmate

Um Fehler in einem GitHub Actions Workflow besser untersuchen zu können, bietet sich der Einsatz von Tmate an. Während andere CI-Systeme wie TravisCI oder CircleCI einen integrierten Debug-Modus mit SSH-Zugriff haben, fehlt eine solche Funktion in GitHub Actions bisher. Mit der Action mxschmitt/action-tmate kann man dennoch eine SSH-Verbindung zu einem laufenden GitHub Runner aufbauen und so interaktiv Probleme analysieren.

Um Tmate zu nutzen, fügt man folgenden Schritt in den Workflow ein:

release-artifacts-debug-demo.yaml
...
      - name: Setup tmate session
        uses: mxschmitt/action-tmate@v3
        with:
          limit-access-to-actor: true (1)
...
1 Für mehr Sicherheit - insbesondere bei öffentlichen Repositories - kann man den Zugriff auf den Nutzer beschränken, der den Workflow ausgelöst hat. Dies geschieht mit dem Parameter limit-access-to-actor: true. So wird verhindert, dass Dritte auf die Debug-Sitzung zugreifen und beispielsweise Umgebungsvariablen wie GITHUB_TOKEN auslesen.

Sobald der Tmate-Schritt im Workflow erreicht wird, startet ein SSH-Server, und in der Log-Ausgabe der Action erscheinen alle paar Sekunden die Verbindungsdaten zur Sitzung. Über diese Verbindung kann man dann auf den laufenden Container zugreifen und z.B. Konfigurationsfehler oder fehlgeschlagene Befehle direkt überprüfen.

Nach kurzer Zeit erscheint in der Konsole ein SSH-Befehl, mit dem man sich direkt mit dem Tmate-Server verbinden kann. Diesen Befehl kann man einfach kopieren und im eigenen Terminal ausführen.

ssh <zeichenfolge>@nyc1.tmate.io
Tmate Console Output

tmate console output

Man befindet sich nun per SSH auf dem GitHub Actions Runner und kann beispielsweise überprüfen, ob die Datei release-notes.txt wie erwartet erstellt wurde.

5. Fazit

Schlussendlich hast du nun eine vollständige CI/CD Pipeline für GitHub-Actions, mit der du deine Anwendung automatisiert bauen und deployen kannst. Außerdem hast du die Grundlagen von GitHub Actions und deren Funktionsweise kennengelernt.