GitHub Actions - Programmatische Trigger, Build Pipelines, Dashboard

in  Builds & Tests , , ,

GitHub Actions - Programmatische Trigger, Build Pipelines, Dashboard

In diesem Beitrag geht es darum, mehrere und unterschiedliche Build Jobs miteinander zu verknüpfen. Je größer eine Build Infrastruktur wird, desto häufiger wird man mit solchen Anforderungen konfrontiert, möchte man nicht einen riesigen Monolithen bauen.

Bei ModuleStudio gibt es beispielsweise mehrere Komponenten, die jeweils komplett unabhängig gebaut und getestet werden. Dennoch muß es für Integrationstests und zum Bauen der fertigen Produkte möglich sein, dass eine neue Version einer Komponente die Jobs für weitere Komponenten startet und somit eine Ausführung deren Workflows nach sich zieht.

Workflows in GitHub Actions verketten

Um mehrere Workflows miteinander in Verbindung zu bringen, sind zwei Seiten zu betrachten:

  • Die auslösende Seite muß einen Trigger auslösen und dies ggf. an Bedingungen knüpfen.
  • Die reagierende Seite muß diesen Trigger implementieren.

Die auslösende Seite

Nun ist es so, dass GitHub Actions erst einmal keine Build Pipelines in direkter Weise unterstützt, wie man es z. B. von Jenkins kennt. Es gibt aber neben regulären Events, wie Zeitsteuerung, Pushes und Pull Requests, auch die Möglichkeit, dass ein Workflow durch ein extern ausgelöstes Ereignis gestartet wird. Das Stichwort hierfür heißt repository_dispatch. Dies ist ein geeigneter Kandidat für individuelle Trigger. Um diese mittels etwaiger Bedingungen einzuschränken, können - wie bei allen Job-Schritten - beliebige Expressions mit dem Keyword if angegeben werden. Hier sind beliebige Bedingungen denk- und einsetzbar. Insbesondere interessant sind aber die sogenannten Check-Funktionen, welche das Ergebnis zuvor ausgeführter Job-Schritte auswerten.

Das repository_dispatch-Event wird über einen POST-Request an die GitHub-API ausgelöst. Damit man das in den Workflows nicht händisch tun bzw. selbst bauen muß, gibt es glücklicherweise es auch eine Action namens repository-dispatch, die zum Auslösen der Trigger eingesetzt werden kann.

Als Beispiel soll hier der Generator von ModuleStudio dienen: wenn die eigentliche Modellierungssprache (DSL) verändert hat, soll der Generator neu gebaut werden. Hierzu wird im Workflow des DSL-Repositories folgender Aufruf verwendet:

1
2
3
4
5
6
- name: Dispatch downstream job
  uses: peter-evans/repository-dispatch@master
  with:
    token: ${{ secrets.DISPATCH_TOKEN }}
    repository: Guite/MostGenerator
    event-type: upstream-build

Der gleiche Generator-Workflow wird auch durch das Pushen von Commits ausgelöst. Im folgenden Screenshot ist zu sehen, wie sich beide Varianten im Protokoll darstellen:

Unterschiedliche Trigger im Build Log

Während die Jobs mit dem Titel upstream-build durch einen Repository dispatch ausgelöst wurden, wurden die übrigen durch einen Commit gestartet.

Die reagierende Seite

Damit ein Workflow ausgeführt wird, muß er auf das genannte Event reagieren. Dies wird, wie bei anderen Events auch, durch das Schlüsselwort on zu Beginn des Workflows angegeben. Hier das Beispiel vom Generator:

1
2
3
4
5
on:
  push:
  pull_request:
  repository_dispatch:
    types: [upstream-build, manual-build]

Dieser Workflow wird also bei Push-Events und bei Pull-Requests, aber auch bei zwei eigenen Triggern - durch verschiedene Event-Typen gekennzeichnet - gestartet. Der Typ upstream-build wird für den oben beschriebenen Fall verwendet, dass eine Komponente A (upstream) eine weitere Komponente B (downstream) triggert. Der zweite Typ manual-build ist für einen anderen Anwendungsfall gedacht, der weiter unten in diesem Artikel vorgestellt wird.

Variante für Batch-Modus

Wenn der Generator neu gebaut worden ist, sollen mehrere Jobs gestartet werden, die jeweils ein Modul neu generieren und einen Pull Request für etwaige Neuerungen oder Änderungen erzeugen. In so einem Fall wäre es müßig, die gezeigte Action mehrfach hintereinander auszuführen. Statt dessen habe ich mich für diesen Fall für eine kleine Skriptlösung entschieden:

1
2
3
4
- name: Regenerate modules
  run: ./.github/scripts/regenerateModules.sh
  env:
    DISPATCH_TOKEN: ${{ secrets.DISPATCH_TOKEN }}

In dem Shell-Skript wird nun eine Schleife über die gewünschten Repositories durchlaufen und jeweils der Workflow ausgelöst:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#!/bin/bash

# list projects
PROJECTS=(
    "Guite/test-actions"
    "Guite/Awards"
    "zikula-modules/Content"
    "zikula-modules/MultiHook"
    "zikula-modules/Multisites"
    "zikula-modules/News"
    "zikula-modules/Pages"
    "zikula-modules/Ratings"
)

# loop through projects
for PROJECT in "${PROJECTS[@]}"
do
    echo "Trigger ${PROJECT}"
    curl POST -H "Authorization: token ${DISPATCH_TOKEN}" \
              -H "Accept: application/vnd.github.everest-preview+json"  \
              -H "Content-Type: application/json" \
              "https://api.github.com/repos/${PROJECT}/dispatches" \
              --data '{"event_type": "generator-updated"}' \
              --silent
done

Wie das im Ergebnis aussieht, lässt sich zum Beispiel hier anschauen:

Dispatching durch externe Trigger hilft bei der Automatisierung

Build Dashboard im Eigenbau

Hat man nun mehrere Komponenten miteinander verbunden, stellt sich noch die Frage, wie man am besten den Überblick behält. Da GitHub Actions immer in einem bestimmten Repository angesiedelt sind, fehlt eine projektübergreifende Gesamtsicht, wie sie in Jenkins auf der Startseite üblich ist.

Der Ansatz

Die Zutaten für solch eine Ansicht sind im Prinzip vorhanden, wir müssen sie nur zusammenfügen.

  • Es bietet sich an, die Datei README.md eines Haupt-Repositories zu zweckentfremden: in Markdown besteht die Möglichkeit, Dinge in einer Tabelle zu arrangieren. Dies bringt auch die Vorteile mit sich, dass es mit in die Versionierung wandert, nichts explizit programmiert werden muß und jederzeit leicht Änderungen vorgenommen werden können.
  • Jeder Workflow kann seinen Status mit einem Badge visualisieren (siehe Doku). Das ist dann einfach ein Bild, das typischerweise ohnehin in der Readme-Datei im jeweiligen Repo eingebunden und angezeigt wird. Wir verwenden es aber nun im Haupt-Repository.
  • Das wichtigste Element neben dem Status ist ein Build-Knopf. Denn es kann immer vorkommen, dass der Build einer bestimmten Komponente neu gestartet werden muß. Und hier kommt der oben gezeigte, zweite Event-Typ manual-build für das repository_dispatch-Event ins Spiel. Die Idee hier ist es, dass jeder Build-Knopf auf ein Skript verlinkt und die benötigten Parameter (im Prinzip nur das Repo, ggf. noch die Art des Workflows) via GET an dieses Skript übergibt.

Die Tabelle

Das Markdown ist relativ einfach aufgebaut:

1
2
3
4
| Component             | Status | Start |
| --------------------- | ------ | ----- |
| [Generator](https://github.com/Guite/MostGenerator) | [![Build Status](https://github.com/Guite/MostGenerator/workflows/Build%20component/badge.svg)](https://github.com/Guite/MostGenerator/actions?query=workflow%3A"Build+component") | [ :arrow_forward: ](https://.../dispatchJob.php?repo=Generator) |
| [Help](https://github.com/Guite/MostHelp) | [![Build Status](https://github.com/Guite/MostHelp/workflows/Build%20component/badge.svg)](https://github.com/Guite/MostHelp/actions?query=workflow%3A"Build+component") | [ :arrow_forward: ](https://.../dispatchJob.php?repo=Help) |

Der Play-Knopf wird über das Emoji ▶️ (: arrow_forward :) dargestellt. Wer ein anderes Symbol bevorzugt, wird sicherlich auf dieser Übersicht fündig.

Skript für einfaches Dispatching

Das Skript dispatchJob.php ist ebenfalls keine Raketenwissenschaft. Hier die wichtigsten Auszüge:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php

$repo = isset($_GET['repo']) ? $_GET['repo'] : '';
if (!in_array($repo, ['Generator', 'Help', '...'], true)) {
    die();
}

$project = 'Guite/Most' . $repo;
$eventType = 'manual-build';
// hier ggf. weitere Varianten für $eventType

$dispatchToken = '12345...';
$postData = [
    'event_type' => $eventType
];

// Beispiel für die Übergabe von Parametern an Workflows:
if ('CreateRelease' === $repo) {
    if (!isset($_GET['version'])) {
        die('<h1 style="background-color: red; padding: 100px 20px">Version parameter is missing! Append "&amp;version=1.2.3" or similar.</h1>');
    }
    $versionNumber = $_GET['version'];
    $postData['client_payload'] = [
        'version_number' => $versionNumber
    ];
}

$ch = curl_init();

curl_setopt($ch, CURLOPT_URL, 'https://api.github.com/repos/' . $project . '/dispatches');
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    'User-Agent: Job Dispatcher',
    'Authorization: token ' . $dispatchToken,
    'Accept: application/vnd.github.everest-preview+json',
    'Content-Type: application/json'
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($postData));

$server_output = curl_exec($ch);

curl_close($ch);

die('<p>Job started... <a href="https://github.com/' . $project . '/actions">look here</a></p>');

Das eigene Dashboard

Das Ergebnis der (doch sehr überschaubaren) Bemühungen sieht so aus:

Ein eigenes Build Dashboard mit GitHub Actions

Weitere Beiträge in Kategorie Builds & Tests

Abhängigkeiten automatisch aktualisieren mit Renovate
- Um Dependencies aktuell zu halten, wird bei GitHub oft und gerne der Dependabot eingesetzt. Mit Renovate existiert allerdings eine äußerst flexible Alternative, die im Folgenden kurz vorgestellt wird. …
Symfony-Projekte im Monorepo mit Nx bauen
- Mit dem Build-System Nx lassen sich beliebige Projekte in einem Monorepo auf einheitliche Weise testen und bauen. Es bedient sich verschiedener npm-Plugins, um dies für unterschiedliche Technologien …
Abhängigkeiten automatisch aktualisieren mit Dependabot
- Vor knapp eineinhalb Jahren haben wir gezeigt, wie man die Aktualisierung verwendeter Drittkomponenten mit GitHub Actions automatisieren kann. Zwischenzeitlich sind wir hierbei auf den Dependabot …
Hugo-Seiten im Netzwerk entwickeln und testen
- Hugo ist ein Generator für statische Seiten, welcher auf der Programmiersprache Go basiert. Die Inhalte werden mit Markdown verwaltet und dynamisch in HTML umgewandelt. Dies ist für die meisten …
SensioLabs Security Checker wurde eingestellt
- Zu den grundlegenden Aufgaben automatisierter Builds zählen unterschiedliche Prüfungen: so bietet es sich an, die Einhaltung der Coding-Styles zu überwachen, Qualitätsmetriken zu erheben und die …