Crear una canalización de CI/CD con GitHub Actions

  • Imagen de redactor Daniel J. Saldaña
  • 27 de diciembre de 2022
Crear una canalización de CI/CD con GitHub Actions

Vamos a desarrollar una solución para una canalización de CI/CD con GitHub Actions.

Para ello vamos a abordar distintos aspectos como pueden ser:

  • Pasar varios code linters en busca de errores de sintaxis
  • Generar un diagrama del contendido del repositorio
  • Autoincrementar la versión del package
  • Automatizar la creación de la release en GitHub
  • Automatizar la actualización del changelog conforme a los cambios en la release
  • Eliminar las últimas 10 release y tags
  • Generar reporte de PageSpeed Insights y adjuntar en el pipeline
  • Generar la imagen Docker y subirla al Registry de GitHub
  • Buscar información sensible “contraseñas o token” en los commit
  • Automatizar la creación de la pull request conforme a una plantilla
  • Crear distintos disparadores en el pipeline según el contenido del commit

Ahora que tenemos los puntos definidos de lo que queremos realizar en nuestra automatización, vamos a comenzar.

GitHub - danieljesussp/danieljsaldana-terminal

Para empezar, vamos a crear nuestro fichero con todos los jobs con los que vamos a trabajar.

📋 En relación con los tutoriales anteriores, en este introduciremos los eventos que disparan nuestro worflow, condicionales y la necesidad de que se complete un job anterior.

Tenemos dos disparados distintos. Por un lado, tenemos el disparador task completed e issue resolved. Cada uno de ellos, nos permitirá controlar que jobs se van a ejecutar según el contenido commit.

ci.yml
name: CI + CD
on:
push:
branches:
- '**'
- '!production'
pull_request:
branches:
- '**'
types:
- synchronize
- closed
concurrency:
group: ci-tests-${{ github.ref }}-1
cancel-in-progress: true
jobs:
gitguardian:
name: GitGuardian scan
if: github.event_name == 'pull_request' && github.event.action == 'synchronize' || contains(github.event.head_commit.message, 'task completed') || contains(github.event.head_commit.message, 'issue resolved')
uses: ./.github/workflows/gitguardian.yml
secrets:
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}
super_linter:
name: Super-Linter scan
if: github.event_name == 'pull_request' && github.event.action == 'synchronize' || contains(github.event.head_commit.message, 'task completed') || contains(github.event.head_commit.message, 'issue resolved')
uses: ./.github/workflows/super-linter.yml
create_pull_request:
name: Create pull request
if: contains(github.event.head_commit.message, 'issue resolved')
needs: [super_linter, gitguardian]
uses: ./.github/workflows/create-pull-request.yml
repo_visualizer:
name: Update diagram
if: github.event.pull_request.merged == true
uses: ./.github/workflows/repo_visualizer.yml
release:
name: Create release
if: github.event.pull_request.merged == true
needs: [repo_visualizer]
uses: ./.github/workflows/release.yml
delete_older_releases:
name: Delete older releases
if: github.event.pull_request.merged == true
needs: [release]
uses: ./.github/workflows/delete-tag-and-release.yml
lighthouse:
name: Lighthouse check action
if: github.event.pull_request.merged == true
uses: ./.github/workflows/lighthouse.yml
build_and_push_to_registry:
name: Build and push Docker image to GitHub Packages
if: github.event.pull_request.merged == true
needs: [release]
uses: ./.github/workflows/build.yml
trivy_scan:
name: Trivy image scan
if: github.event.pull_request.merged == true
needs: [build_and_push_to_registry]
uses: ./.github/workflows/trivy-image.yml

En este job vamos a revisar que nuestros commit no contengan ningún token o contraseña.

💡 Aquí podríamos agregar otros jobs, como por ejemplo test funcionales, pero esto lo dejaremos para más adelante.

gitguardian.yml
name: GitGuardian scan
on:
workflow_call:
secrets:
GITGUARDIAN_API_KEY:
required: false
jobs:
security:
name: GitGuardian scan
runs-on: ubuntu-latest
steps:
- name: '☁️ checkout repository'
uses: actions/checkout@v3
with:
fetch-depth: 0 # fetch all history so multiple commits can be scanned
- name: GitGuardian scan
uses: GitGuardian/gg-shield-action@master
env:
GITHUB_PUSH_BEFORE_SHA: ${{ github.event.before }}
GITHUB_PUSH_BASE_SHA: ${{ github.event.base }}
GITHUB_PULL_BASE_SHA: ${{ github.event.pull_request.base.sha }}
GITHUB_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
GITGUARDIAN_API_KEY: ${{ secrets.GITGUARDIAN_API_KEY }}

Esta es una solución de las muchas que podemos encontrar, ya que verifica que nuestros ficheros no tengan errores de sintaxis. Esto puede ser interesante si tenemos manifiestos Kubernetes o los propios pipeline.

super-linter.yml
name: Lint Code scan
on:
workflow_call:
jobs:
super-linter:
runs-on: ubuntu-latest
steps:
- name: '☁️ checkout repository'
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Lint Code Base
uses: github/super-linter@v4
env:
DEFAULT_WORKSPACE: .github
VALIDATE_ALL_CODEBASE: false
DEFAULT_BRANCH: production
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Archive super-linter artifacts
uses: actions/upload-artifact@v2
with:
name: MegaLinter reports
path: |
super-linter.log

Ahora vamos a automatizar la apertura de la pull request.

create-pull-request.yml
name: Create pull request
on:
workflow_call:
jobs:
create-pull-request:
runs-on: ubuntu-latest
steps:
- name: '☁️ checkout repository'
uses: actions/checkout@v3
- name: Version Increment
id: version
run: |
echo "**********************"
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
npm version minor -m "v%s"
version=$(node -p "require('./package.json').version")
echo "::set-output name=version::${version}"
echo "**********************"
- name: Create Pull Request
id: cpr
uses: peter-evans/create-pull-request@v4
with:
commit-message: Create pull request
committer: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
author: ${{ github.actor }} <${{ github.actor }}@users.noreply.github.com>
signoff: false
branch: ${{ github.ref }}
delete-branch: true
base: production
title: 'v${{ steps.version.outputs.version }} release'
body: |
# Cambios
<!-- Incluya un resumen del cambio y qué problema se solucionó. -->
<!-- Incluya también la motivación y el contexto pertinentes. -->
<!-- Enumere las dependencias necesarias para este cambio. -->
Fixes # (issue)
## De qué se trata este PR
- Ingrese una breve descripción para este PR
### Ejecuciones de prueba
- [Run actions](<>)
## Tipo de cambio
<!-- Elimine las opciones que no sean relevantes. -->
- [ ] 📚 Actualización de documentación
- [ ] 🧪 Casos de prueba
- [ ] 🐞 Corrección de errores (cambio continuo que soluciona un problema)
- [ ] 🔬 Nueva característica (cambio continuo que agrega funcionalidad)
- [ ] 🚨 Cambio importante (corrección o característica que haría que la funcionalidad existente no funcionara como se esperaba)
- [ ] 📝 Este cambio requiere una actualización de documentación
## Checklist
- [ ] Mi código sigue las pautas de estilo de este proyecto
- [ ] He realizado una auto-revisión de mi propio código
- [ ] He comentado mi código, particularmente en áreas difíciles de entender
- [ ] He realizado los cambios correspondientes a la documentación.
- [ ] Mis cambios no generan nuevas advertencias
- [ ] ¿Actualizó CHANGELOG en caso de un cambio importante?
labels: |
automated pr
assignees: ${{ github.actor }}
reviewers: ${{ github.actor }}
draft: false

Ahora vamos a una imagen svg con el que podamos tener un gráfico de burbujas del contenido de nuestro repositorio.

repo_visualizer.yml
name: Repo Visualizer
on:
workflow_call:
jobs:
repo-visualizer:
name: Repo Visualizer
runs-on: ubuntu-latest
steps:
- name: '☁️ checkout repository'
uses: actions/checkout@v3
- name: Update diagram
uses: githubocto/repo-visualizer@0.7.1
with:
output_file: 'diagram.svg'
excluded_paths: 'dist,node_modules'

En el siguiente job, lo más relevante bajo mi punto de vista, sería el hecho de usar una variable en otro step.

release.yml
name: Create release
on:
workflow_call:
jobs:
version:
permissions:
contents: write
pull-requests: write
runs-on: ubuntu-latest
steps:
- name: '☁️ checkout repository'
uses: actions/checkout@v3
- name: Version Increment
id: version
run: |
echo "**********************"
git config user.name "$GITHUB_ACTOR"
git config user.email "$GITHUB_ACTOR@users.noreply.github.com"
npm version minor -m "v%s"
version=$(node -p "require('./package.json').version")
git tag ${VERSION}
git push --force
echo "::set-output name=version::${version}"
echo "**********************"
- name: 'Create release'
uses: release-drafter/release-drafter@v5
id: release
with:
version: ${{ steps.version.outputs.version }}
publish: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Update Changelog
uses: stefanzweifel/changelog-updater-action@v1
with:
latest-version: ${{ steps.release.outputs.tag_name }}
release-notes: ${{ steps.release.outputs.body }}
- name: Commit updated CHANGELOG
uses: stefanzweifel/git-auto-commit-action@v4
with:
branch: ${{ github.event.release.target_commitish }}
commit_message: Update CHANGELOG
file_pattern: CHANGELOG.md

Este paso nos evitará tener que ir borrando manualmente antiguas release y tags.

💡 Si queremos tener todas las release y tags, solo tendremos que eliminar este fichero y también de la parte de ci.yml

delete-tag-and-release.yml
name: Delete tag and release
on:
workflow_call:
jobs:
delete-tag-and-release:
runs-on: ubuntu-latest
steps:
- uses: dev-drprasad/delete-older-releases@v0.2.0
with:
keep_latest: 10
delete_tags: true
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Este job ya nos suena, ya que lo hemos empleado anteriormente.

lighthouse.yml
name: Lighthouse check
on:
workflow_call:
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- name: '☁️ checkout repository'
uses: actions/checkout@v3
- name: 'Create temporary directory'
run: mkdir -p ${{ github.workspace }}/lighthouse/artifacts
- name: Lighthouse
uses: foo-software/lighthouse-check-action@master
with:
outputDirectory: ${{ github.workspace }}/lighthouse/artifacts
urls: 'https://terminal.danieljsaldaña.com'
- name: Upload artifacts
uses: actions/upload-artifact@master
with:
name: Lighthouse reports
path: ${{ github.workspace }}/lighthouse/artifacts

Ahora sí, llego el momento de crear nuestra imagen y subirla a nuestro Registry.

build.yml
name: Build and push Docker image to GitHub Packages
on:
workflow_call:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build_and_push_to_registry:
name: Build and push Docker image to GitHub Packages
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=1.0.${{ github.run_number }},priority=1000
type=ref,event=branch
type=sha
type=raw,value=latest
- name: Build image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
context: .
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Push Docker image
uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc
with:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
push: true

En este caso vamos a utilizar Trivy, pero podemos utilizar la herramienta que queramos para analizar nuestra imagen.

💡 Este job podríamos agregarlo a la parte de build y analizar nuestra imagen antes de publicarla. De esta forma, si tuviera vulnerabilidades críticas, evitaríamos su publicación.

trivy-image.yml
name: Trivy scan image
on:
workflow_call:
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build_and_push_to_registry:
name: Build and push Docker image to GitHub Packages
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: 'Create temporary directory'
run: mkdir -p ${{ github.workspace }}/trivy-image/artifacts
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest'
format: 'table'
exit-code: '0'
ignore-unfixed: true
vuln-type: 'os,library'
output: trivy-image/artifacts/trivy-image.log
severity: 'CRITICAL,HIGH'
env:
TRIVY_USERNAME: ${{ github.repository_owner }}
TRIVY_PASSWORD: ${{ secrets.GITHUB_TOKEN }}
- name: Upload artifacts
uses: actions/upload-artifact@master
with:
name: Trivy image reports
path: ${{ github.workspace }}/trivy-image/artifacts

Ahora sí, hemos terminado nuestra automatización. Espero que os haya parecido interesante este lavatorio.

¡Suscríbete y recibe actualizaciones sobre tecnología, diseño, productividad, programación y mucho más!
0
0