Composite Actions

Composite Actions for Shared Steps

A composite action bundles steps behind an action.yml, so that "install Node.js 2,131 from .nvmrc, restore the npm 2,036 cache, run npm ci" becomes one line in every job. It lives in its own directory, here in the repository itself:

.github/actions/setup-booknest/action.ymlYAML
name: Set up BookNest
description: Install Node.js from .nvmrc and BookNest's locked dependencies
inputs:
  omit-dev:
    description: Leave out devDependencies
    default: "false"
outputs:
  node-version:
    description: The Node.js version that was installed
    value: ${{ steps.node.outputs.node-version }}
runs:
  using: composite
  steps:
    - id: node
      uses: actions/setup-node@v7
      with: {node-version-file: .nvmrc, cache: npm}
    - shell: bash
      run: npm ci ${{ inputs.omit-dev == 'true' && '--omit=dev' || '' }}

Inputs are always strings, hence the comparison with 'true'; the && ... || ... pair is the expression language's conditional. Every run step must name its shell, because a composite action has no defaults to inherit. The log above shows its steps nested under Run ./.github/actions/setup-booknest, and the job read its output as steps.setup.outputs.node-version (v24.21.0). A local action (uses: ./path) needs the repository checked out first; one in another repository is referenced as owner/repo/path@ref and needs no checkout. Composite actions may call other actions, but they cannot declare services, runs-on or environment: those belong to jobs, and so to reusable workflows.