Integración continua con Github II: Github Actions

La mayoría de plataformas como Github o Gitlab (también llamadas “forjas de código”, aunque a mí no me gusta mucho como suena), no han parado de implementar nuevas funcionalidades, tratando de convencer al público de ser la mejor herramienta para la creación de nuevas aplicaciones.

Hace años, casi todos los proyectos Open Source alojados en Github, utilizaban Travis CI para ejecutar sus pipelines de CICD. Sin embargo, a finales de 2020, dicha plataforma cambió sus condiciones y muchos de sus usuarios tuvieron que buscar otra alternativa.

La simbiosis entre Travis y Github no era oficial y tras un tiempo, esta última presentó su propia solución: Github Actions. En este post, busco actualizar una antigua entrada que escribí sobre Travis CI y replicar las funcionalidades de dicho pipeline pero sobre Actions. Pero antes, un poco de historia.

Github y Microsoft

Uno de los bombazos del 2018 fue la compra de Github por Microsoft por aproximadamente una cantidad de 7000 millones de dólares. Dicha compra competía con alguno de los productos que ellos ya comercializaban (Team Foundations Server o Azure DevOps) y parte de la comunidad pensó que alguno de ellos terminaría siendo abandonado.

Sin embargo, estos temores eran infundados y ambas herramientas han estado en constante evolución, pudiendo utilizar cualquiera de ellas para crear pipelines de gran complejidad, según nuestras necesidades.

Github Actions

Github Actions es una plataforma que permite la ejecución de tareas cuando ocurren ciertos eventos sobre nuestro código. Algunos eventos pueden ser “realizar un commit sobre determinadas ramas” o “fusionar distintas ramas de código”, etc.

Los pipelines definidas en Github Actions reciben el nombre de workflows y podemos crear todos los que queramos según nuestras necesidades.

Aunque es una plataforma de pago, su tier gratuito es muy generoso y si queremos probarlo, tenemos hasta 2000 minutos de ejecuciones mensuales sin coste alguno.

Para usar Actions, lo primero que necesitamos es una cuenta y un repositorio en Github. Aunque existen formas de utilizarlas en otras plataformas, yo mantengo un mirroring entre Github y Gitlab para mi blog y puedo usar los servicios de Github de forma indistinta.

blog-integrations-2022

Este diagrama describe el estado actual de las integraciones de mi blog: cuando realizo un cambio sobre mi repositorio en Gitlab, éste se replica automáticamente hacia Github y se ejecutan una serie de operaciones en Gitlab CI que testean el código, crean un contenedor y despliegan el contenido del repositorio en su destino.

Gracias al mirroring con Github, anteriormente había un pipeline que se ejecutaba en Travis CI y creaba una imagen pública para después subirla a DockerHub. Sin embargo, esta parte no funciona desde que Travis CI cambió sus condiciones de uso.

Tras esta introducción, ya podemos ponernos manos a la obra.

Primeros pasos

Github Actions no pretende reinventar la rueda, y al igual que otras plataformas de CICD, se basa en ficheros YAML, con una estructura donde definimos qué acciones queremos ejecutar y cuando hacerlo. En este caso, tan sólo necesitamos crear una carpeta de nombre .github/workloads en la raíz de nuestro repositorio y dentro de ella, uno o más ficheros con extensión .yaml.

Cada fichero .yaml creado genera un workflow distinto y su estructura puede complicarse bastante, así que antes de crear el nuestro, voy a explicarla un poco su sintaxis, basándome en el ejemplo que Github proporciona en el siguiente link:

name: GitHub Actions Demo
run-name: ${{ github.actor }} is testing out GitHub Actions 🚀
on: [push]
jobs:
  Explore-GitHub-Actions:
    runs-on: ubuntu-latest
    steps:
      - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event."
      - run: echo "🐧 This job is now running on a ${{ runner.os }} server hosted by GitHub!"
      - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
      - name: Check out repository code
        uses: actions/checkout@v4
      - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner."
      - run: echo "🖥️ The workflow is now ready to test your code on the runner."
      - name: List files in the repository
        run: |
          ls ${{ github.workspace }}          
      - run: echo "🍏 This job's status is ${{ job.status }}."

En general podemos decir que cada workflow se compone de tres objetos:

  • name es el nombre que nuestro workflow va a tener y a mostrar dentro de la interfaz de Github Actions.
  • on se corresponde con el cuando se va a ejecutar nuestro workflow. Aquí definimos los eventos que van a disparar su ejecución.
  • jobs son la lista de pasos que nuestro workflow va a ejecutar cada vez.

En este ejemplo, estamos creando un workflow de nombre Github Actions Demo, que se ejecuta cada vez que hagamos un push al repositorio y que ejecuta un job llamado Explore-Github_Actions.

A su vez, cada job tiene una serie de palabras clave con su propia sintaxis y estructura:

  • runs-on define la imagen donde queremos que nuestro workflow se ejecute. Puede ser un ejecutor público o privado en función de nuestras necesidades. En este caso está utilizando el ejecutor público llamado ubuntu-latest.
  • steps define uno a uno los procesos que nuestro workflow va a ejecutar.

Este workflow realiza lo siguiente:

  • Primero imprime una serie de líneas por pantalla utilizando como valor algunas de las variables del contexto del repositorio como el tipo de evento, el nombre del repositorio o la rama del mismo, etc. Run nos permite ejecutar comandos sueltos como si de una terminal se tratase.
  • En segundo lugar reutiliza la Github Action de nombre actions/checkout en su versión 4 (en breve hablaremos de esto) para copiar la versión del código definida en on gracias a la directiva uses.
  • Por último, lista los ficheros que hay en esta revisión del código y si todo es correcto, nos indica que la ejecución del código ha funcionado.

Definiendo los diferentes steps

Una de las ventajas de utilizar Github Actions es la inmensa comunidad que hay detrás y lo fácil que es reutilizar acciones de terceros. Esto hace que portar la funcionalidad de mi pipeline en Travis CI sea muy sencillo, sin casi mantener código propio.

github-actions-diagram

Generar la imagen del contenedor sólo consta de dos pasos:

  • En el primero, utilizamos Hugo para generar el HTML final.
  • En el segundo, utilizamos dicho código HTML y el Dockerfile almacenado en el repositorio para crear la imagen, etiquetarla y subirla a DockerHub.

El primer paso es definir el workflow y cuando se va a ejecutar. En mi caso, quiero que sólo se ejecute al realizar un push o un merge a master o main y que su nombre sea “Tangelov GH Actions To DockerHub”.

name: Tangelov GH Actions To DockerHub
on: 
  push:
    branches:
      - 'master'
      - 'main'

Ahora podemos definir los pasos de nuestro workflow:

jobs:
  docker-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository code
        uses: actions/checkout@v4

      - name: Setup Hugo in Github Actions
        uses: peaceiris/actions-hugo@v2

      - name: Build Hugo static content
        run: hugo --minify

      - name: Save output for next steps
        uses: actions/cache@v2
        with:
          path: public
          key: public

Para crear el pipeline, vamos a utilizar distintas Actions mantenidas por la comunidad:

  • Primero, descargamos el código utilizando la acción actions/checkout en su versión 4.
  • Después, utilizamos la acción actions-hugo del usuario peaceiris en su versión 2 y ejecutamos el comando hugo --minify para generar el código HTML.
  • Por último, guardamos la carpeta public dentro de la caché para que otros pasos posteriores puedan utilizarla.

En este punto ya podríamos crear la imagen del contenedor y subirla a DockerHub, pero antes, necesitamos hacer que Github pueda acceder a DockerHub. Para ello, necesitamos generar un token de acceso y almacenarlo en Github Actions.

Crear el token es sencillo y tan sólo tenemos que ir a nuestra cuenta y hacer click aquí:

create-docker-token

Una vez tenemos nuestro token, ahora necesitamos crear dos variables en Github: una llamada DOCKER_USER y otra DOCKER_PASSWORD con el token creado en el paso anterior.

docker-creds-github

Ahora ya podemos añadir el código de los siguientes pasos:

      - name: Docker Login using Github Action
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USER }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Docker Build and Push using Github Action
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ secrets.DOCKER_USER }}/tangelov-me:latest

Cuando reutilizamos una Action, ésta puede ser configurada a través de variables. En este caso estamos utilizando docker/login-action y le estamos indicando que tiene que obtener el usuario y contraseña de DockerHub de los secretos llamados DOCKER_USER y DOCKER_PASSWORD que acabamos de crear. Este “contexto” se le pasa al job a través de la directiva with.

El resto de pasos nos permiten construir, etiquetar y guardar nuestra imagen en DockerHub.

Llegados a este punto, nuestro pipeline ya está completo y funcional, pero… ¿Por qué no añadirle alguna funcionalidad extra?

Tener trazabilidad en nuestro código es importante. Nos permite saber que paquetes o aplicaciones han sido construidas a partir de una versión del código concreta y en base a nuestros tests o resultados, dar marcha atrás si encontramos algún bug o error. Aunque en este caso no es muy útil puesto que el “código” es sólo HTML, espero que este ejemplo sirva como ejemplo para otros.

A nuestro pipeline, vamos a añadirle el uso del identificador SHA de nuestro commit (que es único) para etiquetar cada una de las imágenes Docker que hemos creado:

      - name: Set short git commit SHA
        run: |
          calculatedSha=$(git rev-parse --short ${{ github.sha }})
          echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV          

      - name: Docker Build and Push using Github Action
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: | 
            ${{ secrets.DOCKER_USER }}/tangelov-me:latest
            ${{ secrets.DOCKER_USER }}/tangelov-me:${{ env.COMMIT_SHORT_SHA }}

Por defecto, Github no nos proporciona el SHA “corto” de Git, así que vamos a generarlo usando comandos de git y a generar una nueva variable de entorno llamada COMMIT_SHORT_SHA. Después, lo añadimos como tag en el paso de construir y enviar el contenedor a DockerHub.

El resultado final sería el siguiente:

name: Tangelov GH Actions To DockerHub
on: 
  push:
    branches:
      - 'main'
      - 'master'

jobs:
  docker-and-push:
    runs-on: ubuntu-latest
    steps:
      - name: Check out repository code
        uses: actions/checkout@v4

      - name: Set short git commit SHA
        run: |
          calculatedSha=$(git rev-parse --short ${{ github.sha }})
          echo "COMMIT_SHORT_SHA=$calculatedSha" >> $GITHUB_ENV          

      - name: Setup Hugo in Github Actions
        uses: peaceiris/actions-hugo@v2

      - name: Build Hugo static content
        run: hugo --minify

      - name: Save output for next steps
        uses: actions/cache@v2
        with:
          path: public
          key: public

      - name: Docker Login using Github Action
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USER }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Docker Build and Push using Github Action
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: | 
            ${{ secrets.DOCKER_USER }}/tangelov-me:latest
            ${{ secrets.DOCKER_USER }}/tangelov-me:${{ env.COMMIT_SHORT_SHA }}

Et voilá:

docker-github-action

dockerhub-final

Como podemos ver, el identificador está compartido entre el código de nuestro repositorio y la imagen almacenada en DockerHub.

Conclusión

Github Actions es una plataforma de CICD completa, que no tiene nada que envidiarle a su competencia, que además se beneficia de una inmensa comunidad y popularidad y que facilita la reutilización de código. En puntos donde puede quedarse un poco coja, la comunidad ha tomado el testigo ampliando sus funcionalidades hasta el infinito.

Tiene integraciones nativas con gran cantidad de proveedores de nube y el único pero que tengo de ella serían sus incidencias técnicas, pero espero que su cadencia se vaya reduciendo a medida que el producto esté más pulido.

Así que me despido y espero que este post os sea útil, ¡Happy Coding!

Documentación

Revisado a 12-11-2022