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
| Tipo | Ejemplo | Uso |
|---|---|---|
string | "3.12" | Versiones, nombres, paths |
boolean | true | Feature flags |
number | 15 | Timeouts, 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:
| Aspecto | Secrets explícitos | secrets: inherit |
|---|---|---|
| Seguridad | Solo pasa lo necesario | Pasa todo |
| Claridad | Documentado qué secrets se usan | Opaco |
| Mantenimiento | Actualizar cuando se agrega un secret | Automático |
| Recomendado | Para workflows cross-org | Para 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
| Estrategia | Cuándo usar |
|---|---|
@main | Desarrollo activo, confianza total en main |
@v1 (major) | Producción — solo breaking changes actualizan major |
@v1.2.0 (exact) | Máximo control, actualizaciones manuales |
@sha | Seguridad 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:
- Hacer el repo público
- Duplicar el workflow en cada org
- Usar GitHub Apps con installation tokens (fuera del scope de esta guía)
Comparaciones
Reusable workflows vs Composite actions
| Aspecto | Reusable Workflow | Composite Action |
|---|---|---|
| Scope | Workflow completo (múltiples jobs) | Steps dentro de un job |
| Trigger | workflow_call | uses: en un step |
| Jobs propios | Sí (puede definir múltiples jobs) | No (corre dentro del job del caller) |
| Runners | Puede elegir su propio runner | Usa el runner del caller |
| Cuándo usar | Flujo completo compartido | Steps comunes compartidos |
Reusable workflows vs Templates
| Aspecto | Reusable Workflow | Template (copiar YAML) |
|---|---|---|
| Actualización | Automática (cambias una vez) | Manual (cambias en cada repo) |
| Divergencia | Imposible | Inevitable |
| Flexibilidad | Inputs parametrizan | Modificas libremente |
| Setup | Repo compartido + referencia | Copiar 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:
- Un solo repo con un pipeline único de machine learning training
- 5 microservicios con el mismo flujo de lint → test → deploy
- Un repo de experimentación donde cambias el pipeline cada día
- 3 repos en la misma org, mismo flujo, equipo de 8 developers
Ver solución
- Inline. Un solo repo, pipeline único — no hay beneficio de centralizar.
- Reusable workflow. 5 repos con el mismo flujo — la mantenibilidad justifica el setup.
- Inline. Iteración rápida — el overhead de actualizar un repo compartido no vale cuando cambias el pipeline diariamente.
- 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
- GitHub Actions — Reusing Workflows - Documentación oficial completa
- GitHub Actions — workflow_call - Referencia del trigger
- GitHub Actions — Workflow Inputs - Syntax de inputs
- GitHub Actions — Sharing Workflows - Compartir dentro de una org
- Reusable Workflows Best Practices - Blog oficial de GitHub
- GitHub Actions Starter Workflows - Ejemplos oficiales de workflows