A composite action runs inside an existing job and shares its runner and workspace, which makes it the right unit for a sequence of steps and the wrong one for a whole pipeline.
# .github/actions/setup-php/action.yml
runs:
using: composite
steps:
- uses: shivammathur/setup-php@v2
with: { php-version: ${{ inputs.php-version }} }
- run: composer install --no-progress
shell: bash
# every `run` needs an explicit shell. this is the
# single most common mistake in a composite action.
The rule of thumb that works: composite for something that operates on the checkout the caller already has, reusable workflow for something that needs its own runners or its own matrix. Composite actions could not use if on their steps until later in the year, which limited them to unconditional sequences. Keeping them in the same repository as a ./.github/actions/... path avoids the versioning question entirely for repository-local reuse.