Subsections of Bauen von Programmen, Automatisierung, Continuous Integration

Build-Systeme: Gradle

TL;DR

Um beim Übersetzen und Testen von Software von den spezifischen Gegebenheiten auf einem Entwicklerrechner unabhängig zu werden, werden häufig sogenannte Build-Tools eingesetzt. Mit diesen konfiguriert man sein Projekt abseits einer IDE und übersetzt, testet und baut seine Applikation damit entsprechend unabhängig. In der Java-Welt sind aktuell die Build-Tools Ant, Maven und Gradle weit verbreitet.

In Gradle ist ein Java-Entwicklungsmodell quasi eingebaut. Über die Konfigurationsskripte müssen nur noch bestimmte Details wie benötigte externe Bibliotheken oder die Hauptklasse und sonstige Projektbesonderheiten konfiguriert werden. Über "Tasks" wie build, test oder run können Java-Projekte übersetzt, getestet und ausgeführt werden. Dabei werden die externen Abhängigkeiten (Bibliotheken) aufgelöst (soweit konfiguriert) und auch abhängige Tasks mit erledigt, etwa muss zum Testen vorher der Source-Code übersetzt werden.

Gradle bietet eine Fülle an Plugins für bestimmte Aufgaben an, die jeweils mit neuen Tasks einher kommen. Beispiele sind das Plugin java, welches weitere Java-spezifische Tasks wie classes mitbringt, oder das Plugin checkstyle zum Überprüfen von Coding-Style-Richtlinien.

Videos (HSBI-Medienportal)
Lernziele
  • (K3) Schreiben und Verstehen einfacher Gradle-Skripte

Automatisieren von Arbeitsabläufen

Works on my machine ...

Einen häufigen Ausspruch, den man bei der Zusammenarbeit in Teams zu hören bekommt, ist "Also, bei mir läuft der Code." ...

Das Problem dabei ist, dass jeder Entwickler eine andere Maschine hat, oft ein anderes Betriebssystem oder eine andere OS-Version. Dazu kommen noch eine andere IDE und/oder andere Einstellungen und so weiter.

Wie bekommt man es hin, dass Code zuverlässig auch auf anderen Rechnern baut? Ein wichtiger Baustein dafür sind sogenannte "Build-Systeme", also Tools, die unabhängig von der IDE (und den IDE-Einstellungen) für das Übersetzen der Software eingesetzt werden und deren Konfiguration dann mit im Repo eingecheckt wird. Damit kann die Software dann auf allen Rechnern und insbesondere dann auch auf dem Server (Stichwort "Continuous Integration") unabhängig von der IDE o.ä. automatisiert gebaut und getestet werden.

  • Build-Tools:
    • Apache Ant
    • Apache Maven
    • Gradle

Das sind die drei am häufigsten anzutreffenden Build-Tools in der Java-Welt.

Ant ist von den drei genannten Tools das älteste und setzt wie Maven auf XML als Beschreibungssprache. In Ant müssen dabei alle Regeln stets explizit formuliert werden, die man benutzen möchte.

In Maven wird dagegen von einem bestimmten Entwicklungsmodell ausgegangen, hier müssen nur noch die Abweichungen zu diesem Modell konfiguriert werden.

In Gradle wird eine DSL basierend auf der Skriptsprache Groovy (läuft auf der JVM) eingesetzt, und es gibt hier wie in Maven ein bestimmtes eingebautes Entwicklungsmodell. Gradle bringt zusätzlich noch einen Wrapper mit, d.h. es wird eine Art Gradle-Starter im Repo konfiguriert, der sich quasi genauso verhält wie ein fest installiertes Gradle (s.u.).

Achtung: Während Ant und Maven relativ stabil in der API sind, verändert sich Gradle teilweise deutlich zwischen den Versionen. Zusätzlich sind bestimmte Gradle-Versionen oft noch von bestimmten JDK-Versionen abhängig. In der Praxis bedeutet dies, dass man Gradle-Skripte im Laufe der Zeit relativ oft überarbeiten muss (einfach nur, damit das Skript wieder läuft - ohne dass man dabei irgendwelche neuen Features oder sonstige Vorteile erzielen würde). Ein großer Vorteil ist aber der Gradle-Wrapper (s.u.).

Gradle: Eine DSL in Groovy

DSL: Domain Specific Language

// build.gradle
plugins {
    id 'java'
    id 'application'
}

repositories {
    mavenCentral()
}

dependencies {
    testImplementation 'junit:junit:4.13.2'
}

application {
    mainClass = 'fluppie.App'
}

Dies ist mit die einfachste Build-Datei für Gradle.

Über Plugins wird die Unterstützung für Java und das Bauen von Applikationen aktiviert, d.h. es stehen darüber entsprechende spezifische Tasks zur Verfügung.

Abhängigkeiten sollen hier aus dem Maven-Repository MavenCentral geladen werden. Zusätzlich wird hier als Abhängigkeit für den Test (testImplementation) die JUnit-Bibliothek in einer Maven-artigen Notation angegeben (vgl. mvnrepository.com). (Für nur zur Übersetzung der Applikation benötigte Bibliotheken verwendet man stattdessen das Schlüsselwort implementation.)

Bei der Initialisierung wurde als Package fluppie angegeben. Gradle legt darunter per Default die Klasse App mit einer main()-Methode an. Entsprechend kann man über den Eintrag application den Einsprungpunkt in die Applikation konfigurieren.

Gradle-DSL

Ein Gradle-Skript ist letztlich ein in Groovy geschriebenes Skript. Groovy ist eine auf Java basierende und auf der JVM ausgeführte Skriptsprache. Seit einigen Versionen kann man die Gradle-Build-Skripte auch in der Sprache Kotlin schreiben.

Dateien

Für das Bauen mit Gradle benötigt man drei Dateien im Projektordner:

  • build.gradle: Die auf der Gradle-DSL beruhende Definition des Builds mit den Tasks (und ggf. Abhängigkeiten) eines Projekts.

    Ein Multiprojekt hat pro Projekt eine solche Build-Datei. Dabei können die Unterprojekte Eigenschaften der Eltern-Buildskripte "erben" und so relativ kurz ausfallen.

  • settings.gradle: Eine optionale Datei, in der man beispielsweise den Projektnamen oder bei einem Multiprojekt die relevanten Unterprojekte festlegt.

  • gradle.properties: Eine weitere optionale Datei, in der projektspezifische Properties für den Gradle-Build spezifizieren kann.

Gradle Init

Um eine neue Gradle-Konfiguration anlegen zu lassen, geht man in einen Ordner und führt darin gradle init aus. Gradle fragt der Reihe nach einige Einstellungen ab:

$ gradle init

Select type of project to generate:
  1: basic
  2: application
  3: library
  4: Gradle plugin
Enter selection (default: basic) [1..4] 2

Select implementation language:
  1: C++
  2: Groovy
  3: Java
  4: Kotlin
  5: Scala
  6: Swift
Enter selection (default: Java) [1..6] 3

Split functionality across multiple subprojects?:
  1: no - only one application project
  2: yes - application and library projects
Enter selection (default: no - only one application project) [1..2] 1

Select build script DSL:
  1: Groovy
  2: Kotlin
Enter selection (default: Groovy) [1..2] 1

Select test framework:
  1: JUnit 4
  2: TestNG
  3: Spock
  4: JUnit Jupiter
Enter selection (default: JUnit Jupiter) [1..4] 1

Project name (default: tmp): wuppie
Source package (default: tmp): fluppie

Typischerweise möchte man eine Applikation bauen (Auswahl 2 bei der ersten Frage). Als nächstes wird nach der Sprache des Projekts gefragt sowie nach der Sprache für das Gradle-Build-Skript (Default ist Groovy) sowie nach dem Testframework, welches verwendet werden soll.

Damit wird die eingangs gezeigte Konfiguration angelegt.

Ordner

Durch gradle init wird ein neuer Ordner wuppie/ mit folgender Ordnerstruktur angelegt:

drwxr-xr-x 4 cagix cagix 4096 Apr  8 11:43 ./
drwxrwxrwt 1 cagix cagix 4096 Apr  8 11:43 ../
-rw-r--r-- 1 cagix cagix  154 Apr  8 11:43 .gitattributes
-rw-r--r-- 1 cagix cagix  103 Apr  8 11:43 .gitignore
drwxr-xr-x 3 cagix cagix 4096 Apr  8 11:43 app/
drwxr-xr-x 3 cagix cagix 4096 Apr  8 11:42 gradle/
-rwxr-xr-x 1 cagix cagix 8070 Apr  8 11:42 gradlew*
-rw-r--r-- 1 cagix cagix 2763 Apr  8 11:42 gradlew.bat
-rw-r--r-- 1 cagix cagix  370 Apr  8 11:43 settings.gradle

Es werden Einstellungen für Git erzeugt (.gitattributes und .gitignore).

Im Ordner gradle/ wird der Gradle-Wrapper abgelegt (s.u.). Dieser Ordner wird normalerweise mit ins Repo eingecheckt. Die Skripte gradlew und gradlew.bat sind die Startskripte für den Gradle-Wrapper (s.u.) und werden normalerweise ebenfalls ins Repo mit eingecheckt.

Der Ordner .gradle/ (erscheint ggf. nach dem ersten Lauf von Gradle auf dem neuen Projekt) ist nur ein Hilfsordner ("Cache") von Gradle. Hier werden heruntergeladene Dateien etc. abgelegt. Dieser Order sollte nicht ins Repo eingecheckt werden und ist deshalb auch per Default im generierten .gitignore enthalten. (Zusätzlich gibt es im User-Verzeichnis auch noch einen Ordner .gradle/ mit einem globalen Cache.)

In settings.gradle finden sich weitere Einstellungen. Die eigentliche Gradle-Konfiguration befindet sich zusammen mit dem eigentlichen Projekt im Unterordner app/:

drwxr-xr-x 4 root root 4096 Apr  8 11:50 ./
drwxr-xr-x 5 root root 4096 Apr  8 11:49 ../
drwxr-xr-x 5 root root 4096 Apr  8 11:50 build/
-rw-r--r-- 1 root root  852 Apr  8 11:43 build.gradle
drwxr-xr-x 4 root root 4096 Apr  8 11:43 src/

Die Datei build.gradle ist die durch gradle init erzeugte (und eingangs gezeigte) Konfigurationsdatei, vergleichbar mit build.xml für Ant oder pom.xml für Maven. Im Unterordner build/ werden die generierten .class-Dateien etc. beim Build-Prozess abgelegt.

Unter src/ findet sich dann eine Maven-typische Ordnerstruktur für die Sourcen:

$ tree src/
src/
|-- main
|   |-- java
|   |   `-- fluppie
|   |       `-- App.java
|   `-- resources
`-- test
    |-- java
    |   `-- fluppie
    |       `-- AppTest.java
    `-- resources

Unterhalb von src/ ist ein Ordner main/ für die Quellen der Applikation (Sourcen und Ressourcen). Für jede Sprache gibt es einen eigenen Unterordner, hier entsprechend java/. Unterhalb diesem folgt dann die bei der Initialisierung angelegte Package-Struktur (hier fluppie mit der Default-Main-Klasse App mit einer main()-Methode). Diese Strukturen wiederholen sich für die Tests unterhalb von src/test/.

Wer die herkömmlichen, deutlich flacheren Strukturen bevorzugt, also unterhalb von src/ direkt die Java-Package-Strukturen für die Sourcen der Applikation und unterhalb von test/ entsprechend die Strukturen für die JUnit-Test, der kann dies im Build-Skript einstellen:

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
        resources {
            srcDirs = ['res']
        }
    test {
        java {
            srcDirs = ['test']
        }
    }
}

Ablauf eines Gradle-Builds

Ein Gradle-Build hat zwei Hauptphasen: Konfiguration und Ausführung.

Während der Konfiguration wird das gesamte Skript durchlaufen (vgl. Ausführung der direkten Anweisungen eines Tasks). Dabei wird ein Graph erzeugt: welche Tasks hängen von welchen anderen ab etc.

Anschließend wird der gewünschte Task ausgeführt. Dabei werden zuerst alle Tasks ausgeführt, die im Graphen auf dem Weg zu dem gewünschten Task liegen.

Mit gradle tasks kann man sich die zur Verfügung stehenden Tasks ansehen. Diese sind der Übersicht halber noch nach "Themen" sortiert.

Für eine Java-Applikation sind die typischen Tasks gradle build zum Bauen der Applikation (inkl. Ausführen der Tests) sowie gradle run zum Starten der Anwendung. Wer nur die Java-Sourcen compilieren will, würde den Task gradle compileJava nutzen. Mit gradle check würde man compilieren und die Tests ausführen sowie weitere Checks durchführen (gradle test würde nur compilieren und die Tests ausführen), mit gradle jar die Anwendung in ein .jar-File packen und mit gradle javadoc die Javadoc-Dokumentation erzeugen und mit gradle clean die generierten Hilfsdateien aufräumen (löschen).

Plugin-Architektur

Für bestimmte Projekttypen gibt es immer wieder die gleichen Aufgaben. Um hier Schreibaufwand zu sparen, existieren verschiedene Plugins für verschiedene Projekttypen. In diesen Plugins sind die entsprechenden Tasks bereits mit den jeweiligen Abhängigkeiten formuliert. Diese Idee stammt aus Maven, wo dies für Java-basierte Projekte umgesetzt ist.

Beispielsweise erhält man über das Plugin java den Task clean zum Löschen aller generierten Build-Artefakte, den Task classes, der die Sourcen zu .class-Dateien kompiliert oder den Task test, der die JUnit-Tests ausführt ...

Sie können sich Plugins und weitere Tasks relativ leicht auch selbst definieren.

Auflösen von Abhängigkeiten

Analog zu Maven kann man Abhängigkeiten (etwa in einer bestimmten Version benötigte Bibliotheken) im Gradle-Skript angeben. Diese werden (transparent für den User) von einer ebenfalls angegeben Quelle, etwa einem Maven-Repository, heruntergeladen und für den Build genutzt. Man muss also nicht mehr die benötigten .jar-Dateien der Bibliotheken mit ins Projekt einchecken. Analog zu Maven können erzeugte Artefakte automatisch publiziert werden, etwa in einem Maven-Repository.

Für das Projekt benötigte Abhängigkeiten kann man über den Eintrag dependencies spezifizieren. Dabei unterscheidet man u.a. zwischen Applikation und Tests: implementation und testImplementation für das Compilieren und Ausführen von Applikation bzw. Tests. Diese Abhängigkeiten werden durch Gradle über die im Abschnitt repositories konfigurierten Repositories aufgelöst und die entsprechenden .jar-Files geladen (in den .gradle/-Ordner).

Typische Repos sind das Maven-Repo selbst (mavenCentral()) oder das Google-Maven-Repo (google()).

Die Einträge in dependencies erfolgen dabei in einer Maven-Notation, die Sie auch im Maven-Repo mvnrepository.com finden.

Beispiel mit weiteren Konfigurationen (u.a. Checkstyle und Javadoc)

plugins {
    id 'java'
    id 'application'
    id 'checkstyle'
}


repositories {
    mavenCentral()
}

application {
    mainClass = 'hangman.Main'
}

java {
    sourceCompatibility = JavaVersion.VERSION_11
    targetCompatibility = JavaVersion.VERSION_11
}

run {
    standardInput = System.in
}

sourceSets {
    main {
        java {
            srcDirs = ['src']
        }
        resources {
            srcDirs = ['res']
        }
    }
}

checkstyle {
    configFile = file(${rootDir}/google_checks.xml)
    toolVersion = '8.32'
}

dependencies {
    implementation group: 'org.apache.poi', name: 'poi', version: '4.1.2'
}

javadoc {
    options.showAll()
}

Hier sehen Sie übrigens noch eine weitere mögliche Schreibweise für das Notieren von Abhängigkeiten: implementation group: 'org.apache.poi', name: 'poi', version: '4.1.2' und implementation 'org.apache.poi:poi:4.1.2' sind gleichwertig, wobei die letztere Schreibweise sowohl in den generierten Builds-Skripten und in der offiziellen Dokumentation bevorzugt wird.

Gradle und Ant (und Maven)

Vorhandene Ant-Buildskripte kann man nach Gradle importieren und ausführen lassen. Über die DSL kann man auch direkt Ant-Tasks aufrufen. Siehe auch "Using Ant from Gradle".

Gradle-Wrapper

project
|-- app/
|-- build.gradle
|-- gradlew
|-- gradlew.bat
`-- gradle/
    `-- wrapper/
        |-- gradle-wrapper.jar
        `-- gradle-wrapper.properties

Zur Ausführung von Gradle-Skripten benötigt man eine lokale Gradle-Installation. Diese sollte für i.d.R. alle User, die das Projekt bauen wollen, identisch sein. Leider ist dies oft nicht gegeben bzw. nicht einfach lösbar.

Zur Vereinfachung gibt es den Gradle-Wrapper gradlew (bzw. gradlew.bat für Windows). Dies ist ein kleines Shellskript, welches zusammen mit einigen kleinen .jar-Dateien im Unterordner gradle/ mit ins Repo eingecheckt wird und welches direkt die Rolle des gradle-Befehls einer Gradle-Installation übernehmen kann. Man kann also in Konfigurationskripten, beispielsweise für Gitlab CI, alle Aufrufe von gradle durch Aufrufe von gradlew ersetzen.

Beim ersten Aufruf lädt gradlew dann die spezifizierte Gradle-Version herunter und speichert diese in einem lokalen Ordner .gradle/. Ab dann greift gradlew auf diese lokale (nicht "installierte") gradle-Version zurück.

gradle init erzeugt den Wrapper automatisch in der verwendeten Gradle-Version mit. Alternativ kann man den Wrapper nachträglich über gradle wrapper --gradle-version 6.5 in einer bestimmten (gewünschten) Version anlegen lassen.

Da der Gradle-Wrapper im Repository eingecheckt ist, benutzen alle Entwickler damit automatisch die selbe Version, ohne diese auf ihrem System zuvor installieren zu müssen. Deshalb ist der Einsatz des Wrappers einem fest installierten Gradle vorzuziehen!

Wrap-Up

  • Automatisieren von Arbeitsabläufen mit Build-Tools/-Skripten

  • Einstieg in Gradle (DSL zur Konfiguration)

    • Typisches Java-Entwicklungsmodell eingebaut
    • Konfiguration der Abweichungen (Abhängigkeiten, Namen, ...)
    • Gradle-Wrapper: Ersetzt eine feste Installation
Challenges

Betrachten Sie das Buildskript gradle.build aus Dungeon-CampusMinden/Dungeon.

Erklären Sie, in welche Abschnitte das Buildskript unterteilt ist und welche Aufgaben diese Abschnitte jeweils erfüllen. Gehen Sie dabei im Detail auf das Plugin java und die dort bereitgestellten Tasks und deren Abhängigkeiten untereinander ein.

Quellen

Continuous Integration (CI)

TL;DR

In größeren Projekten mit mehreren Teams werden die Beteiligten i.d.R. nur noch "ihre" Codestellen compilieren und testen. Dennoch ist es wichtig, das gesamte Projekt regelmäßig zu "bauen" und auch umfangreichere Testsuiten regelmäßig laufen zu lassen. Außerdem ist es wichtig, das in einer definierten Umgebung zu tun und nicht auf einem oder mehreren Entwicklerrechnern, die i.d.R. (leicht) unterschiedlich konfiguriert sind, um zuverlässige und nachvollziehbare Ergebnisse zu bekommen. Weiterhin möchte man auf bestimmte Ereignisse reagieren, wie etwa neue Commits im Git-Server, oder bei Pull-Requests möchte man vor dem Merge automatisiert sicherstellen, dass damit die vorhandenen Tests alle "grün" sind und auch die Formatierung etc. stimmt.

Dafür hat sich "Continuous Integration" etabliert. Hier werden die angesprochenen Prozesse regelmäßig auf einem dafür eingerichteten System durchgeführt. Aktivitäten wie Übersetzen, Testen, Style-Checks etc. werden in sogenannten "Pipelines" oder "Workflows" zusammengefasst und automatisiert durch Commits, Pull-Requests oder Merges auf dem Git-Server ausgelöst. Die Aktionen können dabei je nach Trigger und Branch unterschiedlich sein, d.h. man könnte etwa bei PR gegen den Master umfangreichere Tests laufen lassen als bei einem PR gegen einen Develop-Branch. In einem Workflow oder einer Pipeline können einzelne Aktionen wiederum von anderen Aktionen abhängen. Das Ergebnis kann man dann auf dem Server einsehen oder bekommt man komfortabel als Report per Mail zugeschickt.

Wir schauen uns hier exemplarisch GitHub Actions und GitLab CI/CD an. Um CI sinnvoll einsetzen zu können, benötigt man Kenntnisse über Build-Tools. "CI" tritt üblicherweise zusammen mit "CD" (Continuous Delivery) auf, also als "CI/CD". Der "CD"-Teil ist nicht Gegenstand der Betrachtung in dieser Lehrveranstaltung.

Videos (HSBI-Medienportal)
Lernziele
  • (K2) Arbeitsweise von/mit CI

Motivation: Zusammenarbeit in Teams

Szenario

  • Projekt besteht aus diversen Teilprojekten
  • Verschiedene Entwicklungs-Teams arbeiten (getrennt) an verschiedenen Projekten
  • Tester entwickeln Testsuiten für die Teilprojekte
  • Tester entwickeln Testsuiten für das Gesamtprojekt

Manuelle Ausführung der Testsuiten reicht nicht

  • Belastet den Entwicklungsprozess
  • Keine (einheitliche) Veröffentlichung der Ergebnisse
  • Keine (einheitliche) Eskalation bei Fehlern
  • Keine regelmäßige Integration in Gesamtprojekt

Continuous Integration

  • Regelmäßige, automatische Ausführung: Build und Tests
  • Reporting
  • Weiterführung der Idee: Regelmäßiges Deployment (Continuous Deployment)

Continuous Integration (CI)

Vorgehen

  • Entwickler und Tester committen ihre Änderungen regelmäßig (Git, SVN, ...)
  • CI-Server arbeitet Build-Skripte ab, getriggert durch Events: Push-Events, Zeit/Datum, ...
    • Typischerweise wird dabei:
      • Das Gesamtprojekt übersetzt ("gebaut")
      • Die Unit- und die Integrationstests abgearbeitet
      • Zu festen Zeiten werden zusätzlich Systemtests gefahren
    • Typische weitere Builds: "Nightly Build", Release-Build, ...
    • Ergebnisse jeweils auf der Weboberfläche einsehbar (und per E-Mail)

Einige Vorteile

  • Tests werden regelmäßig durchgeführt (auch wenn sie lange dauern oder die Maschine stark belasten)
  • Es wird regelmäßig ein Gesamt-Build durchgeführt
  • Alle Teilnehmer sind über aktuellen Projekt(-zu-)stand informiert

Beispiele für verbreitete CI-Umgebungen

GitLab CI/CD

Siehe auch "Get started with Gitlab CI/CD". (Für den Zugriff wird VPN benötigt!)

Übersicht über Pipelines

  • In Spalte "Status" sieht man das Ergebnis der einzelnen Pipelines: "pending" (die Pipeline läuft gerade), "cancelled" (Pipeline wurde manuell abgebrochen), "passed" (alle Jobs der Pipeline sind sauber durchgelaufen), "failed" (ein Job ist fehlgeschlagen, Pipeline wurde deshalb abgebrochen)
  • In Spalte "Pipeline" sind die Pipelines eindeutig benannt aufgeführt, inkl. Trigger (Commit und Branch)
  • In Spalte "Stages" sieht man den Zustand der einzelnen Stages

Wenn man mit der Maus auf den Status oder die Stages geht, erfährt man mehr bzw. kann auf eine Seite mit mehr Informationen kommen.

Detailansicht einer Pipeline

Wenn man in eine Pipeline in der Übersicht klickt, werden die einzelnen Stages dieser Pipeline genauer dargestellt.

Detailansicht eines Jobs

Wenn man in einen Job einer Stage klickt, bekommt man quasi die Konsolenausgabe dieses Jobs. Hier kann man ggf. Fehler beim Ausführen der einzelnen Skripte oder die Ergebnisse beispielsweise der JUnit-Läufe anschauen.

GitLab CI/CD: Konfiguration mit YAML-Datei

Datei .gitlab-ci.yml im Projekt-Ordner:

stages:
    - my.compile
    - my.test

job1:
    script:
        - echo "Hello"
        - ./gradlew compileJava
        - echo "wuppie!"
    stage: my.compile
    only:
        - wuppie

job2:
    script: "./gradlew test"
    stage: my.test

job3:
    script:
        - echo "Job 3"
    stage: my.compile

Stages

Unter stages werden die einzelnen Stages einer Pipeline definiert. Diese werden in der hier spezifizierten Reihenfolge durchgeführt, d.h. zuerst würde my.compile ausgeführt, und erst wenn alle Jobs in my.compile erfolgreich ausgeführt wurden, würde anschließend my.test ausgeführt.

Dabei gilt: Die Jobs einer Stage werden (potentiell) parallel zueinander ausgeführt, und die Jobs der nächsten Stage werden erst dann gestartet, wenn alle Jobs der aktuellen Stage erfolgreich beendet wurden.

Wenn keine eigenen stages definiert werden, kann man (lt. Doku) auf die Default-Stages build, test und deploy zurückgreifen. Achtung: Sobald man eigene Stages definiert, stehen diese Default-Stages nicht mehr zur Verfügung!

Jobs

job1, job2 und job3 definieren jeweils einen Job.

  • job1 besteht aus mehreren Befehlen (unter script). Alternativ kann man die bei job2 gezeigte Syntax nutzen, wenn nur ein Befehl zu bearbeiten ist.

    Die Befehle werden von GitLab CI/CD in einer Shell ausgeführt.

  • Die Jobs job1 und job2 sind der Stage my.compile zugeordnet (Abschnitt stage). Einer Stage können mehrere Jobs zugeordnet sein, die dann parallel ausgeführt werden.

    Wenn ein Job nicht explizit einer Stage zugeordnet ist, wird er (lt. Doku) zur Default-Stage test zugewiesen. (Das geht nur, wenn es diese Stage auch gibt!)

  • Mit only und except kann man u.a. Branches oder Tags angeben, für die dieser Job ausgeführt (bzw. nicht ausgeführt) werden soll.

Durch die Kombination von Jobs mit der Zuordnung zu Stages und Events lassen sich unterschiedliche Pipelines für verschiedene Zwecke definieren.

Hinweise zur Konfiguration von GitLab CI/CD

Im Browser in den Repo-Einstellungen arbeiten:

  1. Unter Settings > General > Visibility, project features, permissions das CI/CD aktivieren
  2. Prüfen unter Settings > CI/CD > Runners, dass unter Available shared Runners mind. ein shared Runner verfügbar ist (mit grün markiert ist)
  3. Unter Settings > CI/CD > General pipelines einstellen:
    • Git strategy: git clone
    • Timeout: 10m
    • Public pipelines: false (nicht angehakt)
  4. YAML-File (.gitlab-ci.yml) in Projektwurzel anlegen, Aufbau siehe oben
  5. Build-Skript erstellen, lokal lauffähig bekommen, dann in Jobs nutzen
  6. Im .gitlab-ci.yml die relevanten Branches einstellen (s.o.)
  7. Pushen, und unter CI/CD > Pipelines das Builden beobachten
    • in Status reinklicken und schauen, ob und wo es hakt
  8. README.md anlegen in Projektwurzel (neben .gitlab-ci.yml), Markdown-Schnipsel aus Settings > CI/CD > General pipelines > Pipeline status auswählen und einfügen .…

Optional:

  1. Ggf. Schedules unter CI/CD > Schedules anlegen
  2. Ggf. extra Mails einrichten: Settings > Integrations > Pipeline status emails

GitHub Actions

Siehe "GitHub Actions: Automate your workflow from idea to production" und auch "GitHub: CI/CD explained".

Übersicht über Workflows

Hier sieht man das Ergebnis der letzten Workflows. Dazu sieht man den Commit und den Branch, auf dem der Workflow gelaufen ist sowie wann er gelaufen ist. Über die Spalten kann man beispielsweise nach Status oder Event filtern.

In der Abbildung ist ein Workflow mit dem Namen "GitHub CI" zu sehen, der aktuell noch läuft.

Detailansicht eines Workflows

Wenn man in einen Workflow in der Übersicht anklickt, werden die einzelnen Jobs dieses Workflows genauer dargestellt. "job3" ist erfolgreich gelaufen, "job1" läuft gerade, und "job2" hängt von "job1" ab, d.h. kann erst nach dem erfolgreichen Lauf von "job2" starten.

Detailansicht eines Jobs

Wenn man in einen Job anklickt, bekommt man quasi die Konsolenausgabe dieses Jobs. Hier kann man ggf. Fehler beim Ausführen der einzelnen Skripte oder die Ergebnisse beispielsweise der JUnit-Läufe anschauen.

GitHub Actions: Konfiguration mit YAML-Datei

Workflows werden als YAML-Dateien im Ordner .github/workflows/ angelegt.

name: GitHub CI

on:
  # push on master branch
  push:
    branches: [master]
  # manually triggered
  workflow_dispatch:

jobs:

  job1:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - uses: gradle/wrapper-validation-action@v1
      - run: echo "Hello"
      - run: ./gradlew compileJava
      - run: echo "wuppie!"

  job2:
    needs: job1
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - uses: gradle/wrapper-validation-action@v1
      - run: ./gradlew test

  job3:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Job 3"

Workflowname und Trigger-Events

Der Name des Workflows wird mit dem Eintrag name spezifiziert und sollte sich im Dateinamen widerspiegeln, also im Beispiel .github/workflows/github_ci.yml.

Im Eintrag on können die Events definiert werden, die den Workflow triggern. Im Beispiel ist ein Push-Event auf dem master-Branch definiert sowie mit workflow_dispatch: das manuelle Triggern (auf einem beliebigen Branch) freigeschaltet.

Jobs

Die Jobs werden unter dem Eintrag jobs definiert: job1, job2 und job3 definieren jeweils einen Job.

  • job1 besteht aus mehreren Befehlen (unter steps), die auf einem aktuellen virtualisierten Ubuntu-Runner ausgeführt werden.

    Es wird zunächst das Repo mit Hilfe der Checkout-Action ausgecheckt (uses: actions/checkout@v4), das JDK eingerichtet/installiert (uses: actions/setup-java@v3) und der im Repo enthaltene Gradle-Wrapper auf Unversehrtheit geprüft (uses: gradle/wrapper-validation-action@v1).

    Die Actions sind vordefinierte Actions und im Github unter github.com/ + Action zu finden, d.h. actions/checkout oder actions/setup-java. Actions können von jedermann definiert und bereitgestellt werden, in diesem Fall handelt es sich um von GitHub selbst im Namespace "actions" bereit gestellte direkt nutzbare Actions. Man kann Actions auch selbst im Ordner .github/actions/ für das Repo definieren (Beispiel: plfa.github.io).

    Mit run werden Befehle in der Shell auf dem genutzten Runner (hier Ubuntu) ausgeführt.

  • Die Jobs job2 ist von job1 abhängig und wird erst gestartet, wenn job1 erfolgreich abgearbeitet ist.

    Ansonsten können die Jobs prinzipiell parallel ausgeführt werden.

Durch die Kombination von Workflows mit verschiedenen Jobs und Abhängigkeiten zwischen Jobs lassen sich unterschiedliche Pipelines ("Workflows") für verschiedene Zwecke definieren.

Es lassen sich auch andere Runner benutzen, etwa ein virtualisiertes Windows oder macOS. Man kann auch über einen "Matrix-Build" den Workflow auf mehreren Betriebssystemen gleichzeitig laufen lassen.

Man kann auch einen Docker-Container benutzen. Dabei muss man beachten, dass dieser am besten aus einer Registry (etwa von Docker-Hub oder aus der GitHub-Registry) "gezogen" wird, weil das Bauen des Docker-Containers aus einem Docker-File in der Action u.U. relativ lange dauert.

Hinweise zur Konfiguration von GitHub Actions

Im Browser in den Repo-Einstellungen arbeiten:

  1. Unter Settings > Actions > General > Actions permissions die Actions aktivieren (Auswahl, welche Actions erlaubt sind)

  2. Unter Settings > Actions > General > Workflow permissions ggf. bestimmen, ob die Actions das Repo nur lesen dürfen oder auch zusätzlich schreiben dürfen

  3. Unter Actions > <WORKFLOW> den Workflow ggf. deaktivieren:

Wrap-Up

Überblick über Continuous Integration:

  • Konfigurierbare Aktionen, die auf dem Gitlab-/GitHub-Server ausgeführt werden
  • Unterschiedliche Trigger: Commit, Merge, ...
  • Aktionen können Branch-spezifisch sein
  • Aktionen können von anderen Aktionen abhängen
Challenges

Betrachten Sie erneut das Projekt Theatrical Players Refactoring Kata. Erstellen Sie für dieses Projekt einen GitHub-Workflow, der das Projekt kompiliert und die Testsuite ausführt (nur für den Java-Teil, den restlichen Code können Sie ignorieren).

Dabei soll das Ausführen der JUnit-Tests nur dann erfolgen, wenn das Kompilieren erfolgreich durchgeführt wurde.

Der Workflow soll automatisch für Commits in den Hauptbranch sowie für Pull-Requests loslaufen. Es soll zusätzlich auch manuell aktivierbar sein.

Quellen