Using Composite Actions with GitHub Actions

Sep 11, 2021 00:00 ยท 540 words ยท 3 minute read

Basics ๐Ÿ”—

When developing new automations it’s best to make each component as modular and reusable as possible. When using GitHub Actions, Composite Actions may be your answer for quick development and reusability.

Composite Actions allow you to execute multiple shell steps by calling the Action. These shell steps can be bash, python, nodejs, or powershell. This functionality can be very powerful when you have logic you often want to reuse.

Take for example, running a small Python script that requires a dependency in an Actions Workflow. You could create a Composite Action that:

  • Using the bash shell, executes pip3 install <package>
  • Using the python shell, include a Python script in-line
  • OR, using the bash shell, execute a Python script in a file

That might look something like:

name: 'run-python-script'
description: 'Installs a package and runs a python script'
runs:
  using: "composite"
  steps:
    - name: Install boto3
      shell: bash
      run: pip3 install boto3

    - name: Run Python Script
      shell: python
      run: |
        print("hello world")
      

It looks much like a Workflow YAML file but is a little different. The important part to note is using: composite. Additionally, only the run command can be used to execute code in a specified shell.

There is one huge caveat for using Composite Actions - you can’t call another Action.

How and Where ๐Ÿ”—

With that concept in mind, you may want to create many Composite Actions. But, how and where do you put them?

Each GitHub Action must reside in its own action.yml file in its own directory. I’d suggest creating a repository just for reusable Actions. In that repo, or your primary repo, create an /actions/ directory to store the Actions. Put each Action in a separate directory.

It should look like this:

actions
โ”œโ”€โ”€ another-action
โ”‚ย ย  โ””โ”€โ”€ action.yml
โ””โ”€โ”€ first-action
    โ””โ”€โ”€ action.yml

Inputs and Outputs ๐Ÿ”—

Just like a function or method in programming languages, Composite Actions provide for inputs (parameters) or outputs (return values).

In this example, we have an input parameter named name and an output called greeting. Using the bash shell, we return Hello <name> as the greeting output.

name: 'say-hello'
description: 'Returns a greeting output with a greeting to the input name'
inputs:
  name:
    required: true
outputs:
  greeting:
    required: true
runs:
  using: "composite"
  steps:
    - name: say-hello
      shell: bash
      env:
        INPUT_NAME: ${{ inputs.name }}
      run: |
        GREETING="Hello $INPUT_NAME"
        echo "::set-output name=greeting::$GREETING"

We need to do a few things to use inputs and outputs.

First, the inputs and outputs stanzas must be included in the YAML.

Then, to get the input values as environment variables, we must specify under the env key. This is because the inputs are not automatically provided as environment variables when using Composite Actions, unlike Workflows.

Finally, to use the output, we must print a special string to stdout. The string must be in the format of ::set-output name=<NAME>::<VALUE>. This can be accomplished using print() in Python, console.log() in JavaScript, or echo in bash.

Calling Composite Actions ๐Ÿ”—

You can call a Composite Action from a Workflow just like any other Action. If you’re storing them in a private repository, you need to check the repository out first.

After the Composite Action has run, the outputs of the Action are available as steps.<step-name>.outputs.<output-name> and can be used in any future steps.

In this example we’re always using Doug as the value of name, but it could just as well be an input to the workflow_dispatch event.

name: Greetings
on: [workflow_dispatch]
jobs:
  greeting:
    runs-on: ubuntu-latest
    steps:
    - name: Checkout this repo
      uses: actions/checkout@v2

    - name: Call Composite Action say-hello
      uses: ./actions/say-hello
      id: say-hello
      with:
        name: 'Doug'

    - name: echo output from say-hello
      run: |
        echo ${{ steps.say-hello.outputs.greeting }}