GitHub Actions Custom Actions
Custom actions let you package a group of steps into a single reusable unit. When the same set of steps appears in multiple workflows or repositories, a custom action eliminates the duplication. GitHub supports three types of custom actions: composite, JavaScript, and Docker-based.
When to Build a Custom Action
Use a custom action when:
✓ The same steps repeat across multiple workflows
✓ You want to share workflow logic across repositories
✓ A task is too complex for a single run step
✓ You want to publish functionality to the GitHub Marketplace
Stick with inline steps when:
✓ The logic is specific to one workflow
✓ The task is a simple shell command
✓ A Marketplace action already exists for the task
Composite Actions — The Simplest Type
A composite action bundles multiple run and uses steps into one reusable action. It runs in the caller's environment using the caller's runner.
Creating a Composite Action
Create a folder and an action.yml file inside it:
my-repo/
└── .github/
└── actions/
└── setup-and-test/
└── action.yml
# .github/actions/setup-and-test/action.yml
name: 'Setup and Test'
description: 'Sets up Node.js, installs dependencies, and runs tests'
inputs:
node-version:
description: 'Node.js version to use'
required: false
default: '20'
test-command:
description: 'Command to run tests'
required: false
default: 'npm test'
outputs:
test-result:
description: 'Whether tests passed'
value: ${{ steps.run-tests.outputs.result }}
runs:
using: 'composite'
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
cache: 'npm'
- name: Install dependencies
run: npm ci
shell: bash
- name: Run tests
id: run-tests
run: |
if ${{ inputs.test-command }}; then
echo "result=pass" >> $GITHUB_OUTPUT
else
echo "result=fail" >> $GITHUB_OUTPUT
fi
shell: bash
Key rules for composite actions:
- Every
runstep must specify ashellexplicitly - Use
${{ inputs.INPUT_NAME }}to access inputs - Write outputs to
$GITHUB_OUTPUTjust like in regular steps
Using the Composite Action
jobs:
ci:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run setup and tests
id: tests
uses: ./.github/actions/setup-and-test
with:
node-version: '20'
test-command: 'npm run test:ci'
- name: Show test result
run: echo "Tests: ${{ steps.tests.outputs.test-result }}"
JavaScript Actions — For Dynamic Logic
A JavaScript action runs Node.js code directly on the runner. This gives you access to the full GitHub API through the official @actions/core and @actions/github packages — much more powerful than shell scripts.
my-action/
├── action.yml
├── index.js
└── node_modules/ (must be committed)
# action.yml
name: 'My JS Action'
description: 'Does something with the GitHub API'
inputs:
message:
description: 'Message to post'
required: true
outputs:
issue-url:
description: 'URL of the created issue'
runs:
using: 'node20'
main: 'index.js'
// index.js
const core = require('@actions/core');
const github = require('@actions/github');
async function run() {
try {
const message = core.getInput('message');
const token = core.getInput('github-token');
const octokit = github.getOctokit(token);
const context = github.context;
const response = await octokit.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: message,
body: `Created automatically by GitHub Actions run #${context.runNumber}`
});
core.setOutput('issue-url', response.data.html_url);
core.info(`Issue created: ${response.data.html_url}`);
} catch (error) {
core.setFailed(error.message);
}
}
run();
Docker Actions — For Any Language
A Docker action runs inside a container. Use this when your action requires specific system tools or a language other than JavaScript.
# action.yml
name: 'Python Analysis Action'
description: 'Runs a Python script for analysis'
runs:
using: 'docker'
image: 'Dockerfile'
# Dockerfile
FROM python:3.12-slim
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY analyze.py .
ENTRYPOINT ["python", "/analyze.py"]
Composite vs JavaScript vs Docker
Type | Language | Speed | Best for
-------------|-------------|---------|-------------------------------
Composite | Shell/YAML | Fastest | Grouping existing steps
JavaScript | Node.js | Fast | GitHub API interactions
Docker | Any | Slower | Non-JS languages, system tools
Publishing an Action to the Marketplace
Any public repository with an action.yml at the root level can be listed on the GitHub Marketplace:
- Move
action.ymlto the repository root - Go to the repository on GitHub
- Click Draft a release
- Check Publish this Action to the GitHub Marketplace
- Fill in the category and description
- Publish the release
After publishing, users can discover and use your action using the standard owner/repo@version format.
