Switch Language
Toggle Theme

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:

TypeImplementationUse Cases
JavaScript ActionWrite JS/TS codeComplex logic, API calls needed
Docker ActionWrite DockerfileSpecific environment, dependencies needed
Composite ActionPure YAML configurationCombine 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 required
  • default: 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:

  1. Must explicitly specify shell. Every run command must have shell: bash (or sh, pwsh). This is a mandatory requirement for composite actions.

  2. Can use uses to call other actions. Like the actions/setup-node@v4 above.

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@v1
  • your-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:

DimensionComposite ActionReusable Workflow
Execution LevelStep group within JobIndependent Job
RunnerUses caller’s RunnerCreates new Runner (or specified)
SecretsMust be explicitly passedAutomatically inherited
Input TypesOnly stringboolean / number / string
Output Passing$GITHUB_OUTPUToutputs + workflow_call
Concurrency ControlInherits caller’s concurrency limitsCan be independently set
Environment VariablesInherits + can addIndependent scope
Use CasesSingle function encapsulationEntire 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.0 to lock to a specific version
  • Use @v1 to 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:

  1. Click “Releases” → “Draft a new release”
  2. Select tag (like v1.0.0)
  3. Fill in Release Notes
  4. Check “Publish this Action to the GitHub Marketplace”
  5. 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 env to 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:

  1. Use ACTIONS_STEP_DEBUG=true to enable verbose logging
  2. Add echo in steps to print variables
  3. Use tmate action 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. 1

    Step1: Create Action Directory Structure

    Create composite action directory in repository:

    • mkdir -p .github/actions/build-test
    • Create action.yml file
  2. 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. 3

    Step3: Local Reference Test

    Reference and test in workflow:

    • uses: ./.github/actions/build-test
    • with: node-version: '20'
    • Test locally before publishing
  4. 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. 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?
Composite actions are step groups within Jobs, use the caller's Runner, and require explicit Secrets passing. Reusable workflows create independent Jobs, and Secrets are automatically inherited. Use composite actions to encapsulate single functions, use reusable workflows to standardize entire pipelines.
Why don't composite action inputs have a type field?
Composite action inputs only support string type. You can't define boolean or number like in reusable workflows. If you need boolean values, pass strings 'true' or 'false' and check in steps.
How do composite actions pass Secrets?
Composite actions cannot directly access the secrets context, must be explicitly passed. Pass through inputs (will be masked in logs) or through env (more secure, won't show value in logs).
Should I use commit SHA or tag when referencing actions?
Use commit SHA in production environments (safest, immutable). Use major tag (like @v1) for internal projects (automatically tracks latest version). Avoid @latest, may upgrade unexpectedly and break builds.
What's the maximum nesting depth for composite actions?
Composite action calling composite action, maximum depth is 10 layers. Recommend no more than 3 layers, too deep makes debugging difficult.
Do composite actions require explicit shell specification?
Yes. Every run command in composite actions must have shell: bash (or sh, pwsh). This is a mandatory requirement. Normal workflows can omit it, but composite actions must specify.

10 min read · Published on: May 6, 2026 · Modified on: May 6, 2026

Comments

Sign in with GitHub to leave a comment