GitHub Actions Tutorial — Composite Actions
Composite Actions
Composite actions bundle multiple steps into a single reusable action — written entirely in YAML. No JavaScript or Docker required. They’re the easiest type of custom action to create.
When to Use Composite Actions
| Use Case | Example |
|---|---|
| Group common setup steps | Checkout + install deps + cache |
| Standardize CI patterns | Lint + format check across repos |
| Simplify complex step sequences | Build + tag + push container image |
| Share within an org | Common security scanning steps |
Action Directory Structure
my-action/
├── action.yml # required — action metadata
└── README.md # recommended — usage docs
For actions within a repository:
.github/
└── actions/
└── setup-project/
└── action.yml
Basic Composite Action
.github/actions/setup-node-project/action.yml:
name: 'Setup Node Project'
description: 'Checkout, setup Node.js, install dependencies'
inputs:
node-version:
description: 'Node.js version'
required: false
default: '20'
runs:
using: 'composite'
steps:
- uses: actions/checkout@v4
shell: bash
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- run: npm ci
shell: bash
Using It in a Workflow
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: ./.github/actions/setup-node-project
with:
node-version: '20'
- run: npm test
One step replaces three.
Composite Action with Outputs
name: 'Get Version'
description: 'Extract version from package.json'
outputs:
version:
description: 'Package version'
value: ${{ steps.ver.outputs.version }}
runs:
using: 'composite'
steps:
- name: Read version
id: ver
run: |
VERSION=$(jq -r .version package.json)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
shell: bash
Consuming the Output
steps:
- uses: actions/checkout@v4
- name: Get version
id: version
uses: ./.github/actions/get-version
- run: echo "Version is ${{ steps.version.outputs.version }}"
Important Rules
| Rule | Detail |
|---|---|
shell: required |
Every run: step must specify shell: |
No on: trigger |
Actions don’t have triggers — they’re called by workflows |
| Can use other actions | Composite steps can include uses: |
No services: or container: |
Not available in composite actions |
| Inputs are strings | All inputs are passed as strings |
Common Mistakes
| Mistake | Fix |
|---|---|
Missing shell: in run steps |
Always add shell: bash (or pwsh, python, etc.) |
| Action not found | Path must be relative: ./.github/actions/name |
| Output not accessible | Use $GITHUB_OUTPUT and expose via outputs: in action.yml |
Trying to use services: |
Not supported — use a workflow-level service instead |
Quick Reference
# action.yml skeleton
name: 'My Action'
description: 'What it does'
inputs:
param:
description: 'Input parameter'
required: true
outputs:
result:
description: 'Output value'
value: ${{ steps.main.outputs.result }}
runs:
using: 'composite'
steps:
- run: echo "hello"
shell: bash
← Back to: Chapter 11: Custom Actions
Next: JavaScript Actions →