Stop the YAML Sprawl: Centralizing GitHub Actions with Reusable Workflows and Composite Actions

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

The ‘Copy-Paste’ Tax on Engineering Velocity

Scaling from one repository to fifty isn’t just a matter of more code; it’s a test of your infrastructure’s resilience. A silent productivity killer usually hitches a ride: YAML duplication. You build a perfect CI/CD pipeline for Repo A. Then Repo B needs the same logic, so you copy the .github/workflows/main.yml file. By the time you reach Repo J, you are babysitting ten identical files.

Imagine needing to update a security scanner or bump Node.js from v18 to v20 across the entire organization. You would have to open twenty pull requests, wait for twenty builds, and pray you didn’t miss a single line in one of them. This manual toil is exactly what DevOps aims to kill.

In a previous role, I spent 34 hours of my first week manually updating a Docker registry URL across 42 separate repositories. It was soul-crushing work that could have been avoided with a single source of truth. To transition from a ‘YAML engineer’ to a true DevOps architect, you must master the art of abstraction.

GitHub Actions provides two primary tools for this: Composite Actions and Reusable Workflows. They might look similar, but they serve different roles in your quest for a DRY (Don’t Repeat Yourself) pipeline.

The Toolbox: Bricks vs. Blueprints

Picking the right tool early prevents architectural headaches later. You don’t want to build a house out of individual grains of sand when you could use pre-cast concrete slabs.

Composite Actions: Your Custom Lego Bricks

A Composite Action bundles multiple workflow steps into a single, callable action. Think of it as a private function that lives inside a job. Use it for repetitive shell commands that always happen together in the same environment.

  • Scope: Operates within a single job.
  • Structure: Defined in an action.yml file.
  • Key Requirement: Every run step must explicitly declare a shell (e.g., shell: bash).
  • Ideal for: Installing specific CLI tools, setting up cloud credentials, or running standard cleanup scripts.

Reusable Workflows: The Standardized Assembly Line

A Reusable Workflow is an entire YAML file that another workflow can trigger. Unlike actions, these are complete blueprints including jobs, runners, and logic. They act as the “Golden Path” for your entire CI/CD process.

  • Scope: Called at the job level.
  • Trigger: Activated by the on: workflow_call event.
  • Control: You can strictly define which inputs and secrets are required from the caller.
  • Ideal for: Standardizing the entire build-test-deploy lifecycle for 100+ microservices.

Building Your Source of Truth

Let’s get practical. We will create a central repository named devops-assets. This will house our shared logic, which we can then call from any project repository in the organization.

Step 1: Creating a Composite Action for Environment Setup

Inside devops-assets, create the path: .github/actions/setup-node-cache/action.yml. This handles Node.js installation and dependency caching—a repetitive task that usually takes 10-15 lines of YAML per repo.

# devops-assets/.github/actions/setup-node-cache/action.yml
name: 'Setup Node and Cache'
description: 'Installs Node.js and sets up npm caching'
inputs:
  node-version:
    description: 'Version of Node.js to use'
    required: true
    default: '20'

runs:
  using: "composite"
  steps:
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ inputs.node-version }}

    - name: Cache dependencies
      uses: actions/cache@v4
      with:
        path: ~/.npm
        key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

    - name: Install dependencies
      shell: bash
      run: npm ci

The shell: bash line is non-negotiable here. It ensures your scripts run predictably regardless of the runner’s default OS shell.

Step 2: Designing a Reusable Workflow for Testing

Next, we create a complete test suite blueprint in .github/workflows/standard-ci.yml. This workflow uses the composite action we just built.

# devops-assets/.github/workflows/standard-ci.yml
name: Standard CI

on:
  workflow_call:
    inputs:
      node-version:
        required: false
        type: string
        default: '20'
    secrets:
      SONAR_TOKEN:
        required: false

jobs:
  test-and-lint:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Use our composite action
        uses: your-org/devops-assets/.github/actions/setup-node-cache@v1
        with:
          node-version: ${{ inputs.node-version }}

      - name: Run Tests
        run: npm test

The workflow_call trigger is the gateway. It allows this file to be treated like a reusable library function by any other repository.

Step 3: The Lean Caller Workflow

Now, look at how clean your application repository becomes. In my-web-app/.github/workflows/ci.yml, you only need this:

# my-web-app/.github/workflows/ci.yml
name: CI

on:
  push:
    branches: [ main ]

jobs:
  call-standard-ci:
    uses: your-org/devops-assets/.github/workflows/standard-ci.yml@v1
    with:
      node-version: '22'
    secrets: inherit

You’ve just reduced a 100-line workflow to 10 lines. If you need to add a security scan to every project tomorrow, you update one file in devops-assets, and the change propagates everywhere instantly.

Survival Rules for Centralized Pipelines

Centralization simplifies management, but it also creates a single point of failure. If you break the shared workflow, you break the entire company’s deployment ability.

1. Pin Your Versions

Never use @main. If you push a breaking change to the main branch, every project will fail at once. Always use tags (e.g., @v1.2.0) or commit SHAs. Test changes in a branch, tag it, and then upgrade your projects incrementally.

2. The Secret Handshake

Secrets do not follow the caller automatically. You must explicitly pass them or use secrets: inherit. While inherit is convenient for internal tools, explicitly defining secrets in the workflow_call block is better for security audits and the principle of least privilege.

3. Know Your Boundaries

If you need to coordinate multiple jobs—like a build job that must finish before a deploy job starts—a Reusable Workflow is your only choice. If you just want to avoid typing npm install and aws configure for the thousandth time, stick to a Composite Action.

The DevOps Maturity Leap

Transitioning to a centralized architecture is a major milestone. It shifts your focus from “making it work” to “making it scale.” By abstracting infrastructure into Reusable Workflows, you empower developers to focus on features rather than YAML syntax. Start small: identify one repetitive setup task and turn it into a Composite Action today. Your future self—and your on-call rotation—will thank you for the lack of maintenance debt.

Share: