GitHub Actions Tutorial — Custom Actions Exercises
Practice Exercises — Custom Actions
Test your understanding of composite actions, JavaScript actions, Docker actions, metadata, and publishing with these hands-on exercises.
Exercise 1 — Composite Setup Action
Task: Create a composite action called setup-python-project that:
- Accepts a
python-versioninput (default'3.12') - Checks out code
- Sets up Python with the specified version
- Installs dependencies from
requirements.txt - Outputs the installed Python version
Show Solution
.github/actions/setup-python-project/action.yml:
name: 'Setup Python Project'
description: 'Checkout, setup Python, install dependencies'
inputs:
python-version:
description: 'Python version'
required: false
default: '3.12'
outputs:
python-version:
description: 'Installed Python version'
value: ${{ steps.info.outputs.version }}
runs:
using: 'composite'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
cache: 'pip'
- run: pip install -r requirements.txt
shell: bash
- name: Get version
id: info
run: |
VERSION=$(python --version | cut -d' ' -f2)
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
shell: bash
Usage in workflow:
steps:
- id: setup
uses: ./.github/actions/setup-python-project
with:
python-version: '3.12'
- run: echo "Python ${{ steps.setup.outputs.python-version }}"
- run: pytest
Exercise 2 — JavaScript Action Skeleton
Task: Write the action.yml and index.js for a JavaScript action that:
- Takes a
nameinput (required) - Prints “Hello,
<name>!” to the log - Outputs the current UTC timestamp as
greeting-time - Fails with a clear message if
nameis empty
Show Solution
action.yml:
name: 'Greet Action'
description: 'Greets a user and returns the timestamp'
inputs:
name:
description: 'Name to greet'
required: true
outputs:
greeting-time:
description: 'UTC time of greeting'
runs:
using: 'node20'
main: 'index.js'
index.js:
const core = require('@actions/core');
try {
const name = core.getInput('name');
if (!name) {
core.setFailed('Input "name" is required but was empty');
process.exit(1);
}
console.log(`Hello, ${name}!`);
const time = new Date().toISOString();
core.setOutput('greeting-time', time);
core.info(`Greeted ${name} at ${time}`);
} catch (error) {
core.setFailed(error.message);
}
Exercise 3 — Docker Action
Task: Write the action.yml, Dockerfile, and entrypoint.sh for a Docker action that:
- Takes a
directoryinput (default.) - Counts the number of files in the directory
- Prints the count and outputs it as
file-count
Show Solution
action.yml:
name: 'Count Files'
description: 'Count files in a directory'
inputs:
directory:
description: 'Directory to count files in'
required: false
default: '.'
outputs:
file-count:
description: 'Number of files found'
runs:
using: 'docker'
image: 'Dockerfile'
args:
- ${{ inputs.directory }}
Dockerfile:
FROM alpine:3.19
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
ENTRYPOINT ["/entrypoint.sh"]
entrypoint.sh:
#!/bin/sh
set -e
DIR="${1:-.}"
COUNT=$(find "/github/workspace/$DIR" -type f | wc -l)
echo "Files in $DIR: $COUNT"
echo "file-count=$COUNT" >> "$GITHUB_OUTPUT"
Note: The workspace is mounted at /github/workspace inside Docker actions.
Exercise 4 — Action Metadata Review
Task: Identify and fix all the errors in this action.yml:
name: Deploy
inputs:
environment:
required: yes
version:
default: latest
runs:
using: composite
steps:
- run: echo "Deploying"
Show Solution
Fixed version:
name: 'Deploy'
description: 'Deploy application' # missing: required field
inputs:
environment:
description: 'Target environment' # missing: description
required: true # 'yes' is invalid, must be true/false
version:
description: 'Version to deploy' # missing: description
default: 'latest'
runs:
using: 'composite' # should be quoted for consistency
steps:
- run: echo "Deploying ${{ inputs.environment }} v${{ inputs.version }}"
shell: bash # missing: shell is required for composite
Errors found:
- Missing
descriptionat top level (required for Marketplace) required: yes— must betrueorfalse- Missing
descriptionon each input - Missing
shell:onrun:step (required in composite actions)
Exercise 5 — Release and Versioning
Task: You’ve published v1.0.0 of your action. Now you need to:
- Fix a bug (patch release v1.0.1)
- Update the
v1major tag - Add a new optional input (minor release v1.1.0)
- Update the
v1major tag again
Write the exact git commands for each step.
Show Solution
Step 1 — Patch release v1.0.1:
# After committing the bug fix
git tag v1.0.1
git push origin v1.0.1
Step 2 — Update v1 tag:
git tag -fa v1 v1.0.1
git push origin v1 --force
Step 3 — Minor release v1.1.0:
# After committing the new feature
git tag v1.1.0
git push origin v1.1.0
Step 4 — Update v1 tag again:
git tag -fa v1 v1.1.0
git push origin v1 --force
Users on @v1 automatically get all patches and features. They only need to change their reference when you release v2 with breaking changes.
← Back to: Chapter 11: Custom Actions
Next Chapter: Chapter 12: Self-Hosted Runners →