Introducción a Github Actions
Nota: Es recomendable tener una noción básica de la sintáxis YAML. Si no la usaste, podés mirar este post hasta que yo escriba alguno sobre yaml (?)
En mi trabajo, un compañero de equipo tenía asignado semanalmente subir un archivo de textos a una plataforma de traducciones, por lo que todos los viernes se encargaba de loguearse y subir dicho archivo. Ahora que se fue, y me delegaron la tarea a mi, aproveché la oportunidad para automatizarla. Como el archivo a subir formaba parte de nuestro repositorio de trabajo, era una buena oportunidad para probar las GitHub Actions.
Conociendo las GH Actions
Con GitHub actions podemos configurar tareas, en forma declarativa, a ejecutarse como respuesta a determinados eventos, por ejemplo:
- Pusheando a un branch
- Abriendo un PR
- Cada cierto tiempo (símil cron job)
Dado lo cercanas a nuestros repositorios, es muy común que se utilicen para tareas comunes en PRs como ejecutar los tests, correr validaciones (por ej. Eslint), compilaciones, chequeos de coverage, o incluso deployar en algun ambiente tras mergear.
Cada conjunto de tareas (que puede implicar varios pasos) que se agrupan para reaccionar ante un conjunto de eventos se denominan workflows. Los workflows se ejecutan en un runner, que es un entorno que provee Github (GitHub-hosted). Normalmente, estas máquinas se inicializarán "vacías" (nada mas allá del SO), aunque esto es configurable.
Veamos un ejemplo sencillo. Los worfklows se configuran en yml
files bajo la carpeta .github/workflows
. El siguiente file se llama ci.yml
y se encuentra en dicha carpeta en un proyecto pequeño mio, y lo utilizo para correr los tests cuando abro un PR.
name: CI checks
on:
pull_request:
types: [opened, synchronize]
jobs:
build_check:
name: Checks typings and building
runs-on: ubuntu-latest
env:
CI: true
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: '12.x'
registry-url: 'https://npm.pkg.github.com'
- name: Install deps
run: npm ci
- name: run tests
run: npm test
Analicemos en mas detalle lo que pasa en cada parte:
name: CI checks
El campo name
se utiliza para identificar un workflow, y aparecerá en la seccion de Actions
de tu repo.
on:
pull_request:
types: [opened, synchronize]
on es la key que nos permite determinar que eventos causarán la ejecucion de este workflow. La lista es bastante larga, pero en este ejemplo en particular determinamos que queremos ejecutar el workflow solo en los pull_request
y en types
especificamos opened
, es decir, cuando abrimos un PR, y synchronize
, o sea, cuando actualicemos un PR (por ej., lo rebaseemos contra el target branch o cuando agreguemos nuevos commits). Otras opciones (dentro de pull_request
) son por ejemplo assigned
(cuando se le asigna a un usuario), closed
(cuando se cierra), labeled
(cuando se le agrega un label) entre otras.
También existen otras opciones fuera de pull_request
, por ejemplo:
on:
push:
branches:
- main
- develop
Esta opción nos permite correr acciones al pushear contra el branch main
o develop
. Podés listar cualquiera de los branches que te interesan.
También se pueden configurar Cron jobs (ejecución periódica) usando la sintaxis de cron POSIX
Por ejemplo:
on:
schedule:
# * es un caracter especial en YAML, asique se usa un string
- cron: '*/15 * * * *'
ejecuta este workflow cada 15 minutos. Esta fue la opción que configuramos en el caso mencionado el principio, para subir un archivo a una plataforma una vez por semana.
(recomiendo usar alguna página que traduzca el periodo que queremos a estas expresiones, como crontab generator)
Jobs
Dentro de un workflow nos encontramos con el concepto de jobs
. Un job es una unidad lógica que agrupa una serie de pasos a ejecutar. Los jobs
se pueden ejecutar en paralelo, asi que si definieramos N jobs, se correrían los N en paralelo.
Los jobs como unidad lógica pueden ejecutarse correctamente o fallar. Por ese motivo tienen status de ejecución que indican si se ejecutó exitosamente o si falló algo en el medio. Esto es útil para condicionar acciones mediante los resultados de los jobs, por ejemplo, evitar que un PR se mergee si los tests no pasan.
Veamos como crear un job
:
jobs:
build_check:
name: Checks typings and building
runs-on: ubuntu-latest
env:
CI: true
steps: # ver en la sección de steps
Cada key que esté directamente debajo de jobs
es un job_id
- una etiqueta que identifica de manera única a un job. Tiene que ser única por workflow y sirve si, por ejemplo, tuvieras que usar la API de GitHub para identificar un job. Los identificadores solo pueden usar caracteres alfanuméricos, -
o _
.
Cada job tiene distintas propiedades:
name
es para darle un nombre legible y descriptivo al job. Este aparecerá en el listado de jobs al ver una acción
y también ayudará a identificar los jobs asociados a un PR
una vez finalizada la ejecución de los workflow, se verán asi en los PRs
runs-on
nos permite determinar en qué tipo de máquina queremos correr nuestro job. Por defecto, GitHub provee runners (que ellos mismos hostean), aunque uno pudiera usar los suyos. Las configuraciones actuales (julio 2020) son
Ambiente | YAML value |
---|---|
Windows Server 2019 | windows-latest o windows-2019 |
Ubuntu 20.04 | ubuntu-20.04 |
Ubuntu 18.04 | ubuntu-latest o ubuntu-18.04 |
Ubuntu 16.04 | ubuntu16.04 |
macOS Catalina 10.15 | macos-latest o macos-10.15 |
por las dudas recomiendo la documentación ya que esta tabla en un futuro seguramente esté desactualizada.
env
nos permite agregar variables de entorno que estarán disponibles en todas los pasos de estejob
. Dependerá más que nada de qué hagan en dichos jobs.
Aunque no eran necesarias en mi caso, existen otras opciones útiles, como por ejemplo timeout-minutes
para indicar el máximo tiempo de ejecución en minutos.
steps
Habíamos mencionado que un job
agrupa lógicamente una serie de pasos a ejecutarse en conjunto. Para declarar esas tareas, usamos steps
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: '12.x'
registry-url: 'https://npm.pkg.github.com'
- name: Install deps
run: npm ci
- name: run tests
run: npm test
Con steps
declaramos una lista que se ejecutará de manera secuencial. Cada ítem de la lista tendrá un name
con el cual se lo identificará dentro de un job, y una opción run
en la cual se determinará qué comando correr: por ejemplo, si es un proyecto de javascript
y usamos npm
, podemos usar npm ci
para instalar las dependencias en un step
, y luego, en el otro, correr los tests con npm test
(que es lo que hace el ejemplo más arriba).
Lo interesante acá es que al abrirnos la puerta de ejecución a código, nos da una enorme flexibilidad para realizar un montón de tareas, como compilar nuestro proyecto, hacer deploys a ambientes de prueba, o incluso salidas a producción (por ejemplo al mergear contra nuestro branch productivo).
Debido a eso, es natural que muchas tareas sean repetitivas (por ej., instalar algún programa como node
o hacer un checkout de nuestro repositorio en dicha vm), por lo que las acciones se pueden publicar en un repo y reutilizar en varios repositorios (incluso se pueden publicar en el marketplace de GitHub).
GitHub provee algunas acciones comunes, como checkout que permite hacer checkout de nuestro repositorio y que esté disponible en el runner
que corre nuestro job
.
Otra acción interesante es setup-node, que instala nodejs
en nuestro repo, y nos permite correr los comandos de npm
que tengamos configurado.
Para usar una action que esté publicada en algún repositorio o en el marketplace, existe el comando uses
, cuyo valor es <repo-owner>/<repo-name>@<version, tag o commit sha>
. repo-owner
se refiere a qué usuario/organización tiene el repositorio, siendo repo-name
el nombre del mismo. Para el último valor, veamos unos ejemplos:
steps:
# 74bc508 es el hash de un commit especifico
- uses: actions/setup-node@74bc508
# refiere al tag/release v1
- uses: actions/setup-node@v1
# refiere al tag/release v1.2
- uses: actions/[email protected]
# refiere al branch master
- uses: actions/setup-node@master
De esta manera, podemos especificar a qué versión/branch/commit queremos apuntar. Normalmente la documentación de la action debiera decir qué release es estable; recomendamos usar ese.
Si la acción requiere algún tipo de parámetro (por ejemplo, alguna key) se puede usar la opción with
, que permite pasar valores de configuración a la action. Estos valores son parámetros que la acción por dentro obtendrá para realizar alguna acción, pero no confundir con las variables de entorno.
Por ejemplo, en el caso de este step
steps:
- name: Use Node.js 12.x
uses: actions/setup-node@v1
with:
node-version: '12.x'
registry-url: 'https://npm.pkg.github.com'
se está usando la acción setup-node
en su v1
, que si vemos su documentación nos muestra que ofrece los parámetros node-version
y registry-url
para configurarla.
Volviendo a los comandos run
, normalmente recomiendo usar comandos cortos; si un comando se vuelve muy largo es más fácil moverlo a algún archivo js
, o incluso bash (cualquiera que sea ejecutable) y correrlo desde ahí.
Si por algún motivo requerimos 2 o 3 comandos juntos, se pueden ejecutar comandos multilínea (con la ayuda del |
) de la siguiente manera:
steps:
- name: Mi comando multiple
run: |
npm run build
npm run publish
Los steps tienen varias opciones interesantes, como working-directory
que nos permite especificar en qué directorio se va a correr el comando (útil para mono-repos, por ejemplo)
Resolviendo nuestro caso de uso
Con esta introducción podemos pensar a alto nivel cómo solucionar el problema de periódicamente subir el archivo a la plataforma que me fue heredado. Expandiendo un poquito más en el scope, era para subir archivos de texto a la plataforma OneSky, donde luego se pueden solicitar traducciones y descargarlas. Con esto en mente, a alto nivel podemos pensar algo así:
-
Creamos un workflow que use el evento
schedule
, con una expresión POSIX cron para ejecutarse periódicamente. -
Creamos un job (llamémosle
upload
) que corra enubuntu-latest
y tenga una serie de steps. -
El primer step va a ser checkoutear nuestro repo, donde se encuentra el file
uses: actions/checkout@v2
y luego instalarnode
con el action hecho mas arriba. -
Por último, bien podríamos correr un comando con
npm run <mi-comando>
y resolver el upload del file en js.
Pensando que podía ser un problema general terminé creando una GitHub action para poder consumirla desde cualquier workflow, a la cual le pasamos las API Keys de la plataforma (y un par de configuraciones más), y se encarga de subir el archivo por nosotros. Esto resulta útil para cualquier proyecto que use OneSky y esté en GitHub. En este caso, la forma de usarla es similar a los anteriores
- name: Subir el archivo a OneSky
uses: gndelia/[email protected]
with:
projectId: 123456
privateKey: ${{ secrets.PRIVATE_KEY }}
filepath: path/to/folder/where/the/file/is
filename: my-file.json
locale: en-US
# van mas configuraciones - las saqué por claridad
Esa notación que ven ${{ secrets.PUBLIC_KEY }}
permite acceder a los GitHub secrets - básicamente para guardar cosas privadas que normalmente no queremos hardcodeadas (por ejemplo, claves privadas) y hacerlas accesibles a nuestros workflows
Naturalmente, si se puede automatizar la subida de los files, también se puede hacer la descarga una vez que los archivos que subimos fueron traducidos, y aprovechando la API de OneSky se podría crear una acción similar.
Resumen
Las GitHub actions son una manera muy versátil de ejecutar código ante determinados eventos que nosotros podemos definir. Ya sean tareas repetitivas, o para validaciones de nuestro repo, son muy sencillas de configurar y al permitirnos ejecutar código libremente son una herramienta muy potente.