4 points par GN⁺ 2025-04-09 | 2 commentaires | Partager sur WhatsApp
  • 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

 
tujuc 2025-04-09

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

 
GN⁺ 2025-04-09
Commentaires sur Hacker News
  • Par le passé, j’ai utilisé le flag -x pour faire afficher toutes les commandes exécutées par bash dans un workflow Actions, et c’est assez utile pour le debugging
    https://github.com/jstrieb/just.sh/blob/2da1e2a3bfb51d583be0...
    • À noter que si on active pipefail, l’étape échoue lorsqu’un des éléments du pipeline échoue, mais sans sortie d’erreur, donc on peut ne pas savoir pourquoi ça a échoué
      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 de tar, un échec de sudo ou une erreur du shell seront visibles, mais si curl échoue en cours de route à cause d’une erreur réseau, tar peut n’extraire que la moitié du binaire just et 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ée
  • Une astuce GitHub Actions privée assez élégante que j’ai vue au travail, c’est qu’on peut faire correspondre des wildcards dans le nom d’événement repository_dispatch
    On 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 comme security_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 version
    • Je me demande si cette approche centralisée a réellement bien fonctionné. Est-ce que l’organisation exige que tout soit buildé exactement de la même manière ?
      Je 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
  • À mon avis, moins on en fait dans GitHub Actions, mieux c’est
    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
    • Nos workflows se résument en pratique à make build et make test
      Aprè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
    • Cet article ne semble pas dire que son auteur recommande de faire ça, mais plutôt qu’il informe du fait que c’est possible
      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
    • Mon interprétation, c’est que ce n’est pas utile, et certainement pas vraiment utile. En revanche, il y a un risque de sécurité potentiel
      Surtout lorsqu’on « explore » des runners auto-hébergés
  • Notre génération tremblait quand on nous demandait de transformer en code des tableurs en perpétuel changement
    La génération suivante tremblera quand on lui demandera de mettre de la discipline dans des déploiements faits avec GitHub Actions
    • Je travaille justement en ce moment sur la migration d’une grande entreprise vers GH Actions, même si plus précisément il s’agit de « pipelines YAML liés à git ». À quoi devrait ressembler cette discipline ici ?
      Si vous pouvez l’expliquer, je pense qu’on pourrait probablement la mettre en place dans notre organisation
    • Je me demande pourquoi cela serait dépourvu de discipline
  • On peut aussi tromper le shell par défaut, bash, pour lui faire exécuter n’importe quel programme
  • Cela semble assez utile si les autres personnes qui lisent l’Action savent ce qui se passe
    Il 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
  • Le code du GitHub Actions Runner est assez facile à lire. L’endroit où sont définis les arguments par défaut des shells/binaires populaires est ici : https://github.com/actions/runner/blob/main/src/Runner.Worke...
    C’est exposé par la méthode ScriptHandlerHelpers.GetScriptArgumentsFormat. Dans ScriptHandler.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
  • On va enfin pouvoir écrire la lettre C de CI/CD en C pour de la prod, et appeler ça du « travail système bas niveau »
    L’assembleur devrait aussi être possible
  • En voyant ça, je me dis qu’il deviendra peut-être plus simple d’utiliser goeval [1] pour exécuter directement du code Go comme job CI dans un fichier YAML de workflow GitHub
    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
    • Je ne vois pas pourquoi il faudrait une « astuce » shell. go run github.com/dolmen-go/goeval@v1 - < file.go ne fonctionne pas ?
    • Je suis allé sur le dépôt pour voir si c’était un programme qui convertissait automatiquement les espaces en tabulations, mais à voir le contenu, ça semble plutôt pensé pour des expressions sur une ligne
      Dans ce contexte, pour promouvoir un projet perso, montrer un exemple plus pertinent que « afficher hello » m’aurait évité un clic
  • Qu’est-ce que le YAML de GitHub CI apporte de mieux qu’un script bash du style run: pipeline.sh ?
    • Il y a un token d’API GitHub géré automatiquement, utile pour l’automatisation des releases, la publication d’artefacts et de conteneurs
      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
    • Dans l’interface GitHub, on peut voir une vue d’ensemble de chaque étape, et déplier ou replier la sortie étape par étape
      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
    • Du point de vue de GitHub, l’avantage est que les utilisateurs ont moins de chances de partir vers une autre forge Git