You have 5 microservices with nearly identical pipelines. Reusable workflows let you define the pipeline ONCE and call it from each service — the architect's tool for scaling CI/CD across an organization.
The Problem: Copy-Paste Pipelines
How It Works: workflow_call
🏋️ Create a Reusable CI Workflow
.github/workflows/reusable-ci.yml:
name: Reusable CI
on:
workflow_call:
# Parameters the caller provides
inputs:
node-version:
type: string
default: '20'
working-directory:
type: string
default: '.'
run-lint:
type: boolean
default: true
outputs:
version:
description: 'Package version'
value: ${{ jobs.build.outputs.version }}
jobs:
lint:
if: inputs.run-lint
runs-on: ubuntu-latest
defaults:
run: { working-directory: ${{ inputs.working-directory }} }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ inputs.node-version }}, cache: 'npm' }
- run: npm ci
- run: npm run lint
test:
runs-on: ubuntu-latest
defaults:
run: { working-directory: ${{ inputs.working-directory }} }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ inputs.node-version }}, cache: 'npm' }
- run: npm ci
- run: npm test
build:
needs: [lint, test]
if: always() && !contains(needs.*.result, 'failure')
runs-on: ubuntu-latest
outputs:
version: ${{ steps.ver.outputs.value }}
defaults:
run: { working-directory: ${{ inputs.working-directory }} }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: ${{ inputs.node-version }}, cache: 'npm' }
- run: npm ci && npm run build
- id: ver
run: echo "value=$(node -p 'require(\"./package.json\").version')" >> $GITHUB_OUTPUT
Now call it from a service — .github/workflows/ci.yml:
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
ci:
uses: ./.github/workflows/reusable-ci.yml
with:
node-version: '20'
working-directory: '.'
That's it. 3 lines to get a full CI pipeline.
Passing Secrets to Reusable Workflows
# In the reusable workflow definition:
on:
workflow_call:
secrets:
AZURE_CLIENT_ID:
required: true
# In the caller:
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml
secrets:
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
# Or pass ALL secrets at once:
secrets: inherit
Architect Decision: When to Use What
| Mechanism | Scope | Use When |
|---|---|---|
| Reusable Workflow | Full pipeline (multiple jobs) | Standardize CI or CD across services |
| Composite Action | A sequence of steps | Repeated step pattern (setup, auth) |
| workflow_run | Chain workflows | "After CI succeeds, start CD" |
🧠 Recall Check
- What trigger does a reusable workflow use instead of
pushorpull_request? - How does the caller provide different configuration to the same reusable workflow?
- How do you pass secrets from a caller to a reusable workflow?
- What's the key difference between reusable workflows and composite actions?
Reveal answers
on: workflow_call— this declares "I can be called by other workflows."- Via
inputs:— the reusable workflow declares typed parameters, callers provide values withwith:. - Either explicitly (
secrets: AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}) or withsecrets: inheritto pass all. - Reusable workflows contain complete jobs (lint, test, build). Composite actions are a sequence of steps within a single job (checkout + setup + install).
Reusable workflows are how you scale CI/CD across an organization. Define best practices once, enforce them everywhere. When you update the template, all services get the improvement automatically. This is the platform engineering mindset.
Next lesson: Composite Actions — creating reusable step sequences (the other DRY mechanism).