GitHub Actions Tutorial — Composite Actions

1 minute read

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 →

Updated: