Simplifica tu GitHub Actions: Adiós al 'cd' repetido

Cansado de poner 'cd subdirectorio' en cada paso de tu CI? Te muestro cómo simplificar tus flujos de trabajo en GitHub Actions y evitar la repetición innecesaria.

Uf, todavía me acuerdo la rabia que me dio la primera vez que me topé con esto. Estaba montando el CI para un proyecto PHP, de esos donde composer.json no está en la raíz, sino en una carpeta app/. Todo mi workflow de GitHub Actions se veía salpicado de cd app/ en cada puñetero paso. Que cd app/ && composer install, que cd app/ && php artisan migrate, que cd app/ && phpunit. Una lata.

Imagínate, cada vez que tenías que ajustar algo, o añadir un paso nuevo, tenías que asegurarte de que ese cd app/ estuviera ahí. No era solo repetitivo; era una fuente segura de errores. ¿Se te olvida uno? Fallaba el build. ¿Lo pones mal? Fallaba el build. Para mí, que valoro las soluciones simples y que funcionan, esto era sobreingeniería pura por omisión, o al menos, una muy mala práctica que se repetía porque 'así es como se hace'.

Por suerte, hay formas de evitar este baile de directorios. Al principio, la gente tiraba por una solución que, si bien es mejor que el cd explícito en cada comando, sigue siendo un poco verbosa. Me refiero a usar working-directory en cada paso individual.

Se ve algo así:

name: CI

on: [push]

jobs:
  phpunit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v1
      - name: Setup Symfony
        working-directory: ./app
        run: cp .env.dev .env
      - name: Install Composer Dependencies
        working-directory: ./app
        run: composer install --prefer-dist
      - name: Run Tests
        working-directory: ./app
        run: php bin/phpunit

¿Es mejor que poner cd app/ dentro de cada run? Sin duda. Hace que el intent sea más claro y separas la configuración del comando. Pero, ojo, que sigue siendo repetitivo. Si tuvieras diez pasos que dependen de app/, tendrías que escribir working-directory: ./app diez veces. No es ideal, ¿verdad?

En mi caso, cuando me di cuenta de que esto se podía simplificar aún más, me dio una alegría tremenda. La solución que realmente me convenció y que uso ahora siempre que puedo es establecer un working-directory por defecto para todo un job. Esto limpia muchísimo el código y lo hace mucho más legible y mantenible. Es una de esas cosas que, de hecho, desearía haber aprendido antes.

Ahora, tu workflow puede verse así:

name: CI

on: [push]

jobs:
  phpunit:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./app
    steps:
      - uses: actions/checkout@v1
      - name: Setup Symfony
        run: cp .env.dev .env
      - name: Install Composer Dependencies
        run: composer install --prefer-dist
      - name: Run Tests
        run: php bin/phpunit

¿Ves la diferencia? El defaults.run.working-directory le dice a GitHub Actions: "todos los comandos run de este job, ejecútalos desde la carpeta ./app, a menos que se especifique lo contrario". Esto es un game-changer para la claridad del archivo de configuración. Una línea y te olvidas de la repetición.

Pero como casi todo en desarrollo, siempre hay un "pero" o un tradeoff. Esta magia del defaults.run.working-directory aplica solo a los pasos de tipo run, es decir, a los comandos de shell que ejecutas directamente. Si estás usando una acción de GitHub (un paso con uses: ...), esa acción específica no heredará este directorio de trabajo por defecto. En esos casos, si la acción necesita operar en un subdirectorio y no te lo permite configurar con sus propios parámetros, podrías necesitar un working-directory explícito para ese paso con uses, o encapsular el comando de la acción dentro de un run script que sí herede el default.

Para mí, la regla es simple: si la mayoría de mis pasos son run y apuntan al mismo subdirectorio, el defaults.run.working-directory es la solución. Me ahorra dolores de cabeza y mantiene el CI limpio. Si tengo una mezcla muy compleja de uses y run, donde los uses necesitan operar en directorios diferentes o tienen sus propias formas de manejar rutas, entonces tendré que evaluar si tiene sentido usar el default o si me conviene más ser explícito paso a paso. La mayoría de las veces, el default te va a salvar la vida.

Jorge RequenaDeveloper full-stack · Chile