- Dans GitHub Actions,
shell est un paramètre qui détermine la façon d’exécuter un bloc run:, mais en pratique il ne se limite pas à une liste prédéfinie de shells et peut aussi viser des exécutables présents dans le $PATH
- Dans un workflow, ce paramètre est optionnel, mais il est obligatoire dans une définition d’action, et sa valeur par défaut varie selon le runner, comme
bash sur Linux/macOS et pwsh sur Windows
- Si l’on précise
shell: bash, GitHub ajoute des flags supplémentaires comme bash --noprofile --norc -eo pipefail, mais la cible exécutée peut malgré tout être un programme arbitraire
- Si l’exécutable ne prend pas directement une entrée sous forme de fichier unique, il faut ajouter l’argument
{0} dans la valeur de shell ; GitHub le remplace alors par le chemin d’un fichier temporaire contenant le bloc run
- Même des noms familiers comme
bash sont résolus via le $PATH, si bien qu’une modification de $GITHUB_PATH ou un faux exécutable peut influencer la façon dont les étapes suivantes sont lancées
Fonctionnement de base du mot-clé shell
- Dans GitHub Actions, le mot-clé
shell sert à indiquer avec quel shell exécuter un bloc run: donné
- Dans un workflow, il est optionnel, mais il est obligatoire dans une définition d’action
- Le shell par défaut dépend de l’environnement du runner
- Sur Linux et macOS, c’est généralement
bash
- Sur Windows, c’est généralement
pwsh
Définition explicite du shell et flags supplémentaires
- Dans la documentation de
defaults.run.shell, GitHub indique que si l’on précise un shell, les flags choisis par GitHub sont aussi appliqués
- Par exemple, si l’on indique
shell: bash, la forme d’exécution devient la suivante
bash --noprofile --norc -eo pipefail
- À ne regarder que ce comportement, on pourrait facilement penser que GitHub gère une liste limitée de valeurs de shell valides et n’ajoute des flags spéciaux qu’à ces valeurs
N’importe quel exécutable du $PATH peut aussi servir de shell
- En réalité, on peut indiquer dans
shell n’importe quel exécutable présent dans le $PATH
- GitHub exécute alors le bloc
run via l’exécutable indiqué
- Si cette commande n’accepte pas directement une entrée sous forme de fichier unique, il faut transmettre
{0} dans la valeur de shell
- GitHub remplace alors
{0} par le chemin d’un fichier temporaire
- Ce fichier temporaire contient le contenu du bloc
run après expansion du template
Exemple d’utilisation de C comme runner d’étape
- Des outils qui se comportent comme un compilateur/interpréteur C peuvent eux aussi servir à exécuter une étape GitHub Actions
- L’exemple qui définit
tcc -run {0} comme shell fonctionne correctement
- run: sudo apt install -y tcc
- shell: tcc -run {0}
run: |
#include <stdio.h>
int main() {
printf("Hello, world!\n");
return 0;
}
Exemple de modification de la résolution du shell avec $GITHUB_PATH
- Si l’on modifie dynamiquement le
$PATH via $GITHUB_PATH, un shell: bash ultérieur peut pointer vers un exécutable différent de celui attendu
- L’exemple crée un exécutable nommé
bash dans le répertoire courant, puis ajoute ce répertoire à $GITHUB_PATH
- Ensuite, l’usage de
shell: bash peut amener GitHub à lancer le faux bash trouvé dans le $PATH
- 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
Un comportement qui peut surprendre du point de vue de la sécurité
- Il est difficile d’affirmer à quel point ce comportement est important en matière de sécurité
- Dans GitHub Actions, il existe déjà de nombreux chemins où une écriture de fichier peut mener à une exécution, et
GITHUB_ENV en fait aussi partie
- Cela dit, le fait que GitHub effectue une recherche dans le
$PATH même pour des valeurs de shell bien connues comme bash peut être inattendu
- En particulier, si GitHub injecte des flags de ligne de commande selon la valeur d’un shell connu, on pourrait s’attendre à ce que
bash soit fixé à un chemin précis comme /bin/bash
- Plus généralement, on pourrait aussi penser que GitHub n’autorise que des outils préenregistrés dans le tool cache, mais le comportement observé consiste bien à utiliser les exécutables présents dans le
$PATH
2 commentaires
Rien qu’en regardant le dépôt github/runner-image, on voit qu’un bon nombre de packages directement utilisables sont déjà installés....
Quand on crée une image, 1 Go partent facilement....
Commentaires sur Hacker News
-xpour faire afficher toutes les commandes exécutées par bash dans un workflow Actions, et c’est assez utile pour le debugginghttps://github.com/jstrieb/just.sh/blob/2da1e2a3bfb51d583be0...
pipefail n’empêche pas non plus des états d’erreur plus complexes. Par exemple, dans l’étape
curl ... | sudo tar ...de la configuration, un échec d’extraction detar, un échec desudoou une erreur du shell seront visibles, mais sicurléchoue en cours de route à cause d’une erreur réseau,tarpeut n’extraire que la moitié du binairejustet le shell peut se terminer immédiatement. Dans ce cas, il n’y a aucun message d’erreur, un exécutable corrompu reste sur le disque et, si le saut en cas d’échec est activé, son exécution peut même être tentéerepository_dispatchOn peut écrire
on: repository_dispatch: - security_scan - security_scan::*. En centralisant le pipeline de release, on peut forcer chaque dépôt à passer par un workflow réutilisable défini, et si on envoie des événements commesecurity_scan::$product_name::$version, il devient bien plus facile de voir, dans l’onglet Actions du dépôt central de release, quel workflow tourne pour quel produit et quelle versionJe viens justement de rejoindre une organisation qui essaie de faire quelque chose de similaire, et en pratique ça paraît presque inutile. Les templates cassent souvent, et ils supposent fréquemment une manière précise de builder le code sans documentation expliquant comment il faut s’y prendre
En général, je préfère mettre la logique dans un système de build comme Make et seulement l’appeler depuis GitHub Actions, ou bien écrire un petit programme en ligne de commande et l’appeler. Ce genre de chose est bien plus facile à déboguer en local que dans la CI. C’est une astuce intéressante, mais je ne vois pas très bien où elle serait utile
make buildetmake testAprès une acquisition, j’ai regardé les fichiers de workflow de l’entreprise acquéreuse : ils faisaient des centaines de lignes, avec beaucoup de répétitions. Appelez-moi vieux jeu si vous voulez, mais je veux quitter au plus vite le village YAML
Même pour une fonctionnalité qu’on n’a pas l’intention d’utiliser réellement, savoir ce qui est possible dans un système est utile pour la sécurité et le debugging
Surtout lorsqu’on « explore » des runners auto-hébergés
La génération suivante tremblera quand on lui demandera de mettre de la discipline dans des déploiements faits avec GitHub Actions
Si vous pouvez l’expliquer, je pense qu’on pourrait probablement la mettre en place dans notre organisation
bash, pour lui faire exécuter n’importe quel programmeIl m’est souvent arrivé de partir de quelques lignes de commande saisies à la main, presque recopiées telles quelles, puis de voir le script shell devenir un monstre de plus de cent lignes ; à chaque fois, je me suis dit que de vrais tableaux, des types, et le côté batteries included de la bibliothèque standard Python auraient été bien utiles. Cela dit, je n’implémenterais pas la build Action de l’entreprise en elisp
C’est exposé par la méthode
ScriptHandlerHelpers.GetScriptArgumentsFormat. DansScriptHandler.cs, il y a le code de préparation de l’environnement de processus et des arguments, et le code qui lance réellement le processus se trouve ici : https://github.com/actions/runner/blob/main/src/Runner.Worke...Globalement, j’ai été agréablement surpris par la simplicité de ce code. C’est très procédural et ça gère énormément de cas particuliers, mais cela semble facile à comprendre et à déboguer
L’assembleur devrait aussi être possible
Cela dit, goeval ne prend pas encore directement en charge l’entrée depuis un fichier, seulement l’entrée standard, donc il faut une petite astuce shell. Pour l’instant, c’est quelque chose comme
go run github.com/dolmen-go/goeval@v1 - <<'EOF' ... EOF, ce qui demande un peu de boilerplate. Pour info, je suis l’auteur de goeval[1] https://github.com/dolmen-go/goeval
go run github.com/dolmen-go/goeval@v1 - < file.gone fonctionne pas ?Dans ce contexte, pour promouvoir un projet perso, montrer un exemple plus pertinent que « afficher hello » m’aurait évité un clic
run: pipeline.sh?On peut lancer des jobs en parallèle sur plusieurs systèmes d’exploitation et architectures CPU, et on obtient aussi des informations de contexte sur l’événement qui a déclenché le job et sur l’état du dépôt. C’est pratique pour les jobs par PR ou l’automatisation des releases, et cela peut aussi s’intégrer à la web UI GitHub, par exemple pour qu’un linter signale les problèmes ligne par ligne dans une PR ou qu’un échec de test soit rendu sur une page web. On peut aussi utiliser un petit cache pour éviter de retélécharger ou de rebuilder des fichiers inchangés. Dans l’idéal, il vaut mieux mettre le plus possible dans des outils classiques exécutés en local, et faire en sorte que la configuration GitHub CI ne serve que de code glue pour les déclencheurs, le cache et l’intégration GitHub
On peut facilement réutiliser dans le pipeline des GitHub Actions écrites par d’autres, modulariser les workflows, et contrôler les dépendances ainsi que l’exécution en parallèle. Il y a probablement d’autres avantages, mais l’essentiel est qu’on n’a pas besoin d’implémenter soi-même toutes ces choses