Workflows Avanzados en Forgejo Actions

Por: Artiko
forgejoactionsworkflowsmatrixartifactsreusableci-cd

Capitulo 17: Workflows Avanzados

Con los conceptos basicos dominados, este capitulo cubre patrones mas sofisticados que resuelven problemas comunes en pipelines de CI/CD reales.

Matrix strategy

La estrategia de matrix permite ejecutar el mismo job con multiples combinaciones de variables. Es ideal para probar en varias versiones de un lenguaje o sistema operativo.

Ejemplo: probar en multiples versiones de Node.js

name: Tests matrix

on:
  push:
    branches: [main]

jobs:
  test:
    name: Node ${{ matrix.node-version }}
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: ["18", "20", "22"]
      fail-fast: false

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: "npm"

      - run: npm ci
      - run: npm test

Con esta configuracion se crean tres jobs independientes, uno por cada version de Node. Todos corren en paralelo si hay runners disponibles.

fail-fast: por defecto es true, lo que cancela todos los jobs de la matrix si uno falla. Cambiar a false permite que todos terminen independientemente.

Matrix con multiples dimensiones:

strategy:
  matrix:
    os: [ubuntu-latest, debian-latest]
    node: ["18", "20"]

Esto genera cuatro combinaciones: ubuntu+node18, ubuntu+node20, debian+node18, debian+node20.

Excluir combinaciones especificas:

strategy:
  matrix:
    node: ["18", "20", "22"]
    exclude:
      - node: "18"

Concurrency

El campo concurrency controla que pasa cuando multiples ejecuciones del mismo workflow estan activas al mismo tiempo.

Caso de uso tipico: cuando haces varios pushes rapidos a main, no quieres que todos los deployments corran en paralelo. Solo el ultimo deberia ejecutarse.

name: Deploy

on:
  push:
    branches: [main]

concurrency:
  group: deploy-${{ github.ref }}
  cancel-in-progress: true

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - run: echo "Desplegando..."

Con cancel-in-progress: true, si llega una nueva ejecucion mientras la anterior esta corriendo, la anterior se cancela automaticamente.

Para PRs es util agrupar por rama:

concurrency:
  group: pr-${{ github.event.pull_request.number }}
  cancel-in-progress: true

Artifacts

Los artifacts permiten guardar archivos generados durante un job y compartirlos entre jobs o descargarlos desde la UI.

Subir un artifact:

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build

      - name: Guardar artifact de build
        uses: actions/upload-artifact@v4
        with:
          name: dist-produccion
          path: dist/
          retention-days: 7

Descargar el artifact en otro job:

  deploy:
    runs-on: ubuntu-latest
    needs: build
    steps:
      - name: Descargar artifact
        uses: actions/download-artifact@v4
        with:
          name: dist-produccion
          path: ./dist

      - name: Desplegar
        run: rsync -av ./dist/ usuario@servidor:/var/www/html/

El needs: build garantiza que el job deploy espera a que build termine antes de ejecutarse.

Dependencias entre jobs y outputs

El campo needs define el orden de ejecucion. Tambien es posible pasar datos entre jobs usando outputs.

jobs:
  version:
    runs-on: ubuntu-latest
    outputs:
      tag: ${{ steps.get-version.outputs.version }}

    steps:
      - uses: actions/checkout@v4

      - name: Obtener version
        id: get-version
        run: |
          VERSION=$(node -p "require('./package.json').version")
          echo "version=$VERSION" >> $GITHUB_OUTPUT

  build:
    runs-on: ubuntu-latest
    needs: version
    steps:
      - uses: actions/checkout@v4

      - name: Build con version ${{ needs.version.outputs.tag }}
        run: npm run build
        env:
          APP_VERSION: ${{ needs.version.outputs.tag }}

Para que un step produzca un output, usa echo "clave=valor" >> $GITHUB_OUTPUT. Luego el job lo expone en outputs y otros jobs lo acceden con needs.nombre-job.outputs.clave.

Condicionales

El campo if permite saltar steps o jobs enteros segun condiciones.

Condicionales en jobs:

jobs:
  deploy-staging:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/develop'
    steps:
      - run: ./deploy.sh staging

  deploy-prod:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - run: ./deploy.sh production

Condicionales en steps:

steps:
  - name: Notificar en Slack solo si falla
    if: failure()
    run: curl -X POST ${{ secrets.SLACK_WEBHOOK }} -d '{"text":"Pipeline fallido"}'

  - name: Solo en ejecucion manual
    if: github.event_name == 'workflow_dispatch'
    run: echo "Ejecutado manualmente"

Funciones disponibles en condicionales:

Workflows reutilizables

Un workflow puede ser llamado desde otro workflow usando el trigger workflow_call. Esto evita duplicar logica entre proyectos o pipelines.

Definir el workflow reutilizable (.forgejo/workflows/ci-reutilizable.yaml):

name: CI reutilizable

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: "20"
      run-lint:
        required: false
        type: boolean
        default: true
    secrets:
      NPM_TOKEN:
        required: false

jobs:
  ci:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node-version }}

      - name: Instalar dependencias
        run: npm ci
        env:
          NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

      - name: Lint
        if: ${{ inputs.run-lint }}
        run: npm run lint

      - run: npm test

Llamar al workflow reutilizable desde otro workflow:

name: Pipeline principal

on:
  push:
    branches: [main]
  pull_request:

jobs:
  ci:
    uses: ./.forgejo/workflows/ci-reutilizable.yaml
    with:
      node-version: "22"
      run-lint: true
    secrets:
      NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

Ejemplo completo: build, test con matrix y deploy

Workflow que integra todo lo visto: build inicial, pruebas en matrix de versiones, y deploy condicional solo si todo pasa:

name: Pipeline completo

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

concurrency:
  group: pipeline-${{ github.ref }}
  cancel-in-progress: true

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    outputs:
      artifact-name: dist-${{ github.sha }}

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - run: npm ci
      - run: npm run build

      - uses: actions/upload-artifact@v4
        with:
          name: dist-${{ github.sha }}
          path: dist/
          retention-days: 1

  test:
    name: Tests Node ${{ matrix.node }}
    runs-on: ubuntu-latest
    needs: build

    strategy:
      matrix:
        node: ["18", "20", "22"]
      fail-fast: false

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
          cache: "npm"

      - run: npm ci
      - run: npm test

  deploy:
    name: Deploy a produccion
    runs-on: ubuntu-latest
    needs: [build, test]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'

    steps:
      - uses: actions/download-artifact@v4
        with:
          name: dist-${{ github.sha }}
          path: ./dist

      - name: Deploy
        run: rsync -av --delete ./dist/ ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/var/www/app/
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_SSH_KEY }}

En este pipeline:

  1. build compila la aplicacion y sube el artifact
  2. test corre en paralelo con tres versiones de Node, esperando a que build termine
  3. deploy solo corre si build y todos los jobs de test pasaron, y ademas es un push a main (no un PR)

Esta estructura garantiza que nunca se despliega codigo que no compilo o que fallo las pruebas en alguna version soportada.


Siguiente: Capitulo 18: Volver al indice —>