Módulo 7: Monitoring, Notifications y Advanced Patterns

5. Reusable Workflows

Descripción

Tienes 3 microservicios AI: un chatbot, un clasificador de documentos, y un generador de reportes. Los tres usan Python, los tres tienen el mismo flujo de CI (lint → test → AI checks), y los tres usan Docker para deployment. El YAML de CI es prácticamente idéntico en los 3 repos — con pequeñas diferencias como el nombre del servicio o la versión de Python.

Cuando necesitas agregar un step de security scanning al pipeline, tienes que modificar 3 archivos en 3 repos. Cuando actualizas Python de 3.11 a 3.12, son 3 PRs. Un mes después descubres que uno de los repos nunca se actualizó porque el PR se olvidó. Estás manteniendo 3 copias del mismo pipeline, y cada copia diverge silenciosamente de las demás.

Reusable workflows resuelven esto. Defines el workflow una vez en un repo central, y los otros repos lo llaman con workflow_call. Cuando actualizas el workflow central, todos los repos que lo usan reciben el cambio automáticamente. Es DRY aplicado a CI/CD — no por estética, sino por operabilidad.

Conexión con el pipeline final: En el pipeline integrador (Módulo 8), los reusable workflows permiten que el CI, los AI checks, y el CD compartan la misma lógica sin duplicación — un cambio en el workflow se propaga a todos los pipelines.


workflow_call: El trigger de reusable workflows

¿Qué es workflow_call?

Es un trigger que convierte un workflow en una "función" que otros workflows pueden llamar:

# Workflow reutilizable (definición)
on:
  workflow_call:
    inputs:
      python-version:
        type: string
        default: "3.12"
    secrets:
      openai-api-key:
        required: true
# Workflow que llama (consumidor)
jobs:
  ci:
    uses: org/shared-workflows/.github/workflows/ai-ci.yml@main
    with:
      python-version: "3.12"
    secrets:
      openai-api-key: ${{ secrets.OPENAI_API_KEY }}

Analogía

Piensa en un reusable workflow como una función:

Función Python:
  def run_ci(python_version="3.12", api_key=None):
      lint()
      test()
      ai_checks(api_key)

Reusable workflow:
  workflow_call:
    inputs: { python-version: "3.12" }
    secrets: { openai-api-key }
  jobs:
    lint: ...
    test: ...
    ai-checks: ...

Crear un reusable workflow

Paso 1: El workflow reutilizable

# Repo: org/shared-workflows
# Archivo: .github/workflows/ai-ci.yml
name: Reusable AI CI Pipeline

on:
  workflow_call:
    inputs:
      python-version:
        description: "Python version to use"
        type: string
        default: "3.12"
      run-ai-checks:
        description: "Whether to run AI-specific checks"
        type: boolean
        default: true
      working-directory:
        description: "Directory containing the project"
        type: string
        default: "."
    secrets:
      openai-api-key:
        description: "OpenAI API key for AI checks"
        required: false

jobs:
  lint:
    name: "Lint & Format"
    runs-on: ubuntu-latest
    timeout-minutes: 5
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}

      - name: Install linting tools
        run: pip install ruff

      - name: Run linter
        working-directory: ${{ inputs.working-directory }}
        run: ruff check .

      - name: Check formatting
        working-directory: ${{ inputs.working-directory }}
        run: ruff format --check .

  test:
    name: "Tests"
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
          cache: pip

      - name: Install dependencies
        working-directory: ${{ inputs.working-directory }}
        run: pip install -r requirements.txt

      - name: Run tests
        working-directory: ${{ inputs.working-directory }}
        run: pytest tests/ -v --tb=short

  ai-checks:
    name: "AI Checks"
    runs-on: ubuntu-latest
    timeout-minutes: 15
    if: ${{ inputs.run-ai-checks }}
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
          cache: pip

      - name: Install dependencies
        working-directory: ${{ inputs.working-directory }}
        run: pip install -r requirements.txt

      - name: Run prompt regression
        working-directory: ${{ inputs.working-directory }}
        env:
          OPENAI_API_KEY: ${{ secrets.openai-api-key }}
        run: python scripts/prompt_regression.py --mode check

      - name: Run cost estimation
        working-directory: ${{ inputs.working-directory }}
        env:
          OPENAI_API_KEY: ${{ secrets.openai-api-key }}
        run: python scripts/cost_estimation.py --threshold 0.10

Paso 2: Llamar desde otro repo

# Repo: org/chatbot-service
# Archivo: .github/workflows/ci.yml
name: CI Pipeline

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

jobs:
  ci:
    uses: org/shared-workflows/.github/workflows/ai-ci.yml@main
    with:
      python-version: "3.12"
      run-ai-checks: true
    secrets:
      openai-api-key: ${{ secrets.OPENAI_API_KEY }}

Paso 3: Llamar desde otro repo con diferencias

# Repo: org/document-classifier
# Archivo: .github/workflows/ci.yml
name: CI Pipeline

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

jobs:
  ci:
    uses: org/shared-workflows/.github/workflows/ai-ci.yml@main
    with:
      python-version: "3.11"       # Este repo aún usa 3.11
      run-ai-checks: false          # No tiene prompt regression
    secrets:
      openai-api-key: ${{ secrets.OPENAI_API_KEY }}

Inputs y Secrets

Tipos de inputs

TipoEjemploUso
string"3.12"Versiones, nombres, paths
booleantrueFeature flags
number15Timeouts, thresholds
on:
  workflow_call:
    inputs:
      python-version:
        type: string
        default: "3.12"
      run-ai-checks:
        type: boolean
        default: true
      timeout-minutes:
        type: number
        default: 15

Secrets

Los secrets se pasan explícitamente — el reusable workflow no tiene acceso automático a los secrets del repo que lo llama:

on:
  workflow_call:
    secrets:
      openai-api-key:
        required: true
      slack-webhook:
        required: false

secrets: inherit

Si no quieres listar cada secret explícitamente, puedes pasar todos los secrets del repo:

jobs:
  ci:
    uses: org/shared-workflows/.github/workflows/ai-ci.yml@main
    with:
      python-version: "3.12"
    secrets: inherit  # Pasa TODOS los secrets del repo

Trade-off:

AspectoSecrets explícitossecrets: inherit
SeguridadSolo pasa lo necesarioPasa todo
ClaridadDocumentado qué secrets se usanOpaco
MantenimientoActualizar cuando se agrega un secretAutomático
RecomendadoPara workflows cross-orgPara workflows dentro de la misma org

Outputs de reusable workflows

Un reusable workflow puede devolver outputs al workflow que lo llamó:

# Reusable workflow con outputs
on:
  workflow_call:
    outputs:
      test-result:
        description: "Result of tests"
        value: ${{ jobs.test.outputs.result }}
      coverage:
        description: "Test coverage percentage"
        value: ${{ jobs.test.outputs.coverage }}

jobs:
  test:
    runs-on: ubuntu-latest
    outputs:
      result: ${{ steps.test.outputs.result }}
      coverage: ${{ steps.coverage.outputs.pct }}
    steps:
      - id: test
        run: |
          pytest tests/ -v
          echo "result=passed" >> $GITHUB_OUTPUT

      - id: coverage
        run: |
          COV=$(pytest tests/ --cov=src --cov-report=term | grep TOTAL | awk '{print $4}')
          echo "pct=$COV" >> $GITHUB_OUTPUT
# Consumidor que usa los outputs
jobs:
  ci:
    uses: org/shared-workflows/.github/workflows/ai-ci.yml@main
    secrets: inherit

  post-ci:
    needs: ci
    runs-on: ubuntu-latest
    steps:
      - run: |
          echo "Tests: ${{ needs.ci.outputs.test-result }}"
          echo "Coverage: ${{ needs.ci.outputs.coverage }}"

Versioning de reusable workflows

Referencing strategies

# Por branch (latest, potencialmente inestable)
uses: org/shared-workflows/.github/workflows/ai-ci.yml@main

# Por tag (versionado, estable)
uses: org/shared-workflows/.github/workflows/ai-ci.yml@v1.0.0

# Por SHA (inmutable, máxima seguridad)
uses: org/shared-workflows/.github/workflows/ai-ci.yml@abc1234567890

Recomendación

EstrategiaCuándo usar
@mainDesarrollo activo, confianza total en main
@v1 (major)Producción — solo breaking changes actualizan major
@v1.2.0 (exact)Máximo control, actualizaciones manuales
@shaSeguridad crítica, supply chain attacks concern

Para la mayoría de equipos, @v1 es el sweet spot: obtienes bug fixes y minor features automáticamente, pero breaking changes requieren que actualices explícitamente a @v2.


Cuándo usar reusable workflows vs inline

Usa reusable workflows cuando:

  • Múltiples repos comparten el mismo flujo. 3+ repos con el mismo CI pipeline
  • El flujo cambia frecuentemente. Si actualizas el pipeline cada semana, centralizar ahorra tiempo
  • Necesitas consistencia. Todos los repos deben tener exactamente el mismo quality gate
  • Equipo grande. Varios developers mantienen múltiples repos

Usa inline (workflow local) cuando:

  • Un solo repo. No hay beneficio de centralizar si solo tienes un repo
  • Flujo muy específico. El pipeline es tan custom que no aplica a otros repos
  • Iteración rápida. Estás experimentando con el pipeline y no quieres afectar otros repos
  • Equipo pequeño. 1-2 personas, 1-2 repos — el overhead de mantener un repo compartido no vale

Decision tree

¿Cuántos repos usan el mismo flujo de CI?
  ├─ 1 repo → Inline (no hay beneficio de centralizar)
  ├─ 2 repos → Depende (si el flujo es idéntico, considera reusable)
  └─ 3+ repos → Reusable workflow (la mantenibilidad lo justifica)

Cross-repo reusable workflows

Repositorio público

Si el repo con los reusable workflows es público, cualquier repo puede llamarlo:

uses: public-org/shared-workflows/.github/workflows/ci.yml@v1

Repositorio privado (misma org)

Para repos privados dentro de la misma organización, necesitas habilitar el acceso:

Repo shared-workflows → Settings → Actions → General
  → "Allow access from private repositories in the organization"

Repositorio privado (diferente org)

No es posible directamente. Las opciones son:

  1. Hacer el repo público
  2. Duplicar el workflow en cada org
  3. Usar GitHub Apps con installation tokens (fuera del scope de esta guía)

Comparaciones

Reusable workflows vs Composite actions

AspectoReusable WorkflowComposite Action
ScopeWorkflow completo (múltiples jobs)Steps dentro de un job
Triggerworkflow_calluses: en un step
Jobs propiosSí (puede definir múltiples jobs)No (corre dentro del job del caller)
RunnersPuede elegir su propio runnerUsa el runner del caller
Cuándo usarFlujo completo compartidoSteps comunes compartidos

Reusable workflows vs Templates

AspectoReusable WorkflowTemplate (copiar YAML)
ActualizaciónAutomática (cambias una vez)Manual (cambias en cada repo)
DivergenciaImposibleInevitable
FlexibilidadInputs parametrizanModificas libremente
SetupRepo compartido + referenciaCopiar archivo

Troubleshooting

"Error: could not find a workflow"

Causa: La referencia al reusable workflow está mal formateada o el archivo no existe.

Solución: Verifica la ruta completa:

# Formato correcto
uses: {owner}/{repo}/.github/workflows/{file}@{ref}

# Ejemplo
uses: org/shared-workflows/.github/workflows/ai-ci.yml@main

Verifica que el archivo existe en esa ruta exacta del repo referenciado.

"Error: permission denied"

Causa: El repo con el reusable workflow es privado y no tiene habilitado el acceso.

Solución: En el repo del reusable workflow: Settings → Actions → General → "Allow access from private repositories in the organization."

"Los secrets no están disponibles en el reusable workflow"

Causa: Los secrets no se pasan automáticamente. Necesitas pasarlos explícitamente o usar secrets: inherit.

Solución:

jobs:
  ci:
    uses: org/shared-workflows/.github/workflows/ai-ci.yml@main
    secrets:
      openai-api-key: ${{ secrets.OPENAI_API_KEY }}
    # O usa secrets: inherit para pasar todos

"El reusable workflow no refleja mis últimos cambios"

Causa: Estás referenciando una versión fija (@v1.0.0) que no incluye tus cambios.

Solución: Actualiza la referencia al tag nuevo o usa @main temporalmente para testing.


Ejercicios

Ejercicio 1: Crea un reusable workflow básico

Crea un reusable workflow que haga lint y test, parametrizado con la versión de Python.

Ver solución
# .github/workflows/reusable-ci.yml
name: Reusable CI

on:
  workflow_call:
    inputs:
      python-version:
        type: string
        default: "3.12"

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
      - run: pip install ruff
      - run: ruff check .

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
          cache: pip
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v

Ejercicio 2: Llama al reusable workflow desde otro workflow

Escribe el workflow que llama al reusable workflow del Ejercicio 1, pasando Python 3.11.

Ver solución
# .github/workflows/ci.yml
name: CI Pipeline

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

jobs:
  ci:
    uses: ./.github/workflows/reusable-ci.yml
    with:
      python-version: "3.11"

Nota: ./.github/workflows/reusable-ci.yml es la referencia para un reusable workflow en el mismo repo. Para cross-repo sería org/repo/.github/workflows/reusable-ci.yml@main.

Ejercicio 3: Agrega secrets y AI checks opcionales

Extiende el reusable workflow para incluir AI checks opcionales controlados por un input boolean.

Ver solución
on:
  workflow_call:
    inputs:
      python-version:
        type: string
        default: "3.12"
      run-ai-checks:
        type: boolean
        default: false
    secrets:
      openai-api-key:
        required: false

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
      - run: pip install ruff && ruff check .

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
          cache: pip
      - run: pip install -r requirements.txt
      - run: pytest tests/ -v

  ai-checks:
    runs-on: ubuntu-latest
    if: ${{ inputs.run-ai-checks }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: ${{ inputs.python-version }}
          cache: pip
      - run: pip install -r requirements.txt
      - run: python scripts/prompt_regression.py
        env:
          OPENAI_API_KEY: ${{ secrets.openai-api-key }}

Caller:

jobs:
  ci:
    uses: ./.github/workflows/reusable-ci.yml
    with:
      python-version: "3.12"
      run-ai-checks: true
    secrets:
      openai-api-key: ${{ secrets.OPENAI_API_KEY }}

Ejercicio 4: ¿Reusable workflow o inline?

Para cada escenario, decide si usarías un reusable workflow o un workflow inline:

  1. Un solo repo con un pipeline único de machine learning training
  2. 5 microservicios con el mismo flujo de lint → test → deploy
  3. Un repo de experimentación donde cambias el pipeline cada día
  4. 3 repos en la misma org, mismo flujo, equipo de 8 developers
Ver solución
  1. Inline. Un solo repo, pipeline único — no hay beneficio de centralizar.
  2. Reusable workflow. 5 repos con el mismo flujo — la mantenibilidad justifica el setup.
  3. Inline. Iteración rápida — el overhead de actualizar un repo compartido no vale cuando cambias el pipeline diariamente.
  4. Reusable workflow. 3 repos, equipo grande — la consistencia es crítica. Un cambio en el workflow se propaga a los 3 repos automáticamente.

Resumen

  • Reusable workflows convierten un workflow en una función que otros workflows pueden llamar con workflow_call
  • Inputs parametrizan el workflow (string, boolean, number), secrets se pasan explícitamente o con inherit
  • Outputs permiten que el reusable workflow devuelva datos al caller
  • Versioning con tags (@v1) es el sweet spot entre estabilidad y actualizaciones automáticas
  • Cross-repo funciona en repos públicos y privados de la misma org (requiere configuración)
  • Usa reusable workflows para 3+ repos con el mismo flujo; usa inline para repos únicos o iteración rápida
  • Reusable workflows vs composite actions: workflows para flujos completos, composite actions para steps individuales
  • DRY en CI/CD no es estética — es operabilidad: un cambio, propagación automática

Recursos adicionales

  1. GitHub Actions — Reusing Workflows - Documentación oficial completa
  2. GitHub Actions — workflow_call - Referencia del trigger
  3. GitHub Actions — Workflow Inputs - Syntax de inputs
  4. GitHub Actions — Sharing Workflows - Compartir dentro de una org
  5. Reusable Workflows Best Practices - Blog oficial de GitHub
  6. GitHub Actions Starter Workflows - Ejemplos oficiales de workflows