Building Reusable Workflows
What are Reusable Workflows?
Reusable workflows are GitHub Actions workflows that can be called from other workflows, similar to functions in programming. They eliminate duplication by centralizing common CI/CD logic in a single place, making workflows easier to maintain and standardize across repositories.
Instead of copying the same workflow configuration across multiple repositories, define it once and reference it wherever needed.
Why Use Reusable Workflows?
- Eliminate Duplication - Define common workflows once, use them everywhere
- Centralized Maintenance - Update workflow logic in one place
- Standardization - Ensure consistent CI/CD practices across projects
- Versioning - Pin workflows to specific versions for stability
- Abstraction - Hide complexity behind simple interfaces
Reusable Workflow Structure
graph TD
A["Caller Workflow
Repository A"] -->|calls| B["Reusable Workflow
Central Repository"]
C["Caller Workflow
Repository B"] -->|calls| B
D["Caller Workflow
Repository C"] -->|calls| B
B -->E["Job 1: Build"]
B -->F["Job 2: Test"]
B -->G["Job 3: Deploy"]
style A fill:#e1f5ff
style C fill:#e1f5ff
style D fill:#e1f5ff
style B fill:#fff3e0
Creating a Reusable Workflow
Basic Reusable Workflow
# .github/workflows/reusable-build.yml
name: Reusable Build Workflow
on:
workflow_call:
inputs:
node-version:
description: 'Node.js version to use'
required: false
default: '18'
type: string
working-directory:
description: 'Working directory'
required: false
default: '.'
type: string
outputs:
build-status:
description: 'Build completion status'
value: ${{ jobs.build.outputs.status }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
status: ${{ steps.build-step.outputs.status }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
working-directory: ${{ inputs.working-directory }}
run: npm ci
- name: Build application
id: build-step
working-directory: ${{ inputs.working-directory }}
run: |
npm run build
echo "status=success" >> $GITHUB_OUTPUT
Key components:
workflow_call:enables the workflow to be called from other workflowsinputs:defines parameters the caller can passoutputs:defines values the workflow returns to the caller
Calling a Reusable Workflow
Simple Call
# .github/workflows/main.yml
name: Main Workflow
on:
push:
branches:
- main
jobs:
call-build:
uses: organization/repo/.github/workflows/reusable-build.yml@main
with:
node-version: '20'
working-directory: './app'
Calling from Same Repository
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
node-version: '18'
Passing Secrets to Reusable Workflows
Secrets must be explicitly passed to reusable workflows.
Reusable Workflow with Secrets
# .github/workflows/reusable-deploy.yml
name: Reusable Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
secrets:
DEPLOY_TOKEN:
required: true
API_KEY:
required: false
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- name: Deploy application
run: |
echo "Deploying to ${{ inputs.environment }}"
# Use secrets in deployment
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
API_KEY: ${{ secrets.API_KEY }}
Calling Workflow with Secrets
jobs:
deploy-prod:
uses: org/repo/.github/workflows/reusable-deploy.yml@v1
with:
environment: production
secrets:
DEPLOY_TOKEN: ${{ secrets.PROD_DEPLOY_TOKEN }}
API_KEY: ${{ secrets.PROD_API_KEY }}
Using inherit for All Secrets
jobs:
deploy:
uses: org/repo/.github/workflows/reusable-deploy.yml@v1
with:
environment: production
secrets: inherit
Workflow Call Sequence
sequenceDiagram
participant Caller as Caller Workflow
participant Reusable as Reusable Workflow
participant Runner as GitHub Runner
Caller->>Reusable: Call with inputs
Reusable->>Runner: Execute jobs
Runner->>Runner: Run steps
Runner->>Reusable: Return outputs
Reusable->>Caller: Pass outputs back
Caller->>Caller: Continue execution
Real-World Example: Build, Test, and Deploy Pipeline
Reusable Build Workflow
# .github/workflows/reusable-node-ci.yml
name: Node.js CI
on:
workflow_call:
inputs:
node-version:
type: string
default: '18'
run-tests:
type: boolean
default: true
run-lint:
type: boolean
default: true
outputs:
artifact-name:
value: ${{ jobs.build.outputs.artifact }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
artifact: build-${{ github.sha }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
- name: Run linter
if: ${{ inputs.run-lint }}
run: npm run lint
- name: Run tests
if: ${{ inputs.run-tests }}
run: npm test
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: build-${{ github.sha }}
path: dist/
Reusable Deploy Workflow
# .github/workflows/reusable-deploy.yml
name: Deploy
on:
workflow_call:
inputs:
environment:
required: true
type: string
artifact-name:
required: true
type: string
secrets:
DEPLOY_KEY:
required: true
jobs:
deploy:
runs-on: ubuntu-latest
environment: ${{ inputs.environment }}
steps:
- uses: actions/download-artifact@v4
with:
name: ${{ inputs.artifact-name }}
- name: Deploy to server
run: |
echo "Deploying to ${{ inputs.environment }}"
# Deployment logic here
env:
DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
Main Workflow Calling Both
# .github/workflows/main.yml
name: Main Pipeline
on:
push:
branches:
- main
jobs:
build:
uses: ./.github/workflows/reusable-node-ci.yml
with:
node-version: '20'
run-tests: true
run-lint: true
deploy-staging:
needs: build
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: staging
artifact-name: ${{ needs.build.outputs.artifact-name }}
secrets:
DEPLOY_KEY: ${{ secrets.STAGING_DEPLOY_KEY }}
deploy-production:
needs: [build, deploy-staging]
uses: ./.github/workflows/reusable-deploy.yml
with:
environment: production
artifact-name: ${{ needs.build.outputs.artifact-name }}
secrets:
DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }}
Versioning Reusable Workflows
Pin workflows to specific versions for stability.
jobs:
# Pin to specific commit SHA
build:
uses: org/repo/.github/workflows/build.yml@a1b2c3d4
# Pin to tag
test:
uses: org/repo/.github/workflows/test.yml@v1.2.3
# Pin to branch (less stable)
deploy:
uses: org/repo/.github/workflows/deploy.yml@main
Best practices:
- Use tags for production workflows
- Use commit SHAs for maximum stability
- Use branches only for development/testing
Workflow Templates vs Reusable Workflows
graph TD
A["Workflow Templates"] -->|Starter files| B["Copy template
to new repo"]
C["Reusable Workflows"] -->|Reference| D["Call workflow
from central repo"]
B -->E["Independent copy
maintained separately"]
D -->F["Shared workflow
centrally maintained"]
style A fill:#ffcccc
style C fill:#ccffcc
Workflow Templates:
- One-time starter files
- Each repository maintains its own copy
- No automatic updates
Reusable Workflows:
- Called from a central location
- Updates apply to all callers
- Single source of truth
Best Practices
Organization-Level Workflows
Store reusable workflows in a central repository:
org/workflows-repo/
.github/workflows/
reusable-ci.yml
reusable-deploy.yml
reusable-security.yml
Reference them across all organization repositories.
Input Validation
on:
workflow_call:
inputs:
environment:
type: string
required: true
deploy-region:
type: choice
options:
- us-east-1
- us-west-2
- eu-west-1
Documentation
Document inputs, outputs, and usage in workflow files:
# Reusable Node.js CI Workflow
#
# Usage:
# uses: org/repo/.github/workflows/node-ci.yml@v1
# with:
# node-version: '18'
#
# Inputs:
# - node-version: Node.js version (default: '18')
# - run-tests: Run test suite (default: true)
#
# Outputs:
# - artifact-name: Name of uploaded build artifact
Common Patterns
Matrix Build Reusable Workflow
on:
workflow_call:
inputs:
node-versions:
type: string
default: '["16", "18", "20"]'
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ${{ fromJSON(inputs.node-versions) }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
- run: npm test
Conditional Deployment
on:
workflow_call:
inputs:
deploy-enabled:
type: boolean
default: false
jobs:
deploy:
if: ${{ inputs.deploy-enabled }}
runs-on: ubuntu-latest
steps:
- run: echo "Deploying..."
Limitations and Considerations
- Reusable workflows can call other reusable workflows (up to 4 levels deep)
- Cannot use workflow_dispatch trigger in reusable workflows
- Secrets must be explicitly passed (unless using inherit)
- Outputs are limited to strings
- Called workflows execute in the caller's repository context
Key Takeaways
- Reusable workflows eliminate duplication by centralizing common CI/CD logic
- Define workflows with
workflow_calltrigger and expose inputs and outputs - Call workflows using
uses:with repository path and version reference - Secrets must be explicitly passed or inherited from the caller
- Version reusable workflows with tags or commit SHAs for stability
- Store organization-wide workflows in a central repository
- Reusable workflows provide better maintainability than workflow templates
- Chain multiple reusable workflows together for complex pipelines
- Document inputs, outputs, and usage clearly for consumers
Next Steps: Identify duplicated workflow patterns across repositories and refactor them into reusable workflows for centralized maintenance.