En el anterior post dejamos implementado un flujo de CI con GitHub Actions después de agregarle varias mejoras como el uso de containers y la paralización de jobs.
En este post vamos a cerrar un flujo completo de CI/CD que nos permitirá tener automatizado todo cambio que queramos introducir en nuestra aplicación.
Agregando el CD en nuestra CI
Configuración del proyecto para integrarlo con Firebase
Para terminar nuestro flujo y tener nuestra aplicación en un sitio donde podamos visualizarlo, he elegido Firebase que nos va a permitir alojar nuestra aplicación estática en una dirección web accesible de manera gratuita y con integración con GitHub actions.
Como punto de partida continuaremos a partir de donde lo dejamos en el punto anterior. En esta rama de mi repositorio de GitHub podemos continuar a partir de cómo se quedó el proyecto configurado.
Para empezar una vez nos hemos creado una cuenta e iniciado sesión en Firebase. Accedemos a la pantalla de bienvenida y seleccionamos crear nuevo proyecto:
Indicamos el nombre, le damos a siguiente, ignoramos las opciones adicionales y finalizamos y ya tendremos nuestro proyecto creado. Ahora indicamos que este proyecto sea para tipo de aplicación web y seleccionamos la opción de configurar firebase Hosting para poder desplegar nuestra app:
Ejecutamos el comando solo para instalar Firebase y las herramientas para desplegar el hosting:
npm install -g firebase
npm install -g firebase-tools
Una vez instalado lanzamos el siguiente comando para hacer login con nuestras credenciales a firebase:
firebase login
Ahora que estamos logueados empezamos a configurar el proyecto con el siguiente comando en nuestro proyecto con Vue:
firebase init
Seleccionamos la opción configurar nuestros ficheros a hacer deploy y GitHub Actions:
Indicamos que vamos a usar un proyecto existente:
Los pasos para configurar el Hosting son los siguientes:
- La carpeta que contiene nuestro código a desplegar es dist.
- Marcamos que nuestra aplicación no es SPA.
- Queremos configurar builds y deploys con GitHub.
- Permitimos a Firebase acceder a nuestra cuenta de GitHub.
- Indicamos el repositorio de GitHub que contiene nuestro proyecto.
- No queremos un script que haga build antes de hacer el deploy.
- Indicamos que queremos desplegar en nuestro live channel cuando se integre una Pull Request.
- Indicamos que nuestra rama de live channel es master.
Después de todos estos pasos la configuración debería quedar así:
Esto nos habrá creado creado en la carpeta .github/workflows los ficheros firebase-hosting-merge.yml y firebase-hosting-pull-request.yml que contienen respectivamente el flujo para desplegar cuando es en la rama master o en una Pull Request.
Si analizamos el fichero firebase-hosting-pull-request.yml podemos ver lo siguiente:
name: Deploy to Firebase Hosting on PR
on: pull_request
jobs:
build_and_preview:
if: '${{ github.event.pull_request.head.repo.full_name == github.repository }}'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_VUE_GITHUB_ACTIONS_EXAMPLE }}'
channelId: preview
projectId: vue-github-actions-example
La instrucción on indica que este workflow se lanzará cada vez que creemos una Pull Request a cualquier rama.
El job build_and_preview en la cláusula if indica que solo se lanzará cuando la Pull Request sea hacia una rama del mismo repositorio y no de otro.
Utiliza el action FirebaseExtended/action-hosting-deploy@v0 que nos abstrae de la lógica y comandos necesarios para desplegar en nuestro hosting de firebase.
También hace uso de los Secrets para que nuestras credenciales no sean públicas en el código. El primer secret, GITHUB_TOKEN hace referencia a un token que se genera por GitHub en cada ejecución que nos permite autenticar de manera automática nuestra cuenta de GitHub con aplicaciones externas. Mientras el secret FIREBASE_SERVICE_ACCOUNT_VUE_GITHUB_ACTIONS_EXAMPLE hace referencia al token de una service account de firebase para autenticarnos con nuestro hosting también de manera programática. Si nos vamos a la configuración de nuestro proyecto en GitHub podemos ver el apartado para crear y actualizar los secrets de nuestro repositorio:
Por otro lado el fichero firebase-hosting-merge.yml es prácticamente igual pero con algunos cambios:
name: Deploy to Firebase Hosting on merge
on:
push:
branches:
- master
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_VUE_GITHUB_ACTIONS_EXAMPLE }}'
channelId: live
projectId: vue-github-actions-example
Tiene indicado en el valor on.push.branches que se lance este workflow cuando el push sea a la rama master.
El job build_and_deploy es exactamente igual al anterior, con la excepción que al action se le ha agregado el parámetro channelId con el valor live para para indicar que los cambios se tienen que hacer públicos en nuestro dominio principal mientras que el valor preview del job de build_and_preview sirve para indicar que nos va a generar una url temporal donde previsualizar los nuevos cambios.
NOTA: debido a que puede haber problemas por algunas limitaciones encontradas, he preferido reemplazar nuestro GITHUB_TOKEN por un token de acceso personal en nuestro flujo de Pull Request, este cambio no debería afectar a nuestro flujo que queremos realizar, pero podemos crear uno con los siguientes permisos si así lo queremos:
Y en las opciones de nuestro repositorio lo agregamos como secret para poder utilizarlo:
Por último reemplazamos en nuestro fichero firebase-hosting-pull-request.yml la variable secrets.GITHUB_TOKEN por secrets.PAT_SECRET
Integrar nuestro flujo de CD con Firebase en nuestra CI
Ahora que hemos analizado nuestro flujo, para hacerlo funcionar con buenas prácticas e integrarlo con nuestro flujo de CI creado anteriormente debemos realizar los siguientes pasos:
- Nuestro workflow de Pull Request solo se tiene que ejecutar una vez haya finalizado correctamente el workflow de CI definido en los pasos anteriores dentro de nuestro fichero node.js.yml.
- Que no se pueda mergear la Pull Request en la rama master hasta que haya pasado mínimo correctamente una ejecución correcta de nuestro flujo de CI/CD.
- Para hacer funcionar nuestro despliegue y optimizar los pasos que hemos realizado entre nuestros workflows, la carpeta dist a desplegar generada en nuestro workflow de CI debemos poder incluirla en nuestros workflows de CD sin tener que volver a generar el proceso que la construye.
name: Deploy to Firebase Hosting on PR
on:
workflow_run:
workflows: ["Node.js CI"]
types:
- completed
Con este cambio este fichero se vuelve dependiente de nuestro otro fichero de CI, y se ejecutará siempre que el workflow de CI finalice correctamente, por ello vamos a modificar las condiciones que lanzan el workflow node.js.yml. para que se lance solo en Pull Request hacia la rama master eliminando la clave on.push.branches por lo que la sección de los triggers quedaría así:
name: Node.js CI
on:
pull_request:
branches:
- 'master'
Por último tenemos que indicar en el fichero firebase-hosting-merge.yml que se ejecute cuando se cierre una Pull Request contra la rama master:
name: Deploy to Firebase Hosting on merge
on:
pull_request:
types:
- closed
branches:
- 'master'
Como para que algunos cambios de los eventos indicados en los workflows funcionen hay que subirlos directamente a la rama default, en nuestro caso master, vamos a realizar el resto de cambios necesarios en la rama master para luego crear algún cambio a visualizar en la web y validar todo el flujo.
Ahora para poder utilizar la carpeta dist generada en nuestro job de CI, tenemos que utilizar esta action que nos va a hacer de interfaz de la API de GitHub Actions para abstraernos de cómo implementar la API para descargar los artefactos generados en otros workflows.
Los artefactos que hemos usado anteriormente que son los oficiales de GitHub para subir y descargar artefactos, solo están pensados para usarse dentro de un mismo flujo de workflow, por lo que a la hora de escribir este artículo no hay un action oficial soportado por el equipo de GitHub.
¡Pero precisamente ahí reside la potencia del marketplace y que cualquiera pueda contribuir con sus propias actions!
Por lo que nuestro fichero firebase-hosting-pull-request.yml después de introducir el nuevo action quedaría de la siguiente manera:
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on PR
on:
workflow_run:
workflows: ["Node.js CI"]
types:
- completed
jobs:
build_and_preview:
if: ${{ github.event.pull_request.head.repo.full_name == github.repository }} && ${{ github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Download artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: node.js.yml
workflow_conclusion: success
pr: ${{github.event.pull_request.number}}
name: build-folder
path: dist/
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_VUE_GITHUB_ACTIONS_EXAMPLE }}'
projectId: vue-github-actions-example
Con la propiedad on.workflow_run.workflows indicamos que este workflow se lanza siempre después de que se complete uno de CI, pero para indicar que solo queremos que se lance cuando se completa correctamente, tenemos que agregar a nuestra expresión if la evaluación de que la variable del status del workflow que lo ha desencadenado ha sido finalizada correctamente.
Ahora el fichero firebase-hosting-merge.yml quedaría practicamente igual:
# This file was auto-generated by the Firebase CLI
# https://github.com/firebase/firebase-tools
name: Deploy to Firebase Hosting on merge
on:
pull_request:
types:
- closed
branches:
- 'master'
jobs:
build_and_deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Download artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: node.js.yml
workflow_conclusion: success
pr: ${{github.event.pull_request.number}}
name: build-folder
path: dist/
- uses: FirebaseExtended/action-hosting-deploy@v0
with:
repoToken: '${{ secrets.GITHUB_TOKEN }}'
firebaseServiceAccount: '${{ secrets.FIREBASE_SERVICE_ACCOUNT_VUE_GITHUB_ACTIONS_EXAMPLE }}'
channelId: live
projectId: vue-github-actions-example
Para el segundo punto de no poder mergear ninguna Pull Request a master hasta completar nuestro flujo de CI/CD primero agregamos los ficheros de firebase y de los workflows:
git add .firebaserc
git add .github/
git add firebase.json
Creamos commit y subimos los cambios:
git commit -m 'add deploy workflows with firebase'
git push
Como hemos definido que nuestro flujo de CD solo se ejecute cuando se cierre una Pull Request y el de CI cuando se abra, al subir los cambios directamente a master no va a generar ningún workflow. Por este motivo ahora podemos terminar de aplicar los cambios desde una PR.
Creamos la rama:
git add src/
git add cypress/
git commit -m 'change Hello World message'
git push --set-upstream origin feature/change-web-message
Si nos vamos ahora a la página principal de nuestro repositorio nos saldrá para crear nuestra Pull Request:
Pulsamos sobre el botón y creamos la Pull Request contra master como nos propone, y al hacerlo podremos ver como ha empezado un nuevo flujo de nuestro workflow de CI, y al finalizar este como empieza el workflow de desplegar nuestra Pull Request en la preview:
Ahora que ha finalizado nuestro flujo, la aplicación debería estar desplegada para poder visualizar los cambios, para ello volvemos a Firebase y en el apartado de Hosting de nuestro proyecto deberíamos ver que se ha generado un apartado de preview:
Y si pulsamos sobre el botón para visualizarlo nos llevará a la url con nuestros cambios desplegados:
Configuración de reglas para proteger las ramas principales
Aún así podíamos haber integrado nuestra Pull Request sin darnos cuenta antes de que acabase el flujo de CI para validar que todo ha ido correctamente, por ello vamos a la configuración de nuestro repositorio a la parte de ramas donde podemos crear reglas para indicar los criterios a cumplir.
Por lo que vamos a crear una nueva regla para proteger la rama master y exigir que solo se pueda integrar cambios mediante Pull Request y que nuestro job de despliegue haya finalizado correctamente:
Básicamente GitHub en el caso de GitHub Actions considera un status_check cualquier job que se haya lanzado al menos una vez en un flujo de workflow, por eso siempre que modifiquemos nuestros workflows con nuevos jobs que queremos que sean bloqueantes deben ser ejecutados al menos una vez antes de poder agregarlos aquí.
Otra limitación es que no podemos agregar cualquier job que tengamos, es decir, solo funciona con los jobs que se desencadenan en el flujo de Pull Request, que en nuestro caso es nuestro flujo de CI en el fichero node.js.yml, y debido a que nuestro workflow de despliegue se desencadena después, no podemos agregar como condición bloqueante que se ha desplegado correctamente.
En este caso como se visualiza en la imagen de arriba, en el buscador introducimos el nombre de nuestros jobs de CI, y los que vamos a considerar como bloqueantes son:
- ci-unit
- build (16.x)
- ci-e2e-chrome (cypress/browsers:node16.5.0-chrome94-ff93, 94.0.4606.71)
- ci-e2e-firefox (cypress/browsers:node16.5.0-chrome94-ff93, 93)
Al mergear ya deberíamos ver como empieza nuestro último workflow:
Y una vez que ha finalizado correctamente deberíamos ver en firebase que hemos desplegado nuestra primera release:
¡Al igual que antes si accedemos a la url deberíamos ver nuestra web desplegada!
Conclusión
Haciendo un resumen de todas las funcionalidades que hemos tocado de GitHub Actions, que no han sido pocas cosas, podemos decir que hemos visto:
- Crear un flujo básico de CI creando templates a través de la interfaz y de la herramienta cli del framework web que hemos elegido.
- Paralelizar diferentes jobs para reducir el tiempo de ejecución de nuestro flujo de CI.
- Definir partes del flujo que dependen de que finalicen correctamente jobs anteriores como condición antes de empezar.
- Utilizar funciones más avanzadas de GitHub Actions como la matrix para testear compatibilidades con diferentes versiones de node o lanzar tests en diferentes versiones de navegador en paralelo.
- Cómo lanzar nuestros workflows en algunas versiones de diferentes sistemas operativos o en imágenes de docker.
- Cachear dependencias usando actions para reducir el tiempo de ejecución en la instalación de las mismas.
- Utilizar diferentes Actions para almacenar y recuperar diferentes artefactos, tanto para mostrar los resultados de nuestras pruebas desde la propia interfaz de GitHub, como para simular un flujo de reutilizar el mismo artefacto entre diferentes workflows.
- Dividir nuestro flujo de CI/CD en diferentes ficheros que se desencadenan según diferentes eventos cuando realizamos acciones en el repositorio e incluso desencadenar workflows según el resultado de otros.
- Cómo desplegar nuestra aplicación en un hosting gratuito de Firebase.
- El uso de secrets para almacenar las credenciales de nuestras cuentas de GitHub y Firebase.
- Crear reglas para establecer condiciones de nuestros workflows que se deben finalizar correctamente para poder mergear nuestra Pull Request.
Y con esto creo que no me dejo nada en el tintero. La versión final del código podéis encontrarla en este repositorio.
¡Espero que hayáis aprendido mucho y nos vemos a la próxima!


