Composite Actions and Reusable Workflows¶
Overview¶
Copy-pasting fifty lines of setup into every repository does not scale. Composite actions bundle steps into a local or published action; reusable workflows expose entire jobs via workflow_call so service repos invoke a standard platform pipeline with inputs and secrets.
This is Tutorial 14 in Module 14: Reusable Components of the REBASH Academy GitHub Actions for Cloud & DevOps Engineers series — written for Cloud, DevOps, Platform, and SRE engineers building internal developer platforms.
Prerequisites¶
- Release Management and Versioning
- Workflow Syntax: Matrix and Reusable Workflows
- Python 3 with PyYAML for offline validation
Learning Objectives¶
By the end of this tutorial, you will be able to:
- Author a composite action with
action.ymlinputs and steps - Create a reusable workflow callable via
workflow_call - Wire a caller workflow that uses both components together
- Pass inputs, secrets, and outputs across reusable boundaries
- Choose composite action vs reusable workflow for a given problem
Architecture¶
Service repositories call reusable workflows; reusable workflows invoke composite actions for shared step bundles.
Theory¶
What it is¶
| Mechanism | Granularity | Defined in | Called via |
|---|---|---|---|
| Composite action | Steps (shell grouped) | action.yml in repo path | uses: ./.github/actions/name or org/repo/path@ref |
| Reusable workflow | Jobs/workflows | .github/workflows/*.yml with on: workflow_call | uses: org/repo/.github/workflows/x.yml@ref |
| JavaScript action | Custom Node logic | action.yml + dist/ | Same as composite |
| Marketplace action | Third-party | Published repo | uses: owner/action@v4 (pin SHA in prod) |
Composite actions cannot call other composite actions recursively in all cases — keep them focused. Reusable workflows support secrets: inherit and job outputs for platform teams.
Why it matters¶
Platform engineering centralises compliance (scanning, pinning, OIDC login) once. Service teams supply inputs (app name, Python version) without forking platform YAML. Updates roll out when callers pin to new @v2 tag or SHA.
How it works¶
- Platform repo publishes
/.github/actions/setup-python-app/action.yml(composite). - Platform repo publishes
/.github/workflows/ci-reusable.ymlwithworkflow_callinputs. - Service repo workflow:
jobs:
ci:
uses: my-org/platform/.github/workflows/ci-reusable.yml@v1
with:
python-version: '3.12'
secrets: inherit
- Reusable workflow checks out code and
uses: ./.github/actions/setup-python-appwith inputs. - Outputs from reusable jobs expose version or artefact names to caller via
jobs.<id>.outputs.
Key concepts and comparisons¶
| Use composite when | Use reusable workflow when |
|---|---|
| Bundling 3–10 shell steps | Entire CI job graph needed |
| Same repo or lightweight share | Cross-repo standard pipeline |
| Inputs are step parameters | Need job-level needs, environments |
| No separate runner job semantics | Caller should stay minimal |
Common pitfalls¶
- Putting
runs-oninside composite action (invalid — composite runs on caller's runner). - Reusable workflow without documenting required
secrets. - Callers pin
@main— silent breaking changes. - Circular
workflow_calldependencies between repos. - Composite action trying to set job outputs without
outputsinaction.yml.
Hands-on Lab¶
Objective¶
Build a composite action (setup + validate marker file) and a reusable workflow that calls it, then author a caller workflow in the same lab repo — all validated offline.
Prerequisites¶
- Python 3 with PyYAML
- Bash
Lab environment¶
Workspace: ~/rebash-github-actions/module-14
mkdir -p ~/rebash-github-actions/module-14/.github/{actions/setup-lab,workflows} && cd ~/rebash-github-actions/module-14
set -euo pipefail
Real-world scenario¶
Platform team ships setup-lab composite action and ci-reusable.yml reusable workflow. Application repos only maintain a ten-line caller workflow.
Step-by-step tasks¶
Task 1 – Composite action¶
Create .github/actions/setup-lab/action.yml:
name: Setup lab workspace
description: Create marker file and validate lab path
inputs:
lab-name:
description: Lab identifier
required: true
outputs:
marker-path:
description: Path to marker file
value: ${{ steps.mk.outputs.path }}
runs:
using: composite
steps:
- id: mk
shell: bash
run: |
set -euo pipefail
mkdir -p out
path="out/${{ inputs.lab-name }}.txt"
echo "lab=${{ inputs.lab-name }}" > "$path"
echo "path=$path" >> "$GITHUB_OUTPUT"
- shell: bash
run: test -s "${{ steps.mk.outputs.path }}"
Validate offline:
cd ~/rebash-github-actions/module-14
set -euo pipefail
python3 -c "import yaml; yaml.safe_load(open('.github/actions/setup-lab/action.yml')); print('composite action OK')"
grep -q 'using: composite' .github/actions/setup-lab/action.yml
Expected output
composite action OK
Note: The action.yml above uses GitHub expressions in the lab file on disk — in MkDocs the tutorial wraps those fences in raw Jinja blocks. For offline simulation, run the shell steps manually:
Expected output
Silent success (exit 0).
Task 2 – Reusable workflow¶
Create .github/workflows/ci-reusable.yml:
name: CI Reusable
on:
workflow_call:
inputs:
lab-name:
required: true
type: string
outputs:
marker-path:
description: Marker file from setup
value: ${{ jobs.build.outputs.marker-path }}
jobs:
build:
runs-on: ubuntu-latest
outputs:
marker-path: ${{ steps.setup.outputs.marker-path }}
steps:
- uses: actions/checkout@v4
- id: setup
uses: ./.github/actions/setup-lab
with:
lab-name: ${{ inputs.lab-name }}
- name: Prove marker
run: cat "${{ steps.setup.outputs.marker-path }}"
Validate offline:
cd ~/rebash-github-actions/module-14
set -euo pipefail
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/ci-reusable.yml')); print('reusable workflow OK')"
grep -q 'workflow_call' .github/workflows/ci-reusable.yml
Expected output
reusable workflow OK
Task 3 – Caller workflow (pair)¶
Create .github/workflows/caller.yml:
name: Caller
on:
push:
branches: [main]
workflow_dispatch:
permissions:
contents: read
jobs:
platform-ci:
uses: ./.github/workflows/ci-reusable.yml
with:
lab-name: module-14
Validate offline:
cd ~/rebash-github-actions/module-14
set -euo pipefail
python3 -c "import yaml; yaml.safe_load(open('.github/workflows/caller.yml')); print('caller workflow OK')"
grep -q 'uses: ./.github/workflows/ci-reusable.yml' .github/workflows/caller.yml
Expected output
caller workflow OK
Task 4 – Validate pair and export reusable contract¶
Create reusable-contract.yaml:
# Module 14 reusable contract — machine-readable platform API
composite:
path: .github/actions/setup-lab
name: setup-lab
inputs:
lab-name:
required: true
type: string
outputs:
marker-path:
description: Path to marker file
reusable_workflow:
path: .github/workflows/ci-reusable.yml
trigger: workflow_call
inputs:
lab-name:
required: true
type: string
outputs:
marker-path:
from_job: build
caller:
path: .github/workflows/caller.yml
invokes: ./.github/workflows/ci-reusable.yml
with:
lab-name: module-14
pinning:
rule: Callers pin reusable ref to tag or SHA when published cross-repo
Validate and archive:
cd ~/rebash-github-actions/module-14
set -euo pipefail
python3 -c "
import yaml
with open('reusable-contract.yaml') as f:
doc = yaml.safe_load(f)
assert doc['composite']['inputs']['lab-name']['required'] is True
assert doc['reusable_workflow']['trigger'] == 'workflow_call'
assert doc['caller']['with']['lab-name'] == 'module-14'
print('reusable-contract.yaml OK')
"
tar -czf module-14-evidence.tgz .github/actions/setup-lab/action.yml .github/workflows/*.yml reusable-contract.yaml out/module-14.txt 2>/dev/null || \
tar -czf module-14-evidence.tgz .github/actions/setup-lab/action.yml .github/workflows/*.yml reusable-contract.yaml
ls -l module-14-evidence.tgz | tee evidence.txt
Expected output
reusable-contract.yaml OK; evidence archive with composite + reusable + caller.
Validation steps¶
- Composite
action.ymlparses - Reusable workflow has
workflow_calland outputs - Caller workflow references reusable with
with:input - Contract YAML lists composite inputs/outputs and reusable
workflow_callinputs
Common errors and fixes¶
| Error | Cause | Fix |
|---|---|---|
Composite missing shell: | Invalid action.yml | Add shell: bash per step |
| Reusable not found | Wrong path/ref | Use ./.github/workflows/x.yml locally |
| Secret not passed | Caller omitted secrets | Add secrets: inherit or explicit map |
| Output empty | Output not in job outputs | Wire job outputs to step outputs |
uses: composite in wrong path | Checkout missing | Caller must checkout before local action |
Challenge exercise¶
Publish the reusable workflow pattern to a second folder module-14-consumer/ with only caller.yml that references ../module-14 via workflow_call path. Add a validate-consumer.sh script that greps for the reusable path and exits non-zero if missing.
Learning outcomes¶
- Built composite action with inputs/outputs
- Created reusable workflow callable from caller
- Exported platform contract as validated YAML schema
- Validated all YAML offline
Cleanup¶
Validation¶
- Lab completed under
~/rebash-github-actions/module-14/ - You can choose composite vs reusable for a scenario
- You can explain
secrets: inherit - You can describe pinning strategy for reusable refs
Code Walkthrough¶
- Composite for step bundles — setup, lint, scan snippets.
- Reusable for whole CI — jobs, environments, gates.
- Document contract — inputs, secrets, outputs in README.
- Pin refs — SHA/tag for cross-repo callers.
- Checkout first — local actions need files on disk.
Security Considerations¶
- Reusable workflows run in caller context — trust platform repo owners.
- Never pass production secrets to untrusted caller repos without policy.
- Pin reusable refs —
@mainallows supply-chain swap. - Audit composite actions for credential exfiltration (
curlwith secrets). - Limit
workflow_callto trusted repositories via org settings.
Common Mistakes¶
Reusable workflow on @main
Breaking change ships silently. Fix: semver tags; callers pin @v1.
Composite without documented outputs
Callers cannot chain jobs. Fix: define outputs in action.yml.
Duplicating OIDC login in every repo
Drift and review burden. Fix: composite or reusable login job once.
Mega-composite doing deploy + test + scan
Hard to test and reuse. Fix: split composites by concern.
Best Practices¶
- Version platform components (
v1,v2) with changelog. - Provide example caller workflow in platform repo.
- Use
workflow_callinputs withtype:and defaults. - Test reusable workflows with
workflow_dispatchin platform repo. - Align with Module 11 SHA pinning for any external actions inside reusables.
Troubleshooting¶
| Symptom | Likely cause | Fix |
|---|---|---|
workflow was not found | Path or visibility | Public repo or same org access |
| Composite step not found | Wrong uses: path | Relative to repo root |
| Secret empty in reusable | Not mapped | secrets: inherit or explicit |
| Output not available to caller | Job output not exported | Set reusable workflow outputs |
| Local action fails on fork | Path only on default branch | Document minimum ref |
Summary¶
Composite actions bundle steps; reusable workflows bundle jobs — together they form an internal Actions platform. Callers stay thin; platform teams own pins, scans, and OIDC. Next: Production Pipelines and Environments.
Interview Questions¶
1. What is the difference between a composite action and a reusable workflow?
Reveal answer
Composite actions group steps that run on the caller's job runner; reusable workflows define callable jobs/workflows with their own job graph, needs, and environments invokable via workflow_call.
2. When would you choose a composite action over a reusable workflow?
Reveal answer
When you need a small reusable step bundle (setup, lint script) within a job; reusable workflows fit when standardising entire CI pipelines across repositories.
3. How do callers pass secrets to reusable workflows?
Reveal answer
Explicitly map secrets: in the caller job or use secrets: inherit to pass all available secrets — document required secret names in the platform contract.
4. Why pin reusable workflow references by tag or SHA?
Reveal answer
Floating branches (@main) let platform changes break all callers without review; pins make upgrades deliberate and auditable.
5. Can composite actions define runs-on?
Reveal answer
No — composite actions run in the context of the caller job's runner; only reusable workflows and regular jobs specify runs-on.
6. How do reusable workflow outputs reach the caller?
Reveal answer
Define outputs at the reusable workflow level mapping from job outputs; caller accesses via needs.<job-id>.outputs.<name>.
7. What security risk do third-party reusable workflows carry?
Reveal answer
They execute with access to caller secrets if passed — only use trusted org/platform repos and pin immutable refs.
8. How do reusable workflows relate to Jenkins shared libraries?
Reveal answer
Both centralise pipeline logic: shared libraries supply Groovy steps/functions; reusable workflows supply callable CI graphs — service repos invoke standard behaviour with parameters.
Related Tutorials¶
- Workflow Syntax: Matrix and Reusable Workflows
- Production Pipelines and Environments
- Jenkins Shared Libraries