Reusable GitHub Actions Workflows
You have fourteen repositories. Each has a 120-line CI YAML that's 80% identical — checkout, setup Node, install, test, build Docker, push to ECR. A security fix to the Node setup requires editing fourteen files and hoping nobody copy-pasted with slight variations. Reusable GitHub Actions workflows let you define the pipeline once and call it from every repo with inputs for what's different.
Reusable workflow definition
Central repo: org/github-actions-templates/.github/workflows/node-ci.yml
name: Node CI Pipeline
on:
workflow_call:
inputs:
node-version:
type: string
required: false
default: '20'
run-e2e:
type: boolean
required: false
default: false
docker-image:
type: string
required: true
secrets:
NPM_TOKEN:
required: true
AWS_ROLE_ARN:
required: true
outputs:
image-tag:
description: "Built Docker image tag"
value: ${{ jobs.build.outputs.image-tag }}
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- run: npm test
- if: inputs.run-e2e
run: npm run test:e2e
build:
needs: test
runs-on: ubuntu-latest
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
permissions:
id-token: write
contents: read
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
aws-region: us-east-1
- uses: docker/build-push-action@v5
id: meta
with:
push: true
tags: ${{ inputs.docker-image }}:${{ github.sha }}
Calling from a service repo
# my-service/.github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
ci:
uses: org/github-actions-templates/.github/workflows/node-ci.yml@main
with:
node-version: '20'
run-e2e: true
docker-image: 123456789.dkr.ecr.us-east-1.amazonaws.com/my-service
secrets:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
AWS_ROLE_ARN: ${{ secrets.AWS_ROLE_ARN }}
Fourteen lines in the service repo. The template repo owns the pipeline logic.
Pinning workflow versions
# Pin to tag for stability
uses: org/github-actions-templates/.github/workflows/[email protected]
# Pin to SHA for maximum security
uses: org/github-actions-templates/.github/workflows/node-ci.yml@a1b2c3d4e5f6
Don't pin to @main in production service repos — a template change breaks all pipelines simultaneously. Pin to tags or SHAs; update deliberately.
Composing multiple reusable workflows
jobs:
ci:
uses: org/templates/.github/workflows/node-ci.yml@v2
with:
docker-image: registry/my-service
secrets: inherit # pass all secrets (GitHub 2023+)
deploy-staging:
needs: ci
if: github.ref == 'refs/heads/main'
uses: org/templates/.github/workflows/deploy-k8s.yml@v2
with:
environment: staging
image-tag: ${{ needs.ci.outputs.image-tag }}
secrets:
KUBE_CONFIG: ${{ secrets.STAGING_KUBE_CONFIG }}
Job outputs from reusable workflows flow to downstream jobs via needs.
secrets: inherit
jobs:
ci:
uses: org/templates/.github/workflows/node-ci.yml@v2
secrets: inherit
Passes all caller secrets to the reusable workflow. Convenient but less explicit — the reusable workflow can access secrets not listed in its secrets declaration. Use for internal templates; explicit mapping for third-party reusable workflows.
Composite action for step reuse
When you need reusable steps within a job, not a full workflow:
# org/actions/setup-node-aws/action.yml
name: Setup Node with AWS
inputs:
node-version:
default: '20'
runs:
using: composite
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ env.AWS_ROLE_ARN }}
aws-region: us-east-1
# In any workflow step
- uses: org/actions/setup-node-aws@v1
with:
node-version: '20'
Composite actions run in the same job — no separate runner.
Organization strategy
org/
├── github-actions-templates/ # Reusable workflows
│ └── .github/workflows/
│ ├── node-ci.yml
│ ├── deploy-k8s.yml
│ └── security-scan.yml
├── actions/ # Composite actions
│ ├── setup-node-aws/
│ └── docker-build-push/
└── my-service/ # Thin caller workflow
└── .github/workflows/ci.yml # 10-20 lines
Template repo changes go through PR review with test runs from a canary service repo before tagging a release.
Limitations
- Reusable workflows can't call other reusable workflows more than 4 levels deep
- Maximum 20 reusable workflows per caller workflow
workflow_calldoesn't support allon:triggers — caller defines when to run- Matrix strategies work inside reusable workflows but add complexity
Pin reusable workflow refs to SHA or semver tag — @main reusable workflows break all consumers on every push.
Reusable workflow versioning
uses: org/.github/workflows/[email protected] # pin tag, not @main
Breaking change in reusable workflow without version bump breaks all consumers. Semver tags + CHANGELOG.
Common production mistakes
Teams get github actions reusable workflows wrong in predictable ways:
- Skipping failure-mode rehearsal — run a game day or fault injection exercise before peak traffic, not after the first outage.
- Missing correlation context — every error path should carry request, trace, or tenant identifiers so incidents are debuggable.
- Optimizing for demo, not steady state — load tests, cache warm-up, and cold-start paths matter more than local dev latency.
- Undocumented trade-offs — if you chose speed over strict correctness (or vice versa), write that down for the next engineer.
CI/CD for github actions reusable workflows breaks merges when pipeline secrets rotate without updating OIDC trust, cache keys ignore lockfile changes, and deployment gates check build success but not smoke tests.
Debugging and triage workflow
When github actions reusable workflows misbehaves in production, work top-down instead of guessing:
- Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
- Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
- Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
- Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
- Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
- Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.
Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.
Resources
- GitHub Actions reusable workflows documentation
- Workflow syntax — workflow_call
- Composite actions documentation
- secrets: inherit
- Security hardening for GitHub Actions
Frequently asked questions
What are reusable workflows in GitHub Actions?
Reusable workflows are YAML files with workflow_call trigger that other workflows invoke like functions. Define build-and-test once in a central repo; every service repo calls it with repo-specific inputs. Changes to the reusable workflow propagate to all callers on their next run.
How do inputs and secrets work in reusable workflows?
The reusable workflow declares inputs (typed, with defaults) and required secrets in the workflow_call section. The caller passes values via with: and secrets:. Secrets must be explicitly mapped — they are not inherited automatically. Organization-level secrets can be referenced by name.
Reusable workflows vs composite actions — which to use?
Reusable workflows are full workflow graphs with multiple jobs, runners, and job dependencies — use for entire pipelines (build, test, deploy). Composite actions bundle steps into one job — use for reusable step sequences (setup Node, configure AWS). Workflows compose jobs; actions compose steps.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →