GitHub Actions Tutorial — Custom Actions Exercises

3 minute read

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:

  1. Accepts a python-version input (default '3.12')
  2. Checks out code
  3. Sets up Python with the specified version
  4. Installs dependencies from requirements.txt
  5. 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:

  1. Takes a name input (required)
  2. Prints “Hello, <name>!” to the log
  3. Outputs the current UTC timestamp as greeting-time
  4. Fails with a clear message if name is 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:

  1. Takes a directory input (default .)
  2. Counts the number of files in the directory
  3. 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:

  1. Missing description at top level (required for Marketplace)
  2. required: yes — must be true or false
  3. Missing description on each input
  4. Missing shell: on run: step (required in composite actions)

Exercise 5 — Release and Versioning

Task: You’ve published v1.0.0 of your action. Now you need to:

  1. Fix a bug (patch release v1.0.1)
  2. Update the v1 major tag
  3. Add a new optional input (minor release v1.1.0)
  4. Update the v1 major 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 →

Updated: