4 puntos por GN⁺ 2025-04-09 | 2 comentarios | Compartir por WhatsApp
  • El shell de GitHub Actions es una configuración que determina cómo se ejecuta un bloque run:, pero en la práctica no se limita a una lista fija de shells: también puede apuntar a ejecutables en $PATH
  • En los workflows es opcional; en las definiciones de acciones es obligatorio. El valor predeterminado varía según el runner, como bash en Linux/macOS y pwsh en Windows
  • Si se especifica explícitamente, como shell: bash, GitHub agrega flags adicionales como bash --noprofile --norc -eo pipefail, pero el objetivo ejecutado puede ser cualquier programa
  • Si el ejecutable no recibe directamente una entrada de un solo archivo, hay que incluir el argumento {0} en el valor de shell; GitHub lo reemplaza por la ruta del archivo temporal que contiene el bloque run
  • Incluso nombres conocidos como bash se resuelven desde $PATH, por lo que cambios en $GITHUB_PATH o ejecutables falsos pueden afectar cómo se ejecutan los pasos posteriores

Comportamiento básico de la palabra clave shell

  • La palabra clave shell de GitHub Actions especifica con qué shell se ejecutará un bloque run: determinado
  • En los workflows es opcional, pero en las definiciones de acciones es obligatoria
  • El shell predeterminado se determina según el entorno del runner
    • En Linux y macOS suele ser bash
    • En Windows suele ser pwsh

Especificar un shell explícitamente y flags adicionales

  • En la documentación de defaults.run.shell, GitHub indica que, al especificar un shell, también se aplican los flags elegidos por GitHub
  • Por ejemplo, si se especifica shell: bash, la forma de ejecución queda así
    • bash --noprofile --norc -eo pipefail
  • Viendo solo este comportamiento, es fácil pensar que GitHub mantiene una lista limitada de valores válidos de shell y que solo agrega flags especiales a esos valores

Ejecutables arbitrarios en $PATH también pueden ser shells

  • En realidad, en shell se puede especificar cualquier ejecutable que esté en $PATH
  • GitHub ejecuta el bloque run con el ejecutable indicado
  • Si ese comando no recibe directamente una entrada de un solo archivo, se debe pasar {0} en el valor de shell
    • GitHub reemplaza {0} por la ruta de un archivo temporal
    • Ese archivo temporal contiene el contenido del bloque run con las plantillas ya expandidas

Ejemplo usando C como runner de pasos

  • También pueden usarse herramientas que funcionan como compiladores/intérpretes de C para ejecutar pasos de GitHub Actions
  • Un ejemplo que especifica tcc -run {0} como shell funciona correctamente
- run: sudo apt install -y tcc
- shell: tcc -run {0}
  run: |
    #include <stdio.h>
    int main() {
      printf("Hello, world!\n");
      return 0;
    }

Ejemplo de cambiar la resolución del shell con $GITHUB_PATH

  • Si se modifica dinámicamente $PATH mediante $GITHUB_PATH, un shell: bash posterior puede apuntar a un ejecutable distinto al esperado
  • El ejemplo crea en el directorio actual un ejecutable llamado bash y agrega el directorio actual a $GITHUB_PATH
  • Luego, al usar shell: bash, GitHub puede ejecutar el bash falso encontrado en $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

Punto inesperado desde el punto de vista de seguridad

  • Es difícil afirmar si este comportamiento es importante para la seguridad
  • En GitHub Actions ya hay muchos caminos donde escribir archivos puede derivar en ejecución, y GITHUB_ENV también entra en esa categoría
  • Aun así, puede resultar inesperado que GitHub haga una búsqueda en $PATH incluso para valores de shell conocidos como bash
  • En particular, si inyecta flags de línea de comandos según valores de shell conocidos, podría esperarse que bash quedara fijado a una ruta específica como /bin/bash
  • De forma más general, también podría pensarse que GitHub solo permite herramientas registradas previamente en la caché de herramientas, pero el comportamiento observado usa ejecutables de $PATH

2 comentarios

 
tujuc 2025-04-09

Con solo ver el repositorio github/runner-image, ya se nota que vienen instalados bastantes paquetes que se pueden usar tal cual....

Si armas la imagen, fácilmente se va a 1 GB....

 
GN⁺ 2025-04-09
Opiniones de Hacker News
  • Hace tiempo usé la bandera -x para que bash imprimiera todos los comandos que ejecuta en un workflow de Actions, y ayuda bastante para depurar
    https://github.com/jstrieb/just.sh/blob/2da1e2a3bfb51d583be0...
    • Como referencia, si activas pipefail, el paso falla cuando una de las partes del pipe falla, pero puede que no haya salida de error y no sepas por qué falló
      pipefail tampoco evita estados de error más complejos. Por ejemplo, en el paso curl ... | sudo tar ... de la configuración, se evidencian casos como una falla al extraer con tar, una falla de sudo o un error del shell, pero si curl falla a mitad de camino por un error de red, tar puede dejar descomprimido solo la mitad del binario de just y el shell puede terminar de inmediato. En ese caso no hay mensaje de error, el ejecutable dañado queda en el disco y, si está activado omitir fallas, incluso se puede intentar ejecutarlo
  • Un truco genial de GitHub Actions privado que vi en el trabajo es que se pueden usar comodines para hacer match con nombres de eventos repository_dispatch
    Se puede usar algo como on: repository_dispatch: - security_scan - security_scan::*. Si centralizas el pipeline de releases, puedes obligar a que cada repositorio pase por un workflow reutilizable definido, y si envías eventos como security_scan::$product_name::$version, es mucho más fácil ver en la pestaña Actions del repositorio central de releases qué workflows se están ejecutando para qué producto y versión
    • Me da curiosidad si este enfoque centralizado realmente funcionó bien. ¿La organización exige que todo se compile exactamente de la misma manera?
      Acabo de entrar a una organización que intenta hacer algo parecido, pero en la práctica parece casi inútil. Las plantillas se rompen seguido y muchas veces están escritas asumiendo una forma específica de compilar el código, sin documentación sobre cómo debería compilarse
  • Creo que mientras menos cosas hagas dentro de GitHub Actions, mejor
    Normalmente prefiero poner la lógica en un sistema de build como Make y solo invocarlo desde GitHub Actions, o escribir un pequeño programa de línea de comandos e invocarlo. Eso es mucho más fácil de depurar localmente que en CI. Es un truco interesante, pero no sé bien dónde sería útil
    • Nuestro workflow es básicamente make build y make test
      Después de que nos adquirieron, vi el archivo de workflow de la empresa compradora y tenía cientos de líneas, con muchas secciones repetidas. Llámenme anticuado si quieren, pero quiero salir del pueblo YAML lo antes posible
    • Parece que el autor no está recomendando hacer esto, sino informando que es posible
      Saber qué es posible en un sistema es útil para seguridad y depuración, incluso si no planeas usar esa función en la práctica
    • Mi interpretación es que esto no es útil; definitivamente no es útil. Pero sí hay un riesgo de seguridad potencial
      En especial al “explorar” runners autoalojados
  • Nuestra generación se aterraba cuando le pedían convertir a código hojas de cálculo que cambiaban todo el tiempo
    La siguiente generación se aterrorizará cuando le pidan imponer disciplina a despliegues hechos con GitHub Actions
    • Ahora estoy trabajando en migrar una gran empresa a GH Actions; para ser precisos, algo más cercano a “pipelines basados en YAML atados a git”. ¿Cómo debería verse la disciplina ahí?
      Si se puede explicar, probablemente podamos implementarlo en nuestra organización
    • Me pregunto por qué esto carecería de disciplina
  • También se puede engañar al shell predeterminado, bash, para que ejecute un programa arbitrario
  • Si las demás personas que leen la Action saben qué está pasando, parece bastante útil
    Muchas veces he visto scripts de shell que empezaron como unas cuantas líneas de comandos tecleadas a mano, casi copiadas tal cual, y terminaron convertidos en monstruos de más de cien líneas; en esos casos siempre he deseado tener arreglos y tipos reales, y las funciones baterías incluidas de la biblioteca estándar de Python. Dicho eso, no implementaría la Action de build de la empresa en elisp
  • El código de GitHub Actions Runner es bastante fácil de leer. Aquí es donde se definen los argumentos predeterminados para shells/binarios populares: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    Se expone mediante el método ScriptHandlerHelpers.GetScriptArgumentsFormat. En ScriptHandler.cs está el código que prepara el entorno del proceso y los argumentos, y el código que inicia realmente el proceso está aquí: https://github.com/actions/runner/blob/main/src/Runner.Worke...
    En general, me sorprendió para bien la simplicidad de este código. Es muy procedimental y maneja muchísimos casos excepcionales, pero parece fácil de entender y depurar
  • Ahora por fin podremos usar C en CI/CD de producción y llamarlo “trabajo de sistemas de bajo nivel”
    Supongo que también se podrá con ensamblador
  • Esto me da la esperanza de que, usando goeval [1], podría ser fácil ejecutar código Go directamente como tareas de CI desde un archivo YAML de workflow de GitHub
    Aunque goeval todavía no admite entrada de archivos directamente y solo admite entrada estándar, así que hace falta un truco de shell. Ahora es algo como go run github.com/dolmen-go/goeval@v1 - <<'EOF' ... EOF, lo cual requiere un poco de boilerplate. Por cierto, soy el autor de goeval
    [1] https://github.com/dolmen-go/goeval
    • No sé por qué haría falta un “truco” de shell. ¿No bastaría con go run github.com/dolmen-go/goeval@v1 - < file.go?
    • Fui al repositorio para ver si era un programa que convierte espacios en tabs automáticamente, pero parece estar más orientado a expresiones de una sola línea
      Si vas a promocionar un proyecto de juguete en este contexto, mostrar un ejemplo más relevante que “imprimir hello” me habría ahorrado el clic
  • ¿Qué tiene GitHub CI YAML de mejor que un script bash como run: pipeline.sh?
    • Tiene un token de la API de GitHub administrado automáticamente, útil para automatizar releases y publicar artefactos y contenedores
      Puedes ejecutar trabajos al mismo tiempo en varios sistemas operativos y arquitecturas de CPU, y también obtener información de contexto sobre el evento que disparó el trabajo y el estado del repositorio. Es práctico para trabajos por PR o automatización de releases, y también puede integrarse con la UI web de GitHub, por ejemplo para que un linter muestre problemas en cada línea del PR o para renderizar fallas de tests en una página web. También se puede usar una pequeña caché para no volver a descargar ni recompilar archivos que no cambiaron. Idealmente, conviene poner lo más posible en herramientas comunes que corran localmente, y dejar que la configuración de GitHub CI se encargue solo del código pegamento necesario para triggers, caché e integración con GitHub
    • En la UI de GitHub puedes ver un resumen de cada paso y expandir o contraer la salida de cada uno
      Puedes aprovechar fácilmente GitHub Actions hechas por otras personas en tu pipeline, modularizar workflows y controlar dependencias y ejecución en paralelo. Seguramente hay más, pero la ventaja principal es no tener que implementar todo eso tú mismo
    • Desde el punto de vista de GitHub, la ventaja es que reduce la probabilidad de que los usuarios se vayan a otro forge de Git