GitHub Actions: CI/CD Workflows From First Run to Secure Deployments · Pipelines, Deployments and Security
Reusable Workflows and Composite Actions
Remove duplication with workflow_call reusable workflows and your own composite actions.
What you'll build
A reusable test workflow with an input, called twice with different Node.js versions, plus a composite action that bundles setup steps.
The idea
Copy-pasted YAML drifts out of date. GitHub offers two ways to share:
- A reusable workflow is a workflow with the
workflow_calltrigger. It declares typedinputs,secretsandoutputs, and callers use it as a job withuses: ./.github/workflows/test.yml(orowner/repo/.github/workflows/test.yml@ref). A caller job has nostepsorruns-on. - A composite action is an
action.ymlwhoseruns.usingiscomposite; it bundles steps that run inside the caller's job. Eachrunstep needs ashell:.
Choose a reusable workflow to share whole jobs (their own runners, secrets and environments) and a composite action to share a few steps.
The code
The reusable workflow:
name: Reusable test
on:
workflow_call:
inputs:
node-version:
type: string
default: "22"
outputs:
result:
description: Test result
value: ${{ jobs.test.outputs.result }}
jobs:
test:
runs-on: ubuntu-latest
outputs:
result: ${{ steps.done.outputs.result }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: npm
- run: npm ci && npm test
- id: done
run: echo "result=passed" >> "$GITHUB_OUTPUT"
Output
actionlint: no problems found workflow: Reusable test triggers: workflow_call jobs, in the order they run: test: runs on ubuntu-latest | 4 step(s)
Run this on your computer — see “Get Started” for setup.
The caller workflow, which runs it for two Node.js versions:
name: CI
on: [push, pull_request]
jobs:
node-22:
uses: ./.github/workflows/test.yml
with:
node-version: "22"
node-24:
uses: ./.github/workflows/test.yml
with:
node-version: "24"
summary:
needs: [node-22, node-24]
runs-on: ubuntu-latest
steps:
- run: echo "Results ${{ needs.node-22.outputs.result }} and ${{ needs.node-24.outputs.result }}"
Output
actionlint: no problems found workflow: CI triggers: push, pull_request jobs, in the order they run: node-22: runs on (reusable workflow) | calls ./.github/workflows/test.yml node-24: runs on (reusable workflow) | calls ./.github/workflows/test.yml summary: runs on ubuntu-latest | 1 step(s) | after node-22, node-24
Run this on your computer — see “Get Started” for setup.
Common mistakes
- Adding
stepsto the caller job. A job that uses a reusable workflow can't have steps. - Secrets not passed. A reusable workflow gets secrets only if you pass them with
secrets:orsecrets: inherit. - Forgetting
shell:in a composite action. Everyrunstep there needs one. - Unpinned references to other repositories. Pin
@v1or a SHA so shared workflows can't change under you.
Key points
workflow_callmakes a workflow reusable; callers use it as a job.- Composite actions bundle steps; reusable workflows bundle jobs.
- Pass inputs with
with:and secrets withsecrets:. Further reading: GitHub Docs.
Practice questions
When would you prefer a composite action to a reusable workflow?
Answer
When you want to share a handful of steps that run inside the caller's existing job, runner and workspace. Reusable workflows are better for whole jobs with their own runners, secrets and environments.
Why can't the caller job in the example have a runs-on key?
Answer
A job that calls a reusable workflow delegates execution to it; the called workflow's jobs define their own runners, so the caller can only set uses, with, secrets and needs.
Open in the app to mark lessons complete, take notes and track your progress.