2 Punkte von GN⁺ 2025-03-21 | Noch keine Kommentare. | Auf WhatsApp teilen
  • Beim Zurückmigrieren einer komplexen CI zu GitHub Actions zeigte sich, dass merge queue, mehrere Runner, Rust-Builds, Docker-Images und Integrationstests so stark ineinandergreifen, dass die Debugging-Kosten größer sind als der Konfigurationsaufwand
  • Alle Änderungen, die in main gelangen, müssen alle Tests bestehen, und kleine Fehler wie Formatierung, ungenutzte Abhängigkeiten oder Lint-Probleme sollen automatisch behoben werden; außerdem müssen die CI-Artefakte mit den Release-Artefakten identisch sein
  • Um sowohl die Prüfung vor als auch nach der merge queue zu erzwingen, mussten die Job-Namen in beiden Phasen identisch sein; andernfalls können Checks hängen bleiben oder fehlerhafte Änderungen gemergt werden
  • Durch das Zusammenspiel von GITHUB_TOKEN, Workflow-permissions, benutzerdefinierten Tokens sowie Ausnahmen bei Forks und self-hosted runner ist das Sicherheitsmodell schwer zu verstehen und fehleranfällig
  • Docker-Container-Ausführung, YAML-Workflows und eingeschränkte lokale Tests haben die Entwicklung verlangsamt, aber das neue CI-Skript verkürzt die Merge-Zeiten deutlich

Eine komplexe CI zurück auf GitHub Actions

  • In den letzten zwei Wochen wurde das CI-Skript wieder in GitHub Actions neu geschrieben
  • Dies ist bereits der dritte Umbau der CI-Konfiguration
    • Zuerst GitHub Actions
    • Danach Umstieg auf Earthly
    • Nachdem Earthly eingestellt wurde, zurück zu GitHub Actions
  • Die aktuelle CI umfasst merge queue, mehrere Runner, Rust-Builds, Docker-Images und schwere Integrationstests
    • Verwendet werden self-hosted, blacksmith.sh und GitHub-hosted Runner gemeinsam
    • Beim Mergen eines einzelnen PRs fallen über mehrere parallele Runner hinweg etwa 1 Stunde CI-Zeit an

Anforderungen an die CI

  • Alle Änderungen, die in main gelangen, müssen alle Tests bestehen
  • Kleine Fehler wie Formatierung, ungenutzte Abhängigkeiten oder Lint-Probleme sollen nicht einfach nur fehlschlagen, sondern automatisch behoben werden
  • Die in der CI getesteten Artefakte müssen mit den tatsächlichen Release-Artefakten identisch sein
  • Für eine gute Developer Experience muss die CI schnell fertig werden
  • GitHub Actions unterstützt diese Anforderungen technisch, aber die Einrichtung bringt versteckte Fallstricke, inkonsistentes Verhalten und schwieriges Debugging mit sich

Merge queue und Status-Checks

  • Der Schlüssel zu einem sauberen main-Branch ist GitHubs merge queue
    • Die merge queue rebased PRs vor dem CI-Lauf auf main
  • Die nötigen CI-Ausführungen teilen sich in zwei Phasen
    • Vor dem Eintritt in die Queue läuft CI, um kleine Probleme automatisch zu beheben
    • Innerhalb der Queue läuft CI erneut, um den finalen Merge-Zustand zu validieren
  • Damit in GitHub Actions beide Läufe verpflichtend sind, müssen die Job-Namen in beiden Phasen identisch sein
    • GitHub behandelt beide Ausführungen dann als denselben Check, sodass beide erfolgreich sein müssen, bevor gemergt werden kann
    • Diese Lösung wurde erst nach mehreren Stunden Debugging über eine Stack-Overflow-Antwort gefunden
  • Andere Ansätze können dazu führen, dass Status-Checks vor dem Queue-Eintritt im Wartezustand bleiben und Jobs nicht starten, oder dass Jobs, die innerhalb der merge queue fehlschlagen müssten, trotz Fehlern gemergt werden

Das schwer verständliche Sicherheitsmodell von GitHub Actions

  • Nach dem Kompromittierungsfall einer kürzlich populären GitHub Action lautete die Reaktion: „Abhängigkeiten per Hash pinnen“, doch in den Kommentaren war die häufige Reaktion, dass das fast niemand tut
  • GitHub Actions hat ein Standard-Token namens GITHUB_TOKEN
    • Dieses Token wird mit Standardberechtigungen initialisiert
    • Die Standardberechtigungen lassen sich in den Repository-Einstellungen unter Actions → General → Workflow Permissions festlegen
  • Wenn die Standardberechtigungen des GITHUB_TOKEN restriktiv sind, müssen sie für benötigte Actions und Befehle erweitert werden; sind sie großzügig, können im Workflow-File einzelne Berechtigungen entzogen werden
  • Die besseren Defaults wären keine Berechtigungen als Ausgangspunkt, wobei Nutzer nur die wirklich nötigen Rechte hinzufügen
  • Es gibt viele Berechtigungsarten, und ohne tiefes GitHub-Wissen ist schwer zu erkennen, was jede einzelne eigentlich schützt

Tokens und Berechtigungs-Ausnahmen

  • Für die automatische Erstellung von GitHub-Releases mit softprops/action-gh-release wird das benutzerdefinierte Token CI_RELEASE verwendet
- name: Release on GitHub
  if: env.version_exists == 'false'
  uses: softprops/action-gh-release@v2
  with:
    tag_name: v${{ env.CURRENT_VERSION }}
    generate_release_notes: true
    make_latest: true
    token: ${{ secrets.CI_RELEASE }}
  • Mit dem Standard-Token funktioniert zwar das Release selbst, aber nach dem Release wird kein Workflow ausgelöst
    • Da es dafür keinen sichtbaren Hinweis gibt, musste ein Issue von jemand anderem mit demselben Problem gefunden werden, um die Ursache zu verstehen
  • Berechtigungen lassen sich auch direkt im Workflow-YAML erhöhen
    • Es wirkt ungewohnt, Berechtigungen innerhalb des Codes zu erhöhen, den man eigentlich schützen will
  • Laut GitHub-Dokumentation kann man mit dem Schlüssel permissions Leserechte für Fork-Repositories hinzufügen oder entfernen, Schreibrechte aber normalerweise nicht gewähren
    • Eine Ausnahme ist, wenn ein Admin in den GitHub-Actions-Einstellungen die Option Send write tokens to workflows from pull requests aktiviert
  • Wegen der vielen Ausnahmen und Fallstricke ist das Sicherheitsmodell von GitHub Actions zwar mächtig, vergrößert aber zugleich die Angriffsfläche und die Fehlerwahrscheinlichkeit

Die Unsicherheit bei self-hosted runnern

  • GitHub empfiehlt in der Dokumentation nicht, self-hosted runner in öffentlichen Repositories zu verwenden
    • Forks öffentlicher Repositories könnten sonst über PRs gefährlichen Code auf der self-hosted-runner-Maschine ausführen
  • GitHub bietet auch eine self-hosted-runner-Einstellung, bei der die Ausführung von PRs externer Mitwirkender erst genehmigt werden muss
  • Ob diese Einstellung zusammen mit self-hosted runnern tatsächlich sicher ist, wird in der Dokumentation nicht klar beantwortet, und auch im Internet gibt es keinen Konsens
  • Wegen der hohen Komplexität bleibt ein Zustand, in dem man sich nie zu 100 % sicher sein kann

Wenn Docker und GitHub Actions aneinandergeraten

  • GitHub Actions kann Jobs in einem Container ausführen
    • Das hat den Vorteil, dass Abhängigkeiten nicht jedes Mal neu installiert werden müssen, weil sie im Dev-Container vorverpackt sein können
  • In der Praxis treten jedoch wiederholt Dateiberechtigungsprobleme auf
    • Der Container baut Dateien mit einem Benutzer, während der GitHub-Runner mit einer anderen UID/GID laufen kann
    • Dadurch kann der Zugriff auf Dateien im Container, im GitHub-Workspace oder in temporären Host-Verzeichnissen scheitern
  • Auch das $HOME-Verzeichnis kann nicht übereinstimmen
    • Der Dev-Container installiert Tools möglicherweise unter /home/ubuntu
    • Innerhalb von GitHub Actions kann $HOME dagegen /github/home sein
    • Tools, die auf Dateien unter $HOME angewiesen sind, finden dann die nötigen Dateien nicht
  • Auch Actions, die mit dem Host-System interagieren, können kaputtgehen
    • Der GitHub-Cache ist auf 10 GB begrenzt, daher wird mit der Sticky-Disk-Action von blacksmith ein NVMe-Laufwerk zum Caching gemountet
    • Im Container funktionierte das nicht und wurde erst nach einer Anpassung von blacksmith.sh behoben
  • Auch das container-Feld selbst hat Einschränkungen
    • Der entrypoint kann nicht überschrieben werden
    • Es ist auch nicht möglich, nur einige Steps im Container und andere außerhalb laufen zu lassen

Entwicklung und Debugging von YAML-Workflows

  • Die Logik von GitHub Actions wird in YAML geschrieben, und je komplexer sie wird, desto leichter entstehen Fehler
  • Die GitHub-YAML-Linter-Prüfung in RustRover war hilfreich, aber es braucht bessere statische Analyse
  • Lokal lässt sich das tatsächliche CI-Verhalten nur schwer ausreichend testen
    • act ist ein bekanntes Tool, unterstützt aber nur einen kleinen Teil dessen, was hier in der CI gemacht werden soll
  • Die beste Debugging-Methode war letztlich, ein zweites identisches Repository anzulegen und so lange git commit -a -m "wip" && git push test-ci branch zu wiederholen, bis die CI sich wie erwartet verhielt

Workflow-Aufteilung und Wiederverwendung von Artefakten

  • Damit nicht jedes Mal die komplette CI-Pipeline laufen muss, werden einzelne Workflows klein gehalten
  • Am Ende jedes Workflows werden Artefakte hochgeladen, damit spätere Workflows sie herunterladen können, statt alles von vorn neu zu bauen
  • Dadurch lassen sich Workflows isoliert testen, indem Artefakte aus früheren Läufen heruntergeladen werden
    • Beim Herunterladen aus einem früheren Lauf muss der download-artifact-Action allerdings ein Token übergeben werden
    • Dieses Token kann zwar das Standard-Token sein, aber warum es trotzdem explizit angegeben werden muss, bleibt eine offene Frage
  • Die Haupt-Workflow-Datei wird so zu einer Kette von Aufrufen anderer YAML-Dateien
jobs:
  invoke-build-rust:
    name: Build Rust
    uses: ./.github/workflows/build-rust.yml
  invoke-build-java:
    name: Build Java
    uses: ./.github/workflows/build-java.yml
  invoke-tests-unit:
    name: Unit Tests
    needs: [invoke-build-rust, invoke-build-java]
    uses: ./.github/workflows/test-unit.yml
  invoke-tests-adapter:
    name: Adapter Tests
    needs: [invoke-build-rust]
    uses: ./.github/workflows/test-adapters.yml
    secrets: inherit
  invoke-build-docker:
    name: Build Docker
    needs: [invoke-build-rust, invoke-build-java]
    uses: ./.github/workflows/build-docker.yml
  invoke-tests-integration:
    name: Integration Tests
    needs: [invoke-build-docker]
    uses: ./.github/workflows/test-integration.yml
  invoke-tests-java:
    name: Java Tests
    needs: [invoke-build-java]
    uses: ./.github/workflows/test-java.yml
  • Für einige Jobs ist secrets: inherit nötig
    • Wenn ein Workflow einen anderen Workflow aufruft, werden Secrets nicht standardmäßig mitgegeben
    • Das war die Ursache eines Problems, das in der gesamten CI-Pipeline fehlschlug, bei der Ausführung einzelner Steps aber funktionierte

Schnellere Merges, weiterhin teures Debugging

  • Das neue CI-Skript verkürzt die Merge-Zeiten deutlich, und mit dem Ergebnis ist man zufrieden
  • Allerdings hat es viel zu viel Zeit gekostet, dorthin zu kommen, und bei Problemen müsste das Debugging deutlich einfacher werden

Noch keine Kommentare.

Noch keine Kommentare.