5.2 Authoring Composite Actions
Key Takeaways
- Composite actions combine multiple workflow steps (including shell commands and nested actions) into a single reusable action definition using runs.using: 'composite'.
- Every single run: step in a composite action MUST explicitly declare the shell: parameter (e.g., shell: bash or shell: pwsh); failure to specify shell causes an immediate parser validation error.
- Inputs are referenced using ${{ inputs.<input_name> }}, while action outputs must be explicitly mapped in action.yml using value: ${{ steps.<step_id>.outputs.<output_name> }}.
- The ${{ github.action_path }} context variable resolves to the absolute filesystem path of the action directory, enabling companion scripts inside the action repository to be executed reliably.
- Composite actions do not support services:, top-level env: blocks, or runner specification (runs-on); environment variables must be declared per step under steps[].env:.
Authoring Composite Actions
Composite Actions allow workflow authors to consolidate multiple sequential workflow steps—including shell scripts and nested GitHub Actions—into a single reusable action. Prior to composite actions, packaging reusable multi-step automation required writing custom Node.js code or building Docker container images.
With composite actions, developers can write pure YAML to bundle setup routines, custom shell tooling, test suites, and deployment logic into modular, maintainable units. Understanding composite action mechanics, mandatory syntax rules, and context resolution is essential for the GH-200 examination.
1. Anatomy of a Composite Action
A composite action is defined in an action.yml file where runs.using is set to 'composite'. Unlike standard workflow jobs, composite actions define their execution sequence under runs.steps.
+-----------------------------------------------------------------------------+
| COMPOSITE ACTION DIRECTORY LAYOUT |
| |
| .github/actions/composite-deploy/ |
| ├── action.yml # Manifest declaring inputs, outputs & steps |
| ├── scripts/ |
| │ ├── validate-config.sh # Companion validation script |
| │ └── deploy-k8s.py # Main execution script |
| └── README.md # Usage documentation & examples |
+-----------------------------------------------------------------------------+
name: 'Composite Kubernetes Deployer'
description: 'Installs kubectl, validates manifests, and deploys to target cluster'
inputs:
kubeconfig-data:
description: 'Base64 encoded kubeconfig string'
required: true
namespace:
description: 'Target Kubernetes namespace'
required: false
default: 'default'
manifest-path:
description: 'Path to deployment manifests'
required: true
outputs:
deployment-status:
description: 'Final rollout status'
value: ${{ steps.rollout.outputs.status }}
runs:
using: 'composite'
steps:
- name: Setup Kubectl Toolchain
uses: azure/setup-kubectl@v3
with:
version: 'v1.28.0'
- name: Configure Kubeconfig File
shell: bash
run: |
mkdir -p "$HOME/.kube"
echo "${{ inputs.kubeconfig-data }}" | base64 --decode > "$HOME/.kube/config"
chmod 600 "$HOME/.kube/config"
- name: Validate Manifests
shell: bash
run: ${{ github.action_path }}/scripts/validate-config.sh "${{ inputs.manifest-path }}"
- name: Apply Deployment & Capture Status
id: rollout
shell: bash
env:
TARGET_NS: ${{ inputs.namespace }}
run: |
kubectl apply -n "$TARGET_NS" -f "${{ inputs.manifest-path }}"
STATUS=$(kubectl rollout status deployment -n "$TARGET_NS" --timeout=120s)
echo "status=${STATUS}" >> "$GITHUB_OUTPUT"
2. The Mandatory shell: Rule
The most critical syntax rule in composite actions—and one of the most frequently tested topics on the GH-200 exam—is the mandatory shell: requirement.
In standard workflow files (.github/workflows/*.yml), if you omit shell: in a run: step, GitHub Actions defaults to bash on Linux/macOS and pwsh (or cmd) on Windows.
[!CAUTION] Composite Action Parsing Requirement: In a composite
action.yml, every singlerun:step MUST explicitly specify theshell:keyword. Composite actions do not inherit default shells from the runner or caller workflow. Omittingshell:causes the workflow parser to reject the action with a fatal schema validation error before execution begins.
Supported Shell Keywords in Composite Actions
shell: bash(Bourne-Again SHell — cross-platform on hosted runners via MSYS2 on Windows)shell: sh(POSIX standard shell on Unix)shell: pwsh(PowerShell Core — cross-platform on Linux, macOS, and Windows)shell: powershell(Windows Desktop PowerShell)shell: cmd(Windows Command Prompt)shell: python(Directly executes Python script if Python is in$PATH)
3. Context Variables: github.action_path vs GITHUB_WORKSPACE
When a composite action includes standalone helper scripts, configuration templates, or binary utilities within its own repository, referencing their location on disk requires careful use of context variables.
+-----------------------------------------------------------------------------+
| FILESYSTEM LOCATIONS DURING COMPOSITE RUN |
| |
| +---------------------------------------------------------------------+ |
| | RUNNER HOST FILESYSTEM | |
| | | |
| | $GITHUB_WORKSPACE (/home/runner/work/caller-repo/caller-repo) | |
| | └── Target application source code checked out by caller | |
| | | |
| | ${{ github.action_path }} (/home/runner/work/_actions/org/act/v1) | |
| | ├── action.yml | |
| | └── scripts/validate-config.sh <-- Action's internal script! | |
| +---------------------------------------------------------------------+ |
+-----------------------------------------------------------------------------+
The Critical Distinction:
$GITHUB_WORKSPACE/${{ github.workspace }}: Points to the working directory of the caller repository whereactions/checkoutunpacked the user's project files.${{ github.action_path }}: Resolves to the absolute path where the composite action itself is located on the runner. For remote actions, this is inside the runner's internal_actions/cache directory (e.g.,/home/runner/work/_actions/owner/repo/v1).
# ✅ CORRECT: Executes script located inside the action repository
- name: Run Internal Action Script
shell: bash
run: bash "${{ github.action_path }}/scripts/audit.sh"
# ❌ INCORRECT: Looks for script inside caller's repository root
- name: Run Failed Script
shell: bash
run: bash "./scripts/audit.sh"
4. Input & Output Mapping Mechanics
Accessing Inputs
Within composite action steps, input parameters are referenced using standard expression syntax: ${{ inputs.<input_name> }}.
Passing Outputs to Callers
Passing data from a composite action back to the calling workflow requires two coordinated steps:
- Step Output Generation: An internal step writes key-value pairs to the
$GITHUB_OUTPUTfile descriptor. - Action Manifest Output Mapping: The top-level
outputs:block inaction.ymlmaps each public output to the internal step's output using thevalue:key.
# action.yml
outputs:
release-id:
description: 'The created release ID'
value: ${{ steps.create-rel.outputs.rel_id }} # REQUIRED mapping
runs:
using: 'composite'
steps:
- id: create-rel
shell: bash
run: |
ID=$(curl -s https://api.example.com/release)
echo "rel_id=${ID}" >> "$GITHUB_OUTPUT"
# caller-workflow.yml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: my-composite
uses: ./.github/actions/release-action
- run: echo "Created Release: ${{ steps.my-composite.outputs.release-id }}"
5. Architectural Limitations of Composite Actions
While composite actions are versatile and lightweight, they have specific architectural boundaries that differentiate them from workflows and JavaScript actions:
- No
services:Container Support: Composite actions cannot define Docker service containers (e.g., PostgreSQL, Redis). Service containers must be defined at the workflow job level. - No Top-Level
env:Block inaction.yml: Composite actions cannot declare global environment variables under the root ofaction.yml. Environment variables must be declared within individual steps understeps[].env:. - No
runs-onor Job-Level Properties: Composite actions execute within whatever runner environment the caller job provides. - No Native
secrets:Declaration inaction.yml: Composite manifests cannot define a top-levelsecrets:schema. Caller workflows pass secrets explicitly viainputs:or inject them into step-levelenv:variables.
A developer authors a composite action with the following step definition in action.yml:
When a workflow attempts to execute this action, what is the result?runs:
using: 'composite'
steps:
- name: Print Welcome Banner
run: echo "Starting composite execution..."
A composite action stored in a dedicated repository includes a Python script at tools/analyzer.py. A caller workflow in a different repository invokes the composite action. Which expression must the composite action author use to execute tools/analyzer.py regardless of the caller's working directory?
An engineer is designing a composite action that must expose an output named digest generated by an internal shell step. How must action.yml be configured to map this output?