4 Punkte von GN⁺ 2025-04-09 | 2 Kommentare | Auf WhatsApp teilen
  • shell in GitHub Actions ist eine Einstellung, die festlegt, wie ein run:-Block ausgeführt wird, tatsächlich kann damit aber nicht nur eine vordefinierte Shell-Liste, sondern auch eine ausführbare Datei aus $PATH gemeint sein
  • In Workflows ist sie optional, in Aktionsdefinitionen Pflicht; der Standardwert hängt vom Runner ab, z. B. bash unter Linux/macOS und pwsh unter Windows
  • Wenn man shell: bash explizit angibt, fügt GitHub zusätzliche Flags wie bash --noprofile --norc -eo pipefail an, das eigentliche Ausführungsziel kann aber trotzdem ein beliebiges Programm sein
  • Wenn die ausführbare Datei keine einzelne Dateieingabe akzeptiert, muss der shell-Wert das Argument {0} enthalten; GitHub ersetzt es durch den Pfad zu einer temporären Datei mit dem Inhalt des run-Blocks
  • Auch vertraute Namen wie bash werden über $PATH aufgelöst, sodass Änderungen an $GITHUB_PATH oder gefälschte ausführbare Dateien beeinflussen können, wie spätere Schritte ausgeführt werden

Grundverhalten des Schlüsselworts shell

  • Das Schlüsselwort shell in GitHub Actions legt fest, mit welcher Shell ein bestimmter run:-Block ausgeführt wird
  • In Workflows ist es optional, in Aktionsdefinitionen jedoch erforderlich
  • Die Standard-Shell wird durch die Runner-Umgebung bestimmt
    • Unter Linux und macOS normalerweise bash
    • Unter Windows normalerweise pwsh

Explizite Shell-Angabe und zusätzliche Flags

  • In der Dokumentation zu defaults.run.shell weist GitHub darauf hin, dass bei expliziter Angabe der Shell auch von GitHub gewählte Flags angewendet werden
  • Wenn man zum Beispiel shell: bash angibt, sieht die Ausführung so aus
    • bash --noprofile --norc -eo pipefail
  • Betrachtet man nur dieses Verhalten, liegt die Annahme nahe, dass GitHub eine eingeschränkte Liste gültiger Shell-Werte verwaltet und nur diesen Werten spezielle Flags hinzufügt

Beliebige ausführbare Datei aus $PATH kann ebenfalls eine Shell sein

  • Tatsächlich kann man in shell jede beliebige ausführbare Datei angeben, die sich in $PATH befindet
  • GitHub führt den run-Block mit der angegebenen ausführbaren Datei aus
  • Wenn der betreffende Befehl nicht direkt eine einzelne Dateieingabe akzeptiert, muss man {0} im shell-Wert übergeben
    • GitHub ersetzt {0} durch den Pfad zu einer temporären Datei
    • Diese temporäre Datei enthält den Inhalt des per Template erweiterten run-Blocks

Beispiel: C als Step-Runner verwenden

  • Auch Werkzeuge, die wie ein C-Compiler oder -Interpreter arbeiten, lassen sich zum Ausführen von GitHub-Actions-Schritten verwenden
  • Das Beispiel mit tcc -run {0} als shell funktioniert korrekt
- run: sudo apt install -y tcc
- shell: tcc -run {0}
  run: |
    #include <stdio.h>
    int main() {
      printf("Hello, world!\n");
      return 0;
    }

Beispiel: Shell-Auflösung über $GITHUB_PATH verändern

  • Wenn man $PATH dynamisch über $GITHUB_PATH verändert, kann shell: bash später auf eine andere ausführbare Datei zeigen als erwartet
  • Im Beispiel wird im aktuellen Verzeichnis eine ausführbare Datei namens bash erstellt und das aktuelle Verzeichnis zu $GITHUB_PATH hinzugefügt
  • Verwendet man danach shell: bash, kann GitHub die über $PATH gefundene gefälschte bash ausführen
- run: |
    touch ./bash
    chmod +x ./bash
    echo '#!/bin/sh' > ./bash
    echo 'echo hello from fake bash' >> ./bash
    echo "${PWD}" >> "${GITHUB_PATH}"
- run: |
    echo "this doesn't do what you expect"
  shell: bash

Unerwarteter Punkt aus Sicherheitssicht

  • Es ist schwer eindeutig zu sagen, wie sicherheitsrelevant dieses Verhalten ist
  • In GitHub Actions gibt es bereits viele Wege, auf denen Dateischreibzugriffe zur Ausführung führen können; GITHUB_ENV gehört ebenfalls zu solchen Fällen
  • Dennoch kann es unerwartet sein, dass GitHub selbst bei bekannten Shell-Werten wie bash eine $PATH-Suche durchführt
  • Vor allem wenn GitHub abhängig von bekannten Shell-Werten Kommandozeilen-Flags einfügt, könnte man erwarten, dass bash auf einen bestimmten Pfad wie /bin/bash festgelegt wird
  • Allgemeiner könnte man annehmen, dass GitHub nur vorab im Tool-Cache registrierte Werkzeuge zulässt, das beobachtete Verhalten verwendet jedoch ausführbare Dateien aus $PATH

2 Kommentare

 
tujuc 2025-04-09

Schon ein Blick in das Repository github/runner-image zeigt, dass dort ziemlich viele Pakete vorinstalliert sind, die man direkt nutzen kann....

Wenn man ein Image baut, sind 1 GB ganz schnell erreicht....

 
GN⁺ 2025-04-09
Meinungen auf Hacker News
  • Ich habe früher einmal das Flag -x verwendet, damit bash in Actions-Workflows alle ausgeführten Befehle ausgibt, und das war beim Debugging ziemlich hilfreich.
    https://github.com/jstrieb/just.sh/blob/2da1e2a3bfb51d583be0...
    • Hinweis: Wenn man pipefail aktiviert, schlägt der Schritt fehl, sobald ein Teil der Pipe fehlschlägt, aber es gibt keine Fehlerausgabe, sodass man eventuell nicht weiß, warum er fehlgeschlagen ist.
      pipefail schützt auch nicht vor komplexeren Fehlerzuständen. In einem Schritt wie curl ... | sudo tar ... in der Konfiguration werden zum Beispiel ein fehlgeschlagenes Entpacken durch tar, ein fehlgeschlagenes sudo oder Shell-Fehler sichtbar. Wenn curl aber wegen eines Netzwerkfehlers mittendrin fehlschlägt, kann tar das just-Binary nur zur Hälfte entpacken und die Shell sofort beenden. Dann gibt es keine Fehlermeldung, und eine beschädigte ausführbare Datei bleibt auf der Platte zurück; wenn das Überspringen von Fehlern aktiviert ist, kann sogar versucht werden, sie auszuführen.
  • Ein cooler privater GitHub-Actions-Trick, den ich bei der Arbeit gesehen habe: Man kann im Namen von repository_dispatch-Events mit Wildcards matchen.
    Man kann es etwa so schreiben: on: repository_dispatch: - security_scan - security_scan::*. Wenn man die Release-Pipeline zentralisiert, kann man erzwingen, dass jedes Repository über die definierten wiederverwendbaren Workflows läuft. Und wenn man Events wie security_scan::$product_name::$version sendet, lässt sich im Actions-Tab des zentralen Release-Repositories viel leichter erkennen, für welches Produkt und welche Version gerade ein Workflow läuft.
    • Mich würde interessieren, ob dieser zentralisierte Ansatz in der Praxis wirklich gut funktioniert hat. Verlangt eure Organisation, dass alles exakt auf dieselbe Weise gebaut wird?
      Ich bin gerade zu einer Organisation gestoßen, die etwas Ähnliches versucht, aber in der Praxis wirkt es fast nutzlos. Die Templates gehen häufig kaputt und sind oft ohne Dokumentation dazu geschrieben, wie der Code gebaut werden soll; stattdessen setzen sie eine bestimmte Build-Methode voraus.
  • Meiner Meinung nach ist es besser, je weniger man in GitHub Actions selbst macht.
    Normalerweise bevorzuge ich es, die Logik in ein Build-System wie Make zu legen und sie aus GitHub Actions nur aufzurufen, oder ein kleines Kommandozeilenprogramm zu schreiben und dieses aufzurufen. Solche Dinge lassen sich lokal viel einfacher debuggen als in CI. Ein interessanter Trick, aber ich bin mir nicht sicher, wofür er nützlich wäre.
    • Unsere Workflows sind im Grunde nur make build und make test.
      Nach der Übernahme habe ich mir die Workflow-Dateien des übernehmenden Unternehmens angesehen: Hunderte Zeilen, mit vielen Wiederholungen. Nennt mich altmodisch, aber aus dem YAML-Dorf möchte ich so schnell wie möglich raus.
    • Ich glaube, der Autor empfiehlt nicht, das zu tun, sondern weist darauf hin, dass es möglich ist.
      Zu wissen, was in einem System möglich ist, ist für Security und Debugging nützlich, selbst wenn man eine Funktion nicht wirklich nutzen will.
    • Nach meinem Verständnis ist das nicht nützlich, ganz sicher nicht. Es birgt aber ein potenzielles Sicherheitsrisiko.
      Besonders, wenn man selbst gehostete Runner „erkundet“.
  • Unsere Generation hatte Angst, wenn sie gebeten wurde, sich ständig ändernde Spreadsheets in Code zu übertragen.
    Die nächste Generation wird zittern, wenn sie gebeten wird, Deployments aus GitHub Actions zu disziplinieren.
    • Ich arbeite gerade daran, ein großes Enterprise auf GH Actions umzustellen, genauer gesagt eher auf „YAML-basierte Pipelines, die an Git gebunden sind“. Wie sollte Disziplin dabei aussehen?
      Wenn du das erklären kannst, könnten wir es in unserer Organisation wahrscheinlich umsetzen.
    • Mich würde interessieren, warum das undiszipliniert sein soll.
  • Man kann auch die Standard-Shell bash austricksen, sodass sie ein beliebiges Programm ausführt.
  • Wenn andere Leute, die die Action lesen, verstehen, was passiert, wirkt das ziemlich nützlich.
    Mir ist es oft passiert, dass ein Shell-Skript, das fast unverändert aus ein paar von Hand eingegebenen Befehlen entstanden ist, zu einem Monster mit über hundert Zeilen wurde. Jedes Mal hätte ich mir echte Arrays und Typen sowie die Batterien inklusive der Python-Standardbibliothek gewünscht. Trotzdem werde ich die Build-Action meiner Firma nicht in elisp implementieren.
  • Der Code des Github Actions Runner ist ziemlich gut lesbar. Die Stelle, an der die Standardargumente für beliebte Shells/Binaries definiert sind, ist hier: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    Sie wird über die Methode ScriptHandlerHelpers.GetScriptArgumentsFormat offengelegt. In ScriptHandler.cs findet sich der Code zur Vorbereitung der Prozessumgebung und der Argumente, und der tatsächliche Code zum Starten des Prozesses ist hier: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    Insgesamt war ich von der Einfachheit dieses Codes positiv überrascht. Er ist sehr prozedural und behandelt zahlreiche Sonderfälle, wirkt aber leicht verständlich und gut zu debuggen.
  • Jetzt können wir endlich C in der Produktions-CI/CD verwenden und es „Low-Level-Systemarbeit“ nennen.
    Assembly dürfte auch gehen.
  • Das lässt mich hoffen, dass man mit goeval [1] Go-Code direkt als CI-Job in einer GitHub-Workflow-YAML-Datei ausführen kann.
    Allerdings unterstützt goeval derzeit keine direkte Dateieingabe, sondern nur Standardeingabe, daher braucht es einen Shell-Trick. Aktuell läuft es etwa so: go run github.com/dolmen-go/goeval@v1 - <<'EOF' ... EOF, was ein wenig Boilerplate erfordert. Nebenbei: Ich bin der Autor von goeval.
    [1] https://github.com/dolmen-go/goeval
    • Ich verstehe nicht, warum ein Shell-„Trick“ nötig ist. Geht nicht go run github.com/dolmen-go/goeval@v1 - < file.go?
    • Ich bin zum Repository gegangen, um zu sehen, ob es ein Programm ist, das Leerzeichen automatisch in Tabs umwandelt, aber es scheint eher auf Einzeilen-Ausdrücke ausgerichtet zu sein.
      Wenn man in diesem Kontext ein Spielzeugprojekt bewerben will, hätte ein relevanteres Beispiel als „hello ausgeben“ mir den Klick erspart.
  • Was ist an GitHub-CI-YAML besser als ein bash-Skript wie run: pipeline.sh?
    • Es gibt automatisch verwaltete GitHub-API-Tokens, die für Release-Automatisierung sowie das Veröffentlichen von Artefakten und Containern nützlich sind.
      Man kann Jobs gleichzeitig auf mehreren Betriebssystemen und CPU-Architekturen laufen lassen und erhält Kontextinformationen zum auslösenden Event und zum Zustand des Repositories. Das ist praktisch für PR-spezifische Jobs oder Release-Automatisierung, und es lässt sich auch in die GitHub-Web-UI integrieren, etwa wenn ein Linter Probleme direkt an den jeweiligen Zeilen eines PRs markiert oder Testfehlschläge auf einer Webseite gerendert werden. Kleine Caches kann man ebenfalls nutzen, damit unveränderte Dateien nicht erneut heruntergeladen oder neu gebaut werden müssen. Idealerweise steckt man so viel wie möglich in normale Tools, die lokal laufen, und lässt die GitHub-CI-Konfiguration nur den Klebecode für Trigger, Caching und GitHub-Integration übernehmen.
    • In der GitHub-UI sieht man eine Übersicht der einzelnen Schritte und kann die Ausgabe pro Schritt ein- oder ausklappen.
      Man kann GitHub Actions anderer Leute leicht in der Pipeline nutzen, Workflows modularisieren und Abhängigkeiten sowie parallele Ausführung steuern. Es gibt sicher noch mehr, aber der Hauptvorteil ist, dass man diese Dinge nicht selbst implementieren muss.
    • Aus GitHubs Sicht hat es den Vorteil, dass die Wahrscheinlichkeit sinkt, dass Nutzer zu einer anderen Git-Forge wechseln.