Composite actions
Когда composite action лучше copy-paste steps: metadata, inputs, outputs, shell portability, path handling, limitations и локальное использование внутри repo.
Composite actions
Composite action — это custom action, который собирает несколько workflow steps в один uses-step. Он хорошо подходит для повторяющихся кусков YAML: setup языка, проверка lockfile, подготовка toolchain, стандартный набор lint-команд, публикация отчёта в $GITHUB_STEP_SUMMARY. В отличие от Reusable workflows, composite action не описывает целый job или pipeline: у него нет своего runs-on, permissions, environment, services или matrix. Он выполняется внутри job, который уже выбрал runner и выдал токен.
GitHub поддерживает три типа custom actions: composite, JavaScript actions и Docker container actions. Composite — самый лёгкий вариант, когда логика уже выражается как shell steps и вызовы других actions. Если нужна сложная переносимая логика через GitHub API, лучше смотреть в сторону JavaScript action. Если нужно контролируемое Linux-окружение с системными пакетами, это зона Docker action.
flowchart TD A[Workflow job] --> B[Step: uses composite action] B --> C[action.yml] C --> D[Input mapping] C --> E[runs.steps] E --> F[uses: setup action] E --> G[run: bundled script] E --> H[write to GITHUB_OUTPUT] H --> I[Composite output] I --> J[Caller reads steps.<id>.outputs.*] K[Job-level settings] --> A K --> L[runs-on] K --> M[permissions] K --> N[environment] style C fill:#e8f4ff,stroke:#3b82f6 style K fill:#fff7ed,stroke:#f97316
Базовая структура
Composite action живёт в директории с metadata-файлом action.yml или action.yaml; предпочтительный вариант — action.yml. Для локальных действий внутри repo часто используют .github/actions/<name>/action.yml:
.github/
workflows/
ci.yml
actions/
setup-node-project/
action.yml
scripts/
validate-lockfile.shМинимальный action.yml выглядит так:
name: "Setup Node project"
description: "Install Node.js, dependencies and validate lockfile"
inputs:
node-version:
description: "Node.js version"
required: true
install-command:
description: "Dependency install command"
required: false
default: "npm ci"
outputs:
node-version:
description: "Resolved Node.js version label"
value: ${{ steps.meta.outputs.node-version }}
runs:
using: "composite"
steps:
- uses: actions/setup-node@v6
with:
node-version: ${{ inputs.node-version }}
cache: npm
- name: Install dependencies
run: ${{ inputs.install-command }}
shell: bash
- name: Validate lockfile
run: "$GITHUB_ACTION_PATH/scripts/validate-lockfile.sh"
shell: bash
env:
GITHUB_ACTION_PATH: ${{ github.action_path }}
- id: meta
run: echo "node-version=${NODE_VERSION}" >> "$GITHUB_OUTPUT"
shell: bash
env:
NODE_VERSION: ${{ inputs.node-version }}Здесь runs.using: "composite" говорит GitHub, что внутри будет список steps. Эти steps могут быть run или uses: например, composite action может вызвать actions/setup-node, локальный action или action из Marketplace. Для каждого run-step в composite action нужно явно указать shell. Это полезная дисциплина: сразу видно, завязан ли action на bash, pwsh или другую оболочку.
Как вызывать локальный composite action
Если action лежит в том же репозитории, workflow вызывает его по относительному пути. Для локального action обычно нужен actions/checkout, потому что runner должен получить файлы repo до uses: ./.github/actions/...:
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-24.04
permissions:
contents: read
steps:
- uses: actions/checkout@v6
- id: setup
uses: ./.github/actions/setup-node-project
with:
node-version: "22"
install-command: "npm ci"
- run: echo "Composite used Node ${{ steps.setup.outputs.node-version }}"
shell: bash
- run: npm test
shell: bashДля внешнего repo синтаксис такой же, как у других actions: owner/repo/path@ref. По тем же причинам, что в Marketplace actions и pinning, production-sensitive composite action лучше pin-ить по commit SHA или по контролируемому release tag, а не по default branch.
Inputs, outputs и env
Inputs доступны через ${{ inputs.<name> }}. Они не становятся автоматически shell-переменными с удобными именами, поэтому для shell-кода лучше прокидывать их через env. Это снижает путаницу между expression evaluation и runtime shell evaluation из Contexts и expressions:
- name: Print target
run: echo "Target: $DEPLOY_TARGET"
shell: bash
env:
DEPLOY_TARGET: ${{ inputs.target }}Outputs строятся в два шага. Внутренний step пишет значение в $GITHUB_OUTPUT, а metadata-файл мапит его наружу через outputs.<id>.value:
outputs:
report-path:
description: "Path to generated report"
value: ${{ steps.report.outputs.path }}
runs:
using: "composite"
steps:
- id: report
run: echo "path=reports/junit.xml" >> "$GITHUB_OUTPUT"
shell: bashПосле этого caller читает output как steps.<step_id>.outputs.report-path, то есть через ту же модель, что в Step outputs и job outputs. Ограничения на размер outputs остаются workflow-level ограничениями: не надо передавать большие JSON-отчёты через output, для этого есть Artifacts и reports.
Path handling: где реально лежит script
Самая частая ошибка в composite actions — написать run: ./scripts/foo.sh и ожидать, что путь будет относиться к директории action. На практике команда выполняется в контексте job workspace, то есть обычно в checkout-е caller repo. Если нужен файл, который поставляется вместе с action, используйте ${{ github.action_path }} или переменную GITHUB_ACTION_PATH.
- name: Run bundled script
run: "$GITHUB_ACTION_PATH/scripts/validate-lockfile.sh"
shell: bash
env:
GITHUB_ACTION_PATH: ${{ github.action_path }}Если нужен путь в проекте caller-а, используйте рабочую директорию явно:
- name: Build package
run: npm run build
shell: bash
working-directory: ${{ inputs.package-dir }}Так action не путает «мои собственные файлы» и «код репозитория, который меня вызвал».
Shell portability и границы инструмента
Composite action переносим настолько, насколько переносимы его steps. bash отлично подходит для Ubuntu/macOS runners, но на Windows он означает зависимость от доступного Git Bash. Если action должен честно работать на Windows, macOS и Linux, иногда проще развести шаги по condition:
runs:
using: "composite"
steps:
- name: Unix setup
if: runner.os != 'Windows'
run: ./scripts/setup.sh
shell: bash
- name: Windows setup
if: runner.os == 'Windows'
run: ./scripts/setup.ps1
shell: pwshЕсли таких развилок становится много, composite action перестаёт быть простым wrapper-ом. Тогда стоит вынести логику в JavaScript actions, где один runtime может скрыть платформенные отличия, или в Docker container actions, если допустим Linux-only execution.
Есть и security-граница. Composite action не объявляет собственный контракт secrets как reusable workflow. Обычные secrets он получает только если caller явно передал их через with или env; secrets context внутри composite action недоступен. При этом GITHUB_TOKEN доступен action через github.token, а его права всё равно задаются в caller workflow/job через Permissions и GITHUB_TOKEN, а не внутри composite action. Не передавайте untrusted данные из PR title, branch name или issue body прямо в run-команды; это уже тема Secure use и threat model и pull_request_target и untrusted PR.
Когда composite action лучше copy-paste
Composite action оправдан, когда повторяется один и тот же step-level фрагмент, но workflow-структура остаётся разной. Например, десять repos по-разному запускают тесты, но одинаково ставят Node, валидируют lockfile и печатают summary. Это не повод централизовать весь pipeline через Reusable workflows, но хороший повод убрать copy-paste setup-блока.
Не стоит делать composite action для одного короткого run: npm ci, для job-level политики deploy или для сложного API-клиента. Хороший composite action читается как маленькая команда: setup-node-project, prepare-docker-metadata, publish-test-summary, validate-release-tag. Его inputs описывают варианты поведения, outputs возвращают короткие значения, а все существенные права и deploy-решения остаются видны в caller workflow.
See also
- Reusable workflows — когда переиспользовать целые jobs и pipelines, а не набор steps.
- Marketplace actions и pinning — как выбирать и фиксировать actions, которые composite вызывает через
uses. - JavaScript actions — когда shell-wrapper уже слишком хрупкий и нужна полноценная action-логика.
- Docker container actions — когда важнее контролировать Linux-окружение, чем запускаться на всех OS.
- Step outputs и job outputs — базовая модель
$GITHUB_OUTPUTи передачи значений. - Secure use и threat model — почему inputs и contexts нельзя автоматически считать безопасными.
Внешние справки: GitHub Docs по creating a composite action, metadata syntax, custom actions, contexts и workflow commands.
