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
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....
Meinungen auf Hacker News
-xverwendet, 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...
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 durchtar, ein fehlgeschlagenessudooder Shell-Fehler sichtbar. Wenncurlaber wegen eines Netzwerkfehlers mittendrin fehlschlägt, kanntardasjust-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.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 wiesecurity_scan::$product_name::$versionsendet, 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.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.
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.
make buildundmake 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.
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.
Besonders, wenn man selbst gehostete Runner „erkundet“.
Die nächste Generation wird zittern, wenn sie gebeten wird, Deployments aus GitHub Actions zu disziplinieren.
Wenn du das erklären kannst, könnten wir es in unserer Organisation wahrscheinlich umsetzen.
bashaustricksen, sodass sie ein beliebiges Programm ausführt.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.
Sie wird über die Methode
ScriptHandlerHelpers.GetScriptArgumentsFormatoffengelegt. InScriptHandler.csfindet 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.
Assembly dürfte auch gehen.
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
go run github.com/dolmen-go/goeval@v1 - < file.go?Wenn man in diesem Kontext ein Spielzeugprojekt bewerben will, hätte ein relevanteres Beispiel als „hello ausgeben“ mir den Klick erspart.
run: pipeline.sh?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.
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.