GitHub Actions Composite Action Development: Complete Guide from action.yml to Marketplace Publishing
At 3 AM, I stared at the 15th failed workflow log on my screen. The error? I forgot to add NODE_AUTH_TOKEN in a private repository’s npm install step.
This was the third time this week. We have eight repositories, each with nearly identical workflow configurations copied and pasted: checkout, setup-node, install, build, test. Changing one place meant syncing across eight repositories. One day, while upgrading Node versions, I updated five repositories and forgot the remaining three.
That moment, I realized: this can’t continue. GitHub Actions Composite Action exists precisely for this purpose - packaging repeated steps into a reusable component that can be used across multiple workflows like calling a function. This article will guide you from developing your first composite action to publishing it on Marketplace, mastering the core skills of CI/CD componentization.
Chapter 1: Composite Action Core Concepts
1.1 What is a Composite Action
A composite action is a componentization mechanism provided by GitHub Actions. It encapsulates multiple steps into a single independent action that can be reused across different workflows.
Unlike JavaScript Actions or Docker Actions, composite actions don’t require writing code. You only need to write YAML configuration, define a series of steps, and GitHub will execute them automatically. Simply put, a composite action is a “function encapsulation” of steps.
The official definition states: composite actions use runs.using: "composite" to identify themselves, and all steps execute in the same Runner. This means you can directly access the working directory, environment variables, and even call other actions.
1.2 Comparison of Three Action Types
GitHub Actions supports three action types:
| Type | Implementation | Use Cases |
|---|---|---|
| JavaScript Action | Write JS/TS code | Complex logic, API calls needed |
| Docker Action | Write Dockerfile | Specific environment, dependencies needed |
| Composite Action | Pure YAML configuration | Combine existing steps, quick reuse |
The advantages of composite actions are clear: zero code, rapid development, easy maintenance. You don’t need to handle packaging, compilation, or dependency management - just write YAML.
1.3 Composite Action vs Reusable Workflow
This is where many people get confused. At first glance, both seem like “reuse,” but they’re fundamentally different:
Composite Action is a step group. It executes within a Job, uses the caller’s Runner, and requires explicit Secrets passing.
Reusable Workflow is a complete pipeline. It creates independent Jobs, can have its own Runner, and Secrets are automatically inherited.
For example: if you want to package “install dependencies + run tests” for reuse, use a composite action. If you want to standardize the entire “build→test→deploy” pipeline, use a reusable workflow. Chapter 4 will compare them in detail.
Chapter 2: Developing Your First Composite Action
2.1 action.yml Structure Explained
The core of a composite action is the action.yml file. This is a metadata file that defines the action’s name, inputs, outputs, and execution steps.
Let’s start with a minimal example:
name: 'Hello World'
description: 'A simple composite action'
runs:
using: "composite"
steps:
- run: echo "Hello from composite action!"
shell: bash
This action does only one thing: prints a line of text. But in real projects, we need parameterization and outputs.
2.2 Complete action.yml Example
Here’s a practical composite action for building and testing Node.js projects:
name: 'Build and Test'
description: 'Install dependencies, build project, and run tests'
author: 'Your Name'
inputs:
node-version:
description: 'Node.js version to use'
required: true
default: '20'
install-command:
description: 'Command to install dependencies'
required: false
default: 'npm ci'
build-command:
description: 'Command to build the project'
required: false
default: 'npm run build'
test-command:
description: 'Command to run tests'
required: false
default: 'npm test'
outputs:
build-path:
description: 'Path to the build output'
value: ${{ steps.build.outputs.path }}
test-coverage:
description: 'Test coverage percentage'
value: ${{ steps.coverage.outputs.value }}
runs:
using: "composite"
steps:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
run: ${{ inputs.install-command }}
shell: bash
- name: Build project
id: build
run: |
${{ inputs.build-command }}
echo "path=dist" >> $GITHUB_OUTPUT
shell: bash
- name: Run tests
id: coverage
run: |
${{ inputs.test-command }}
echo "value=85" >> $GITHUB_OUTPUT
shell: bash
2.3 Field Explanations
inputs: Define parameters the action receives. Each parameter can specify:
description: Parameter description (will be shown in Marketplace)required: Whether it’s requireddefault: Default value
outputs: Define the action’s outputs. Passed through the $GITHUB_OUTPUT environment variable. Note the format:
echo "name=value" >> $GITHUB_OUTPUT
runs.steps: Execution steps. Similar to normal workflows, but with two key differences:
-
Must explicitly specify shell. Every
runcommand must haveshell: bash(orsh,pwsh). This is a mandatory requirement for composite actions. -
Can use
usesto call other actions. Like theactions/setup-node@v4above.
2.4 Common Pitfalls
During development, I encountered several pitfalls:
Pitfall 1: inputs has no type field
Composite action inputs only support string type. You can’t define type: boolean or type: number like in reusable workflows. If you need boolean values, you can only pass strings "true" or "false" and then check in steps.
Pitfall 2: shell must be explicitly specified
In normal workflows, run commands can omit shell, and GitHub will automatically choose based on the Runner. But composite actions must explicitly specify it, otherwise an error will occur.
Pitfall 3: outputs must reference step id
The value field of outputs must reference the step’s output:
outputs:
my-output:
value: ${{ steps.my-step.outputs.result }}
And the step must have id set:
- id: my-step
run: echo "result=hello" >> $GITHUB_OUTPUT
Chapter 3: Using Composite Actions
3.1 Local Reference
The simplest way to use it is referencing within the same repository. Assuming your action is at .github/actions/build-test/action.yml:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Local reference
- uses: ./.github/actions/build-test
with:
node-version: '20'
test-command: 'npm run test:ci'
The path starts with ./, relative to the repository root. This approach is suitable for internal team reuse without publishing to Marketplace.
3.2 Cross-Repository Reference
If you want to share across multiple repositories, you can put the action in an independent repository and then reference cross-repository:
steps:
# Reference shared action within the organization
- uses: your-org/shared-actions/build@v1
with:
node-version: '18'
The path format is owner/repo/path@version. Where path is the action’s relative path in the repository.
Recommended organization-level action repository design:
shared-actions/
├── build/
│ └── action.yml # Build related
├── deploy/
│ └── action.yml # Deploy related
├── lint/
│ └── action.yml # Code linting
└── README.md
This makes referencing clear:
your-org/shared-actions/build@v1your-org/shared-actions/deploy@v1
3.3 Using from Marketplace
After publishing to Marketplace, users can directly search and reference:
steps:
# Assuming your action is published as "build-test-action"
- uses: your-org/[email protected]
with:
node-version: '20'
Marketplace provides features like version selection, usage statistics, README display, suitable for open source projects or publicly shared actions.
3.4 Version Selection Strategy
When referencing actions, there are three version selection methods:
# Method 1: commit SHA (safest, immutable)
- uses: your-org/action@a1b2c3d4e5f6...
# Method 2: semantic version tag (recommended)
- uses: your-org/[email protected]
# Method 3: major tag (automatically tracks latest v1.x)
- uses: your-org/action@v1
# Not recommended: latest tag (may upgrade unexpectedly)
# - uses: your-org/action@latest
Security Recommendations:
For production environments, using commit SHA is the safest choice. Because SHA is immutable, it won’t accidentally upgrade when maintainers update tags.
For internal projects, using major tag (like @v1) is a flexible choice. It automatically tracks the latest v1.x version, getting bug fixes and new features.
Avoid using @latest. It may lead to unexpected upgrades that break builds.
Chapter 4: Composite Action vs Reusable Workflow
4.1 Feature Comparison Table
This is the core reference when choosing:
| Dimension | Composite Action | Reusable Workflow |
|---|---|---|
| Execution Level | Step group within Job | Independent Job |
| Runner | Uses caller’s Runner | Creates new Runner (or specified) |
| Secrets | Must be explicitly passed | Automatically inherited |
| Input Types | Only string | boolean / number / string |
| Output Passing | $GITHUB_OUTPUT | outputs + workflow_call |
| Concurrency Control | Inherits caller’s concurrency limits | Can be independently set |
| Environment Variables | Inherits + can add | Independent scope |
| Use Cases | Single function encapsulation | Entire pipeline standardization |
4.2 Selection Decision Matrix
Choose the appropriate reuse method based on scenario:
Use Composite Action:
- Package repeated build/test/deploy steps
- Need to interact with other steps within a Job
- Want to maintain workflow flexibility, only reuse partial steps
- Secrets passing is controllable, high security requirements
Use Reusable Workflow:
- Standardize entire CI/CD pipeline
- Multiple projects need the same complete flow
- Need automatic Secrets inheritance (reduce configuration)
- Need workflow-level configurations like
if,timeout-minutes
Combined Use:
In practice, the best approach is combined use. Reusable workflows call composite actions:
# .github/workflows/ci.yml (Reusable Workflow)
on:
workflow_call:
inputs:
node-version:
type: string
default: '20'
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# Call composite action
- uses: ./.github/actions/build-test
with:
node-version: ${{ inputs.node-version }}
This provides both workflow-level standard configuration and action-level step reuse.
Chapter 5: Version Management and Publishing
5.1 Git Tags Best Practices
The core of publishing actions is Git Tags management. Recommended approach: semantic versioning + major tag dual strategy:
# Create semantic version tag
git tag -a v1.0.0 -m "Initial release"
git push origin v1.0.0
# Create major tag (tracks latest v1.x)
git tag -fa v1 -m "Update v1 tag to latest"
git push origin v1 --force
When releasing v1.1.0, update the major tag:
git tag -a v1.1.0 -m "Add new feature"
git push origin v1.1.0
# Update v1 tag to point to latest version
git tag -fa v1 -m "Update v1 tag to v1.1.0"
git push origin v1 --force
This way users can choose:
- Use
@v1.1.0to lock to a specific version - Use
@v1to automatically get v1.x updates
5.2 Marketplace Publishing Process
Publishing to GitHub Marketplace requires:
1. Prepare README.md
README must include:
- Action name and description
- Inputs and Outputs explanation
- Usage examples
- License
2. Prepare action.yml
Ensure name, description, author, branding (optional) fields are complete.
3. Create Release
On the GitHub repository page:
- Click “Releases” → “Draft a new release”
- Select tag (like
v1.0.0) - Fill in Release Notes
- Check “Publish this Action to the GitHub Marketplace”
- Publish
GitHub will automatically validate action.yml and publish to Marketplace.
5.3 Automated Release Workflow
You can create a workflow for automated publishing:
name: Release Action
on:
push:
tags:
- 'v*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Update major tag
run: |
# Extract major version number
MAJOR=$(echo $GITHUB_REF | sed 's/refs\/tags\/v\([0-9]*\).*/\1/')
# Update major tag
git config user.name github-actions
git config user.email [email protected]
git tag -fa v$MAJOR -m "Update v$MAJOR tag"
git push origin v$MAJOR --force
- name: Create GitHub Release
uses: softprops/action-gh-release@v1
with:
generate_release_notes: true
This workflow automatically updates major tags and creates releases when pushing tags.
Chapter 6: Advanced Techniques and Best Practices
6.1 Secure Secrets Passing
Composite actions cannot directly access the secrets context. Must be explicitly passed:
# In workflow
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: ./.github/actions/deploy
with:
token: ${{ secrets.DEPLOY_TOKEN }}
env:
AWS_ACCESS_KEY: ${{ secrets.AWS_ACCESS_KEY }}
# In action.yml
inputs:
token:
description: 'Deploy token'
required: true
runs:
using: "composite"
steps:
- run: deploy --token ${{ inputs.token }}
shell: bash
env:
AWS_ACCESS_KEY: ${{ env.AWS_ACCESS_KEY }}
Security Recommendations:
- Don’t pass sensitive information in inputs (may be visible in logs)
- Use
envto pass sensitive information (will be masked) - Clearly document required Secrets in README
6.2 Using Local Scripts
Complex logic isn’t suitable for writing in YAML. You can place script files in the action directory:
.github/actions/build-test/
├── action.yml
└── scripts/
└── build.sh
Call in action.yml:
runs:
using: "composite"
steps:
- run: $GITHUB_ACTION_PATH/scripts/build.sh
shell: bash
$GITHUB_ACTION_PATH is the root directory path of the composite action.
6.3 Organization-Level Action Repository Design
For teams, recommend centralized management of shared actions:
your-org/shared-actions/
├── .github/
│ └── workflows/
│ └── test.yml # Test all actions
├── build/
│ ├── action.yml
│ └── README.md
├── deploy/
│ ├── action.yml
│ └── README.md
├── lint/
│ ├── action.yml
│ └── README.md
└── README.md
Each subdirectory is an independent action with complete README and tests.
6.4 Common Pitfalls and Debugging Tips
Pitfall 1: Nesting Depth Limit
Composite action calling composite action, maximum depth is 10 layers. Exceeding will error. Recommend nesting no more than 3 layers, difficult to debug.
Pitfall 2: Environment Variable Scope
env in composite actions is only valid within steps. If you need to share across steps, use GITHUB_ENV:
steps:
- run: echo "MY_VAR=value" >> $GITHUB_ENV
shell: bash
- run: echo $MY_VAR # Can access
shell: bash
Debugging Tips:
- Use
ACTIONS_STEP_DEBUG=trueto enable verbose logging - Add
echoin steps to print variables - Use
tmateaction for SSH debugging (not recommended in production)
Conclusion
Composite actions are the core tool for GitHub Actions componentization, letting you say goodbye to repetitive configurations and encapsulate CI steps like writing functions.
Remember three key points:
Clear Structure: action.yml defines inputs/outputs/steps, like designing function signatures. Parameterization makes actions more flexible, outputs let callers retrieve results.
Right Choice: Composite actions encapsulate single functions (build, test, deploy), reusable workflows standardize entire pipelines. Best used in combination.
Version Security: commit SHA is safest, suitable for production environments. major tag is flexible, suitable for internal projects. Avoid latest.
Next step: Create your first composite action in your project, package repeated build-test steps, then try publishing to Marketplace to share with your team or community.
Develop GitHub Actions Composite Action
Create reusable composite action from scratch, package build/test steps
⏱️ Estimated time: 30 min
- 1
Step1: Create Action Directory Structure
Create composite action directory in repository:
• mkdir -p .github/actions/build-test
• Create action.yml file - 2
Step2: Write action.yml Configuration
Define inputs, outputs, and execution steps:
• inputs define parameters (node-version, test-command)
• outputs define outputs (build-path, coverage)
• runs.steps must explicitly specify shell: bash - 3
Step3: Local Reference Test
Reference and test in workflow:
• uses: ./.github/actions/build-test
• with: node-version: '20'
• Test locally before publishing - 4
Step4: Create Git Tags
Use semantic versioning + major tag:
• git tag -a v1.0.0 -m 'Initial release'
• git tag -fa v1 -m 'Update v1 tag'
• git push origin v1.0.0 v1 - 5
Step5: Publish to Marketplace
Publish via GitHub UI:
• Releases → Draft a new release
• Select tag (like v1.0.0)
• Check Publish to Marketplace
• GitHub automatically validates and publishes
FAQ
What's the difference between composite actions and reusable workflows?
Why don't composite action inputs have a type field?
How do composite actions pass Secrets?
Should I use commit SHA or tag when referencing actions?
What's the maximum nesting depth for composite actions?
Do composite actions require explicit shell specification?
10 min read · Published on: May 6, 2026 · Modified on: May 6, 2026
GitHub Actions Complete Guide
If you landed here from search, the fastest way to build context is to jump to the previous or next post in this same series.
Previous
GitHub Actions Matrix Builds: A Practical Guide to Multi-Platform Multi-Version Parallel Testing
A practical guide to GitHub Actions Matrix builds: from basic syntax to advanced techniques like exclude/include, fail-fast, and max-parallel, with 5 production-ready workflow templates to help you generate multi-platform multi-version parallel tests with 60%+ less configuration code
Part 7 of 8
Next
This is the latest post in the series so far.
Related Posts
GitHub Actions Matrix Build: Multi-Version Parallel Testing in Practice
GitHub Actions Matrix Build: Multi-Version Parallel Testing in Practice
GitHub Actions Getting Started: YAML Workflow Basics and Trigger Configuration
GitHub Actions Getting Started: YAML Workflow Basics and Trigger Configuration
GitHub Actions Getting Started: YAML Workflow Basics and Trigger Configuration

Comments
Sign in with GitHub to leave a comment