- 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.