Crear una canalización de CI/CD con GitHub Actions
Daniel J. Saldaña
- 27 de diciembre de 2022
- Puntuación de feedback

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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.