Aus Linux-Magazin 03/2023

Watchbot für Git-Repositories implementieren

© Lukas Uher / 123RF.com

Um Software von Github zu installieren, muss man sie erst kompilieren und in ein Paketformat wie DEB oder RPM schnüren. Mit unserem verteilten System realisieren wir einen Bot, der uns benachrichtigt, wenn sich ein Repository verändert.

Versionskontrolle hat sich in der Softwareentwicklung durchgesetzt, und auch wer hobbymäßig Software entwickelt, verwendet mit sehr großer Wahrscheinlichkeit Git. Obwohl es ursprünglich für den Entwicklungsprozess des Linux-Kernels gedacht war, eignet es sich für Software jeder Größe; einen Großteil aller Projekte findet man bei Hostern wie Github oder Gitlab.

Ein Git-Repository enthält üblicherweise den Quellcode einer Anwendung, den es vor der Installation erst zu kompilieren gilt. Im Idealfall wollen wir Software aber nicht händisch installieren, sondern über den Paketmanager unserer Distribution. Deshalb schreiben wir im Folgenden ein verteiltes System, das aus Git-Repositories automatisiert Pakete baut und zur Installation bereitstellt. Dazu erstellen wir einen Bot – so nennen wir die autonomen Prozesse in dem verteilten System –, der Git-Repositories überwacht. Entsprechend seines Aufgabenbereichs soll er Watchbot heißen.

Verteilte Systeme in der Bash (Teil 1)

LM 06/2022, S. 76

https://www.lm-online.de/47902

Verteilte Systeme in der Bash (Teil 2)

LM 07/2022, S. 82

https://www.lm-online.de/47967

Framework für automatisierte Tests

LM 08/2022, S. 80

https://www.lm-online.de/48126

Komfortables Logging für die Bash

LM 09/2022, S. 78

https://www.lm-online.de/48193

Kommandozeilenparser im Eigenbau

LM 10/2022, S. 72

https://www.lm-online.de/48240

Mutexe und Semaphoren in der Bash

LM 11/2022, S. 80

https://www.lm-online.de/48402

Deadlock-freie Queues in der Bash

LM 12/2022, S.82

https://www.lm-online.de/48475

IPC mit den Mitteln der Bash

LM 01/2023, S.80

https://www.lm-online.de/48493

Daemons mit den Mitteln der Bash

LM 02/2023, S.78

https://www.lm-online.de/48647

Watchbot für Github-Repositories

LM 03/2023, S.78

https://www.lm-online.de/xxxxx

Geschichtsstunde

Ein Git-Repository besteht aus einem oder mehreren Branches, also Entwicklungszweigen, die wiederum einen oder mehrere Commits enthalten. Ein Commit ist eine Veränderung, die von einem Entwickler an die Geschichte eines Branches angehängt wurde. Commits lassen sichnur an das Ende eines Zweigs anhängen, sofern man nicht die gesamte Geschichte des Branches umschreibt. Den letzten Commit in einem Zweig bezeichnet man als Head. Beobachtet der Bot dessen Hash, kann er leicht feststellen, ob der Branch verändert wurde.

In einem lokalen Repository befindet sich im Verzeichnis ».git/refs/heads/« (beziehungsweise »refs/heads/« für mit »–bare« erstellte Repos) für jeden Branch eine Datei, die den Hash des letzten Commits enthält. Entsprechend unkompliziert fällt eine Funktion »fetch_head_local()« aus, die den Head in einem lokalen Repository ausliest (Listing 1).

Listing 1

Head auslesen

fetch_head_local() {
  local repository="$1"
  local branch="$2"
  local head
  if ! head=$(cat "$repository/.git/refs/heads/$branch" 2>/dev/null) &&
     ! head=$(cat "$repository/refs/heads/$branch" 2>/dev/null); then
     return 1
  fi
  echo "$head"
  return 0
}

Bei einem Projekt auf Github wollen wir aber nicht jedes Mal das komplette Repo herunterladen, nur um einen Hash auszulesen. Das ist auch nicht nötig, denn Git bietet dazu ein HTTP-basiertes Protokoll [1] an. So lässt sich etwa unter der URL »https://github.com/Projekt/info/refs?service=git-upload-pack« eine Liste aller Heads im Repository herunterladen.

Die Schwierigkeit dabei: Wir können die Antwort des Servers nicht direkt einer Shell-Variable zuweisen, da sie Binärdaten enthält. Statt der regulären Ausdrücke der Bash müssen wir also Grep bemühen. Listing 2 zeigt, dass das nicht weiter kompliziert ist, wenn man den richtigen Ausdruck kennt. In der Antwort des Servers stehen vier Hex-Ziffern vor den 40-stelligen Hashes. Verwenden wir Grep mit Perl-kompatiblen regulären Ausdrücken, können wir es mit »\K« anweisen, nur den nachfolgenden Teil des Treffers auszugeben. In diesem Fall umfasst das den Hash sowie den Pfad des Heads. Letzterer lässt sich mit einer simplen String-Expansion vom Ende des Strings entfernen.

Listing 2

Head per HTTP auslesen

fetch_head_http() {
  local repository="$1"
  local branch="$2"
  local re
  local url
  local data
  re="^00[0-9a-f]{2}\\K[0-9a-f]{40} refs/heads/$branch\$"
  url="$repository/info/refs?service=git-upload-pack"
  if ! data=$(curl --get --silent --location "$url" 2>/dev/null |
              grep -oP "$re" --binary-files=text); then
    return 1
  fi
  echo "${data%% *}"
  return 0
}

Mit diesen Funktionen können wir nun einem Branch eines Repositories einen Hash zuordnen, der seinen aktuellen Zustand beschreibt. Zudem brauchen bedarf es jedoch einer Möglichkeit, einem Branch einen Hash zuzuordnen, der seinen vorherigen Zustand beschreibt. Die Bash kennt zwar nicht viele Datentypen, aber für derartige Zuordnungen sind assoziative Arrays ideal. Wir merken uns also alle zuletzt gesehenen Hashes in dem Array. Als Index dient eine Zeichenkette bestehend aus der URL des Repositories und dem Namen des Zweigs, getrennt durch eine Raute.

Um beim Auslesen der Hashes nicht jedes Mal eine Fallunterscheidung machen zu müssen, implementieren wir bei dieser Gelegenheit noch eine Funktion »fetch_head()«. Ihr übergeben wir als einziges Argument ein Repository samt Branch entsprechend dem Schema »URL#Branch«. Enthält der String keine Raute (und somit keinen Branch), soll der Zweig »main« abgefragt werden. Die Implementierung dieser Funktion können Sie dem Begleitmaterial zu diesem Beitrag entnehmen.

Mehrfachoptionen

Repositories enthalten meist mehr als einen Zweig. Für das automatisierte Bauen von Paketen ist aber nur ein Branch interessant. Der Watchbot soll deshalb nicht alle Zweige eines Repos überwachen, sondern nur jene, die wir ihm via Befehlszeile mitgeteilt haben.

Der Bot erkennt Veränderungen, indem er in regelmäßigen Abständen den Zustand der Repositories abfragt. Je kürzer das Intervall, desto schneller erkennt er Veränderungen. Zu häufige Abfragen belasten jedoch das Netzwerk. Deshalb soll der Benutzer das Intervall über die Befehlszeile verändern können. Erkennt er eine Veränderung, soll der Bot eine PubSub-Nachricht an einen Topic senden. Dieser Topic soll sich ebenfalls per Option ändern lassen, aber eine sinnvolle Voreinstellung mitbringen.

In der Main-Funktion deklarieren wir daher die drei Optionen »–repository«, »–interval« und »–publish-to« so, wie wir es bereits im Beitrag über das Modul »opt« gesehen haben. Ein Repo muss entweder ein lokaler Pfad oder eine URL mit HTTPS-Protokoll sein, was wir mit einem einfachen regulären Ausdruck prüfen. Dabei darf der Benutzer mehrere Repositories auf der Kommandozeile angeben. Wir definieren daher ein Callback »_add_to_watchlist()«, das die übergebenen Werte an das globale Array »watchlist« anhängt.

Nachdem die Optionen erfolgreich ausgelesen wurden, bemühen wir die Funktion »inst_start()« (aus dem Modul»inst«), um eine Instanz des Bots zu starten. Der Hauptfunktion der Instanz, »watch_repos()«, geben wir die drei Optionen mit auf den Weg (Listing 3).

Listing 3

Watchbot initialisieren

main() {
  declare -g watchlist
  local publish_to
  local -i interval
  opt_add "r" "repository" "rv" ""        \
          "Zu ueberwachendes Repository"  \
          '^(/.*|https://.*)' _add_to_watchlist
  opt_add "p" "publish-to" "v"  "commits" \
          "Topic fuer Benachrichtigungen"
  opt_add "i" "interval" "v"    300       \
          "Sekunden zwischen Updates" '^[0-9]+$'
  if ! opt_parse "$@"; then
    return 1
  fi
  publish_to=$(opt_get "publish-to")
  interval=$(opt_get "interval")
  inst_start watch_repos "$publish_to" "$interval" "${watchlist[@]}"
  return 0
}

Kopf an Kopf

In der Hauptfunktion des Bots müssen wir lediglich über die Liste der zu beobachtenden Repositories iterieren und Veränderungen feststellen. Dazu deklarieren wir am Anfang der Funktion ein assoziatives Array »heads«, das den letzten Zustand aller überwachten Branches festhält.

Die Schleife fragt dann den Head eines Zweigs ab und vergleicht ihn mit dem im assoziativen Array gespeicherten Head. Da das Array beim ersten Durchlauf der Schleife noch leer ist, müssen wir auch testen, ob der Eintrag im Array gesetzt ist. Anderenfalls würde der Bot beim Start eine Veränderung in jedem Branch melden – manchmal kann das aber auch gewollt sein.

Ist der Wert im Array gesetzt und hat sich der Zustand des Branches seit dem letzten Test verändert, rufen wir die Funktion »send_notification()« auf, die eine PubSub-Nachricht versendet. Den Wert im Array aktualisieren wir aber nur dann, wenn das Versenden der Benachrichtigung erfolgreich war. Schlägt es fehl, versucht der Bot es im nächsten Durchgang erneut (Listing 4).

Listing 4

Kern des Watchbots

watch_repos() {
  local topic="$1"
  local interval="$2"
  local watchlist=("${@:3}")
  local endpoint
  declare -A heads
  if ! endpoint=$(ipc_endpoint_open); then
    return 1
  fi
  while inst_running; do
    local watch
    log_info "Pruefe ${#watchlist[@]} Repositories
 auf Updates"
    for watch in "${watchlist[@]}"; do
      local new_head
      if ! new_head=$(fetch_head "$watch"); then
        continue
      fi
      if [[ -n "${heads[$watch]}" ]] &&
         [[ "${heads[$watch]}" != "$new_head" ]]; then
        log_info "HEAD von $watch hat sich veraendert"
        if ! send_notification "$endpoint" "$topic" "$watch" "$new_head"; then
          log_warn "Kann nicht unter Topic $topic veroeffentlichen"
          continue
        fi
      fi
    heads["$watch"]="$new_head"
    done
    sleep "$interval"
  done
  ipc_endpoint_close "$endpoint"
  return 0
}

Gedankenübertragung

Hat der Watchbot eine Veränderung an einem Zweig festgestellt, gibt er mit einer PubSub-Nachricht allen interessierten Prozessen Bescheid. Wie schon bei der Implementierung des IPC-Moduls setzen wir dabei auf JSON als Nachrichtenformat. Die Funktion »send_notification()« generiert also ein JSON-Objekt, das neben der URL des Repositories und dem Namen des Branches auch den Hash des Commits enthält.

Wir geben dem Object auch ein Feld »type« mit dem Wert »commit«. Sollten wir die Nachrichten debuggen müssen, können wir so erkennen, um welche Art von Nachricht es sich handelt (Listing 5). Die Hilfsfunktionen »watch_to_repository()« und »watch_to_branch()« bestimmen die Adresse des Repositories beziehungsweise den Namen des Branches aus einer Repository-URL, wie sie vom Benutzer auf der Befehlszeile übergeben wurde. Da die beiden Funktionen keiner Erklärung bedürfen, ist sie hier nicht abgedruckt, Sie können sie dem Begleitmaterial entnehmen.

Listing 5

Nachrichten im JSON-Format

send_notification() {
  local endpoint="$1"
  local topic="$2"
  local watch="$3"
  local ref="$4"
  local repository
  local branch
  local msg
  repository=$(watch_to_repository "$watch")
  branch=$(watch_to_branch "$watch")
  msg=$(printf '{"type": "commit", "repository": "%s", "branch": "%s", "ref": "%s"}\n' "$repository" "$branch" "$ref")
  if ! ipc_endpoint_publish "$endpoint" "$topic" "$msg"; then
    return 1
  fi
  return 0
}

Den Bot speichern wir nun unter dem Pfad »/usr/local/share/dbs/watchbot.sh« und erstellen einen Symlink von »/usr/local/bin/watchbot« auf das Skript. Bevor wir den Bot auf ein Repository ansetzen, brauchen wir aber auch noch ein Skript, das die Nachrichten anzeigt, die der Watchbot veröffentlicht hat. Es abonniert einen auf der Befehlszeile definierten Topic, wartet auf eine Nachricht und gibt sie auf der Standardausgabe aus. Die Funktion »main()« zeigt Listing 6; das komplette Skript finden Sie im Begleitmaterial zum Artikel.

Listing 6

Nachrichtenempfänger

main() {
  local topic
  local endpoint
  opt_add "t" "topic" "rv" "" "The topic to subscribe to"
  if ! opt_parse "$@"; then
    return 1
  fi
  topic=$(opt_get "topic")
  if ! endpoint=$(ipc_endpoint_open); then
    return 1
  fi
  if ipc_endpoint_subscribe "$endpoint" "$topic"; then
    local msg
    if ! msg=$(ipc_endpoint_recv "$endpoint"); then
      log_error "Konnte Nachricht nicht empfangen"
    else
      ipc_msg_get_data "$msg"
    fi
  fi
  ipc_endpoint_close "$endpoint"
  return 0
}

Das Testskript speichern wir mit dem Namen »readcommit.sh« an einem beliebigen Ort und rufen es mit »./readcommit.sh –topic commits« auf. Anschließend klonen wir mit »git clone« ein beliebiges Repository von Github in ein temporäres Verzeichnis und weisen den Watchbot an, das lokale Repo zu beobachten (Listing 7).

Listing 7

Testlauf

$ git clone -b stable https://github.com/m10k/toolbox /tmp/toolbox
$ watchbot --repository /tmp/toolbox#stable --interval 15

Jetzt verändern wir das lokale Repository, um zu testen, ob der Watchbot die Modifikation erkennt. Die Änderung muss nicht groß sein; eine neue leere Datei genügt völlig. Im lokalen Repository führen wir die Befehle aus Listing 8 aus, um eine leere Datei zu erzeugen und an die Historie des Branches anzuhängen. Wer zum ersten Mal Git verwendet, muss möglicherweise Benutzernamen und E-Mail-Adresse konfigurieren, bevor ein Commit erzeugt werden kann. Dazu gibt Git aber eine informative Nachricht aus.

Listing 8

Watchbot-Test-Repo

$ touch leer
$ git add leer
$ git commit -m "Leere Datei hinzufuegen"

Der letzte der drei Befehle aus Listing 8 fügt einen neuen Commit am Ende des Zweigs ein. Da wir den Watchbot mit einem Update-Intervall von 15 Sekunden gestartet haben, sollte er die Veränderung innerhalb weniger Sekunden entdecken, sodass das Testskript den Inhalt der Benachrichtigung auf der Standardausgabe darstellt. Das Ergebnis ähnelt der Ausgabe in Abbildung 1.

Abbildung 1: Das Testskript (oben) empfängt Nachrichten von Watchbot.

Abbildung 1: Das Testskript (oben) empfängt Nachrichten von Watchbot.

An dieser Stelle wäre es schön gewesen, eine Änderung in einem Repository bei einem Hoster zu beobachten. Doch selbst bei beliebten Projekten kann es Stunden bis Tage dauern, bis ein Branch verändert wird. Wer bei Github oder Gitlab registriert ist, kann den Watchbot an einem persönlichen Projekt ausprobieren – oder dem Autor vertrauen, dass er den Bot ausreichend getestet hat.

Ausblick

Dank des Watchbots können wir nun sehr flexibel auf Veränderungen in einem Git-Repository reagieren. Als Nächstes brauchen wir einen Bot, der den Code aus dem Git-Repo in ein Debian-Paket verpackt. Darum wird es in der nächsten Folge gehen. (jcb)

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben