DevOps

🔄 GitHub Actions: Reusable Workflows

GitHub Actions Reusable Workflows allow you to avoid duplication of YAML code in projects with multiple repositories or microservices. With this functionality, you can define a pipeline template (for example, Docker build, Kubernetes deployment, or code quality analysis) in one place and call it from dozens of different repositories.

1. What is a Reusable Workflow and why use it?

In medium to large organizations, maintaining identical copies of pipelines in 50 different repositories leads to the maintenance nightmare known as DRY violation (Don't Repeat Yourself). If you need to update a version of Node.js or fix a security parameter in Helm, you would need to send 50 Pull Requests.

Key Concept: A Called Workflow exposes the on: workflow_call trigger. A Caller Workflow calls that pipeline within one of its jobs using the uses: directive.

2. Reusable Workflows vs. Custom Actions

It is common to confuse Reusable Workflows with Composite/Custom Actions. Although both promote code reuse, they resolve different levels of abstraction:

FeatureReusable WorkflowsComposite / Custom Actions
Hierarchy LevelComplete Job Level. Can include multiple jobs in parallel/sequence.Individual Step level within an existing job.
Runners / EnvironmentIt manages its own runners (runs-on), permissions (permissions) and matrix.It is executed within the runner already assigned by the job that invokes it.
Matrix and ParallelismSupports complex arrays and multiple coordinated jobs.It does not handle independent employment or direct execution matrices.
Secrets and EnvironmentsSupports native integration with environment and prior approval.Access the variables and secrets transmitted to the step.

3. Reusable Workflow Syntax (Called Workflow)

The file is usually saved within .github/workflows/ (for example, _build_and_deploy.yml). The fundamental key lies in defining the activation event on: workflow_call.

yaml
name: Standard Build & Deploy Pipeline

on:
  workflow_call:
    # 1. Definición de Parámetros de Entrada
    inputs:
      environment:
        description: 'Entorno de despliegue (staging, production)'
        required: true
        type: string
      node_version:
        description: 'Versión de Node.js'
        required: false
        default: '20'
        type: string
      run_tests:
        description: 'Si se deben ejecutar pruebas unitarias'
        required: false
        default: true
        type: boolean

    # 2. Definición de Secretos Requeridos
    secrets:
      AWS_ROLE_ARN:
        description: 'ARN del rol de AWS IAM mediante OIDC'
        required: true
      SLACK_WEBHOOK:
        description: 'URL de webhook para notificaciones'
        required: false

    # 3. Salidas generadas para el caller
    outputs:
      image_tag:
        description: 'Tag generado de la imagen Docker'
        value: ${{ jobs.build.outputs.docker_tag }}

jobs:
  build:
    name: Build & Test
    runs-on: ubuntu-latest
    outputs:
      docker_tag: ${{ steps.prep.outputs.tag }}
    steps:
      - name: Checkout del repositorio caller
        uses: actions/checkout@v4

      - name: Configurar Node.js
        uses: actions/setup-node@v4
        with:
          node-version: ${{ inputs.node_version }}

      - name: Instalar Dependencias
        run: npm ci

      - name: Ejecutar Tests
        if: ${{ inputs.run_tests }}
        run: npm test

      - name: Generar Docker Tag
        id: prep
        run: echo "tag=${{ inputs.environment }}-${{ github.sha }}" >> $GITHUB_OUTPUT

  deploy:
    name: Deploy a K8s
    needs: build
    runs-on: ubuntu-latest
    environment: ${{ inputs.environment }}
    steps:
      - name: Notificar inicio
        run: echo "Desplegando imagen ${{ needs.build.outputs.docker_tag }} en ${{ inputs.environment }}"

4. Invoking the Workflow (Caller Workflow)

The repository that wants to consume this standardized pipeline creates a normal workflow (triggered by push, pull_request, etc.) and calls the reusable workflow by defining uses: at the job level.

yaml
name: Production Release

on:
  push:
    branches:
      - main

jobs:
  # Llama al Reusable Workflow dentro del mismo repositorio (vía ruta relativa)
  call-pipeline:
    uses: ./.github/workflows/_build_and_deploy.yml
    with:
      environment: production
      node_version: '20'
      run_tests: true
    secrets:
      AWS_ROLE_ARN: ${{ secrets.PROD_AWS_ROLE_ARN }}
      SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK_URL }}

  # Job posterior que utiliza las salidas (outputs) del reusable workflow
  notify:
    needs: call-pipeline
    runs-on: ubuntu-latest
    steps:
      - name: Ver Tag Desplegado
        run: |
          echo "Despliegue finalizado exitosamente."
          echo "Tag publicado: ${{ needs.call-pipeline.outputs.image_tag }}"

5. Advanced Secret Management: secrets: inherit

Manually passing 10 or 15 secrets through the secrets: clause can be tedious. GitHub Actions provides the secrets: inherit directive for the reusable workflow to automatically inherit all repository and organization secrets.

yaml
  call-central-pipeline:
    # El llamado hereda implícitamente todos los secretos accesibles en el caller
    uses: mi-organizacion/ci-templates/.github/workflows/deploy.yml@v2
    with:
      environment: staging
    secrets: inherit

6. Centralization between Multiple Repositories (Cross-Repository)

The biggest advantage of Reusable Workflows is unlocked by hosting your templates in a centralized repository within your organization (e.g. mi-org/devops-templates).

Reference by Git Reference (Branch, Tag or Commit SHA):

  • By Semantic Tag (Recommended for production): uses: mi-org/devops-templates/.github/workflows/docker-build.yml@v1.2.0
  • By Branch (Development/Testing): uses: mi-org/devops-templates/.github/workflows/docker-build.yml@main
  • By Commit SHA (Maximum security and immutability): uses: mi-org/devops-templates/.github/workflows/docker-build.yml@a1b2c3d4e5f6...

Setting Permissions in GitHub Org: So that other private repositories in your organization can invoke the reusable workflow hosted in a central private repo, you must go to:

Repository Settings → Actions → General → Access and select "Accessible from repositories in the 'NAME' organization".

7. Good Practices and Golden Rules

  1. Prefix with underscore: Name your internal reusable workflows starting with an underscore (ex: _terraform-pipeline.yml) to visually differentiate them from standard input workflows.
  1. Nesting Limit: You can nest Reusable Workflows (one reusable workflow calling another), but there is a maximum limit of 4 levels deep.
  1. Versioning Strategy: Use semantic releases (tags v1, v1.1.0) in your centralized pipeline repository to not break consuming projects when you make backwards incompatible changes.
  1. Avoid hidden dependencies: Explicitly declare all necessary inputs and secrets in on: workflow_call to have self-contained documentation.