GitHub Actions Reusable Workflows Tutorial
Stop copy-pasting CI YAML across repos. Learn how to build reusable GitHub Actions workflows with inputs, secrets, outputs, and per-environment overrides.
What you'll learn
- ✓The difference between composite actions and reusable workflows
- ✓How to define and call reusable workflows
- ✓Passing inputs, secrets, and outputs
- ✓Versioning and pinning patterns
- ✓A real multi-repo CI structure
Prerequisites
- •Basic experience writing GitHub Actions YAML
What and Why
Most teams accumulate near-identical CI YAML in every repo: same Node setup, same lint, same test, same Docker build. When you need to change one step (say, swap actions/checkout v3 for v4), you fan out PRs to 20 repos. Reusable workflows solve that. One workflow lives in a central repo, and other repos call it.
Mental Model
GitHub Actions has two reuse mechanisms:
- Composite actions: small reusable steps. They look like an
action.ymlwithruns.using: composite. Called like any other action viauses:. - Reusable workflows: whole workflows you call from another workflow with
uses:. They include aworkflow_calltrigger and acceptinputs,secrets, and produceoutputs.
Composite actions are for sequences of steps; reusable workflows are for entire jobs.
repo-a/.github/workflows/ci.yml
|
| jobs:
| build:
| uses: org/ci-workflows/.github/workflows/node-ci.yml@v1
| with: { node-version: '20' }
| secrets: inherit
v
org/ci-workflows/.github/workflows/node-ci.yml
on: workflow_call
jobs: lint, test, build Hands-on Example
A reusable workflow in org/ci-workflows:
# .github/workflows/node-ci.yml
name: Node CI
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
run-coverage:
type: boolean
default: false
secrets:
NPM_TOKEN:
required: false
outputs:
image-tag:
description: Built image tag
value: ${{ jobs.build.outputs.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
- run: npm run lint
- run: npm test
- if: inputs.run-coverage
run: npm run coverage
build:
needs: test
runs-on: ubuntu-latest
outputs:
tag: ${{ steps.meta.outputs.tag }}
steps:
- uses: actions/checkout@v4
- id: meta
run: echo "tag=ghcr.io/${{ github.repository }}:${{ github.sha }}" >> $GITHUB_OUTPUT
A consumer in another repo:
# .github/workflows/ci.yml
name: CI
on:
pull_request:
push: { branches: [main] }
jobs:
ci:
uses: org/ci-workflows/.github/workflows/node-ci.yml@v1
with:
node-version: '20'
run-coverage: true
secrets: inherit
deploy:
needs: ci
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- run: echo "Deploying ${{ needs.ci.outputs.image-tag }}"
secrets: inherit forwards all org-level and repo-level secrets without listing them. Use it when you trust the callee; otherwise pass only what is needed.
Common Pitfalls
Pinning to a moving tag. @main means every workflow run uses whatever is currently on the callee’s main. Pin to a specific tag (@v1.2.0) or a SHA for production-critical workflows.
Forgetting permissions. Each job in a reusable workflow inherits the caller’s permissions: block by default. If the caller restricts permissions, callee actions may fail. Set explicit permissions in the reusable workflow:
permissions:
contents: read
packages: write
Secrets that do not exist. A required secret declared in workflow_call.secrets must be passed by the caller, or the workflow fails to start.
Cross-repo private callees. Calling a private reusable workflow from another private repo requires the callee repo to allow it under “Actions settings > Access”.
Nested matrix madness. A reusable workflow run as part of a matrix job multiplies cost quickly. Confirm runtime expectations before flipping the switch.
Practical Tips
Keep a single repo (often ci-workflows or actions-platform) for your reusable workflows and composite actions. Version it with semantic tags:
git tag v1.2.0
git push --tags
Maintain a sliding v1 tag that points to the latest non-breaking 1.x release. Most callers track v1; cautious ones pin to v1.2.0.
Combine reusable workflows with environments for staged deploys:
deploy:
uses: org/ci-workflows/.github/workflows/deploy.yml@v1
with: { env: production }
secrets: inherit
The callee references environment: ${{ inputs.env }} so deploy approvals, environment secrets, and protection rules apply.
Combine with OIDC for cloud auth. No long-lived AWS keys; the workflow exchanges a GitHub OIDC token for short-lived STS credentials.
Use composite actions for things like “setup repo”: checkout, language runtime, dependency cache. Compose multiple composite actions inside a reusable workflow for readable, layered YAML.
Add a workflow_dispatch trigger to your reusable workflow during development so you can run it standalone and test changes without modifying every caller.
Wrap-up
Reusable workflows turn duplicated CI YAML into a shared library you maintain in one place. Define them with workflow_call, accept inputs and secrets, expose outputs to chain into later jobs, and pin callers to immutable tags. Use composite actions for step-level reuse and reusable workflows for job-level reuse. Combine with OIDC and environments for safe deploys. Once the pattern is in place, updating CI for 50 repos becomes a single PR — which is exactly the leverage CI is supposed to give you.
Related articles
- DevOps CI/CD Pipeline Design Fundamentals
How to design a CI/CD pipeline that stays fast, reliable, and reversible: stages, caching, parallelism, environments, and rollback strategies that scale with the team.
- CI/CD CI/CD Secrets Management Best Practices
Keep API keys, tokens, and database credentials safe in CI/CD with rotation, scoping, secret managers, and OIDC-based authentication.
- CI/CD CI/CD Self-Hosted Runners Tutorial
A practical guide to running your own CI/CD runners. Learn when self-hosting beats cloud runners, how to set them up safely, and how to keep them healthy in production.
- DevOps Git Branching Strategies: Trunk vs Gitflow vs GitHub Flow
Compare trunk-based development, gitflow, and GitHub flow. Learn when each strategy fits, how they affect release cadence, and which commands to actually use day to day.