© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2021
C. Chandrasekara, P. HerathHands-on GitHub Actionshttps://doi.org/10.1007/978-1-4842-6464-5_9

9. Creating Custom Actions

Chaminda Chandrasekara1   and Pushpa Herath2
(1)
Dedigamuwa, Sri Lanka
(2)
Hanguranketha, Sri Lanka
 

You must use the default actions and the community-created actions when developing various workflow needs. However, sometimes the requirements that you need to implement in a workflow are not supported by available actions. You may want to create actions to define workflows as you desire in such scenarios.

This chapter explores creating custom actions and utilizing them in GitHub Actions workflows.

Types of Actions

Actions perform specific tasks in a GitHub Actions workflow. With custom actions, you can interact with a GitHub repo using the GitHub API or interact with external APIs to perform activities.

There are three types of actions: Docker container actions, JavaScript actions, and composite run steps actions. Let’s look at each of these types.
  • Docker container actions: The Docker container action’s dependencies are packaged as a Docker container to utilize the action reliably and consistently. Since they need to build and retrieve the container before executing the actions, Docker container actions are slower than JavaScript actions. Docker container actions can only be run on Linux runners. If you want to use a Linux-based self-hosted runner to run Docker container actions, you must first install Docker.

  • JavaScript actions: JavaScript actions run faster and run directly on the runner machine. If you intend to run JavaScript actions on GitHub-hosted runners, the actions should be written in pure JavaScript without any dependencies on any other binaries. JavaScript actions can run on Windows, macOS, or Linux runners.

  • Composite run steps actions: You can combine multiple run steps into a single action and enable a workflow to execute all the run steps defined in the action as a single action. Composite run step actions can run on Windows, macOS, or Linux runners.

This section looked at types of actions and their differences.

Creating Custom Actions

Custom actions perform desired steps and are reusable in multiple workflows. This section looks at creating custom actions.

JavaScript Custom Action

Let’s begin with creating a public GitHub repo. Once the repo is created, it can be cloned to your machine using VS Code. You need to have Node.js 12.x or higher and npm installed on your machine to perform the steps described here. You can verify the node and npm versions with the following commands in a VS Code terminal (also see Figure 9-1).
node --version
npm --version
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig1_HTML.jpg
Figure 9-1

Check node and npm versions

You need to execute npm init -y to initialize the folder with a package.json file (see Figure 9-2).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig2_HTML.jpg
Figure 9-2

Folder for first custom action initialized

Next, you need to create an action metadata file in the folder. The metadata file defines the action's main entry point, input, and output. The name of the file must be action.yml or action.yaml. The following YAML file includes using: 'node12', which says this is a JavaScript action, and main: 'index.js', which defines the entry point. The sample action metadata file is shown next.
name: 'DemoJSAction'
description: 'Display massage'
inputs:
  name-of-you:  # id of input
    description: 'Your name'
    required: true
    default: 'Chaminda'
outputs:
  time: # id of output
    description: 'The time of the message'
runs:
  using: 'node12'
  main: 'index.js'

This metadata file defines one input parameter that asks to provide a name and one output parameter that is the time of the message.

Next, you must set up the actions toolkit packages’ actions/core and actions/github in the custom actions folder. To do this, you need to execute the following commands (also see Figure 9-3).
npm install @actions/core
npm install @actions/github
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig3_HTML.jpg
Figure 9-3

Install actions toolkit components

The code needs to execute the action to index.js because it is the file specified in the metadata to run (see Figure 9-4).
const core = require('@actions/core');
const github = require('@actions/github');
try {
  // `name-of-you` input defined in action metadata file
  const yourName = core.getInput('name-of-you');
  console.log(`Hello ${yourName}!`);
  const time = (new Date()).toTimeString();
  core.setOutput("time", time);
  // Get the JSON webhook payload for the event that triggered the workflow
  const payload = JSON.stringify(github.context.payload, undefined, 2)
  console.log(`The event payload: ${payload}`);
} catch (error) {
  core.setFailed(error.message);
}
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig4_HTML.jpg
Figure 9-4

Code for the action

Optionally, you can add a readMe.md file to the repo so that users know how to use it.
# Demo javascript action
This action prints "Hello Chaminda" or "Hello" + the name of a person
## Inputs
### `name-of-you`
**Required** The name of the You. Default `"Chaminda"`.
## Outputs
### `time`
The time of the message.
## Example usage
uses: chamindac/demojsaction@v1.1
with:
  name-of-you: 'Pushpa'
To compile the code and the modules for distribution, you can use @vercel/ncc, which you must first install. Execute npm i -g @vercel/ncc to install @vercel/ncc/ in the terminal (see Figure 9-5).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig5_HTML.jpg
Figure 9-5

Installing @vercel/ncc

Now you can build the distribution package for the action by using the following command (see Figure 9-6).
ncc build index.js --license licenses.txt
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig6_HTML.jpg
Figure 9-6

Build action for distribution

The dist/index.json is added with node module content, and dist/licenses.txt is added with all the license information for the node modules used (see Figure 9-7).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig7_HTML.jpg
Figure 9-7

Distribution files for action

The action.yml metadata file should be updated to use the new entry point, dist/index.js (see Figure 9-8).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig8_HTML.jpg
Figure 9-8

Change entry point of action

The next step is to commit the code and compiled action.js files to the repo. Use the following command to add the files for commit (also see Figure 9-9).
git add action.yml index.js package.json package-lock.json README.md dist/*
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig9_HTML.jpg
Figure 9-9

Add files

The following commands commit and push the action files to the repo (see Figure 9-10).
git commit -m "First js action is ready"
git tag -a -m "First js action release" v1
git push --follow-tags
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig10_HTML.jpg
Figure 9-10

Commit and push custom action

The action files are available in the public repo, as shown in Figure 9-11.
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig11_HTML.jpg
Figure 9-11

Custom action files in public GitHub repo

You can use a custom action within a new GitHub repo workflow, as shown next. Public repo actions can be used in any repo.
on: [workflow_dispatch]
jobs:
  custom_js_action_job:
    runs-on: ubuntu-latest
    name: Custom js Action Demo
    steps:
    - name: First js action step
      id: myjsaction
      uses: chamindac/demojsaction@v1
      with:
        name-of-you: 'Pushpa'
    # Use the output from the `myjsaction` step
    - name: Get the output message time
      run: echo "The time was ${{ steps.myjsaction.outputs.time }}"
The action step prints the message with the input name (see Figure 9-12).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig12_HTML.jpg
Figure 9-12

Print message in custom action

Next, the message time is printed as output obtained from the custom action step (see Figure 9-13).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig13_HTML.jpg
Figure 9-13

Print message time

You have created an action in a public repo and used it in another GitHub repo workflow. However, if you create a custom action in a private GitHub repo, it is only usable in the same repo. You need to check out the repo and state to use its root if the action is in the root of the repo, as shown next.
on: [workflow_dispatch]
jobs:
  custom_js_action_job:
    runs-on: ubuntu-latest
    name: Custom js Action Demo
    steps:
      # To use this repository's private action,
      # you must check out the repository
      - name: Checkout
        uses: actions/checkout@v2
      - name: Custom js Action Step
        uses: ./ # Uses an action in the root directory
        id: myjsaction
        with:
          name-of-you: 'Pushpa'
      # Use the output from the `myjsaction` step
      - name: Get the output time
        run: echo "The time was ${{ steps.myjsaction.outputs.time }}"

This section discussed developing a custom JavaScript action to enhance GitHub workflows.

Composite Run Steps Action

Composite actions let you combine multiple run steps in a single action. Let’s create a simple composite action to understand how it works. As a prerequisite, let’s create a public repo and clone it to a local machine. Next, open it in Visual Studio Code. Create a folder named mycompositeaction in the repo. Add a file named helloworld.sh and enter the echo "Hello World! This is my composite action" (see Figure 9-14).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig14_HTML.jpg
Figure 9-14

helloworld.sh

You must make the helloworld.sh executable. For this, you can use chmod +x hellowold.sh on a Linux machine. However, if you are using a Windows machine, you need to use the following commands to make the helloworld.sh executable and let Git notify with it (also see Figure 9-15).
git add helloworld.sh
git update-index --chmod=+x helloworld.sh
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig15_HTML.jpg
Figure 9-15

Make helloworld.sh executable

Next let’s add an action.yml with the custom action’s metadata. It takes two inputs (your name and country), greets you, and prints.
name: 'Hello World'
description: 'saying hello world to composite action'
inputs:
  your-name:  # id of input
    description: 'Your Name'
    required: true
    default: 'Chaminda'
runs:
  using: "composite"
  steps:
    - run: echo Hello ${{ inputs.your-name }}.
      shell: bash
    - run: ${{ github.action_path }}/helloworld.sh
      shell: bash
Next, add action.yml, git, commit, and push (see Figure 9-16).
git add action.yml
git commit -m "my composite action added"
git tag -a -m "my composite action release" v1
git push --follow-tags
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig16_HTML.jpg
Figure 9-16

Commit and push

You can test the composite action using the following workflow. Notice that we are referring to an action in a repo folder. This way, you can keep multiple actions in the same repo.
on: [workflow_dispatch]
jobs:
  composite_action_job:
    runs-on: ubuntu-latest
    name: My composite action use
    steps:
    - name: First composite action step
      id: mycompositeaction
      uses: chamindac/CustomActions/mycompositeaction@v1
      with:
        your-name: 'Pushpa'
The composite action executed in the workflow prints the input name and the message from helloworld.sh (see Figure 9-17).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig17_HTML.jpg
Figure 9-17

Composite action in a workflow

Docker Container Action

Docker container actions let you develop your actions using any language because it runs on an image selected by you. Let’s use the composite run steps action repo for the container action.

First, create a folder named mycontaineraction in the repo folder's root (see Figure 9-18).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig18_HTML.jpg
Figure 9-18

Folder for container action

Next, add a Docker file and define the image and the code file to copy to the container root for execution (see Figure 9-19).
# Container image to run the code
FROM alpine:3.10
# Copy the code file to the container root
COPY mydockeractionsample.sh /mydockeractionsample.sh
# execute code file when container starts
ENTRYPOINT ["/mydockeractionsample.sh"]
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig19_HTML.jpg
Figure 9-19

Dockerfile

Next, add the code file to the repo. The following code prints “Hello” and your name and outputs the message time (see Figure 9-20).
#!/bin/sh -l
echo "Hello $1"
time=$(date)
echo "::set-output name=timeofmessage::$time"
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig20_HTML.jpg
Figure 9-20

Action code to execute in container

Next, add the following action metadata file (also see Figure 9-21).
name: 'Container Action'
description: 'Container action demo'
inputs:
  your-name:  # id of input
    description: 'your name'
    required: true
    default: 'Chaminda'
outputs:
  time: # id of output
    description: 'The time of the message'
runs:
  using: 'docker'
  image: 'Dockerfile'
  args:
    - ${{ inputs.your-name }}
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig21_HTML.jpg
Figure 9-21

Metadata file

Next, add the files to git.
git add action.yml mydockeractionsample.sh Dockerfile
You must enable the execution for mydockeractionsample.sh file . In Linux, you can use chmod +x mydockeractionsample.sh. However, in Windows, use the following command.
git update-index --chmod=+x mydockeractionsample.sh
Next, commit, tag, and push the container action to the repo.
git commit -m "My first container action"
git tag -a -m "My first container action release" v3
git push --follow-tags
Use a workflow to test the new container action, as shown next.
on: [workflow_dispatch]
on: [workflow_dispatch]
jobs:
  container_action_job:
    runs-on: ubuntu-latest
    name: container action demo
    steps:
    - name: First container action step
      id: mycontaineraction
      uses: chamindac/CustomActions/mycontaineraction@v3
        with:
          your-name: 'Pushpa'
      # Use the output from the `mycontaineraction` step
      - name: Get the output time
        run: echo "The time was ${{ steps.mycontaineraction.outputs.timeofmessage }}"
The executed workflow successfully uses the container action (see Figure 9-22).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig22_HTML.jpg
Figure 9-22

Container action used in workflow

Publishing Custom Actions

You can publish the custom actions you created in the GitHub Marketplace for others to use. However, you need to satisfy the following requirements in your action to allow it to be published in the GitHub Marketplace.
  • The repo must be public.

  • The repo can only contain a single action. In the previous section, you created a JavaScript action as a single action in the repo. Therefore, you can publish it to the marketplace. However, the container and composite step run actions were created in the same repo, which prevents you from publishing them to the marketplace.

  • An action.yml metadata file must be in the root of the repo.

  • The name of the action cannot have a name already used in the marketplace.

Let’s try to publish the JavaScript action in the Marketplace. When you open the repo, you see that you can draft a release to make your action discoverable in the GitHub Marketplace (see Figure 9-23).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig23_HTML.jpg
Figure 9-23

Draft a release

You can tag a release by accepting the Marketplace agreement before publishing (see Figure 9-24).
../images/502534_1_En_9_Chapter/502534_1_En_9_Fig24_HTML.jpg
Figure 9-24

Agreement

You must complete two-factor authentication before publishing an action to the marketplace.

Summary

This chapter explored developing custom actions for GitHub Actions workflows using JavaScript, containers, or composite step-run actions. Custom actions interact with GitHub or external APIs, further enhancing your workflows’ capabilities.

The next chapter looks at a few quick-start examples of GitHub Actions.