DEV Community

Cover image for Avoid Duplication! GitHub Actions Reusable Workflows
Davide 'CoderDave' Benvegnù
Davide 'CoderDave' Benvegnù

Posted on

Avoid Duplication! GitHub Actions Reusable Workflows

Thanks to the new GitHub Actions feature called "Reusable Workflows" you can now reference an existing workflow with a single line of configuration rather than copying and pasting from one workflow to another.

Basically GitHub Actions Templates on steroids!

What Are Reusable Workflows

So, Reusable Workflows in GitHub Actions. Thanks to this feature you can now reference an entire Actions workflow in another workflow, like if it were a single action.

This new feature builds on top of the Composite Actions introduced a while back. If you don't know what Composite Actions are, check this post or this video, but in short they are one or more steps packaged together which can be then referenced in an Actions workflows by a single line.

Reusable Workflows extend this concept, allowing you to reference an entire workflow in another one. If Composite Actions can be thought of as Templates, Reusable Workflows is on another new level.

Right, let's see how to create a reusable workflow.

Video

As usual, if you are a visual learner, or simply prefer to watch and listen instead of reading, here you have the video with the whole explanation and demo, which to be fair is much more complete than this post.

Link to the video: https://youtu.be/lRypYtmbKMs

If you rather prefer reading, well... let's just continue :)

Create a Reusable Workflow

Reusable workflows are normal Actions YAML files, and as such they have to reside in the .github/workflows folder in the root of a repo.

The only particular thing they have to have is a special trigger:

on:
  workflow_call:
Enter fullscreen mode Exit fullscreen mode

The workflow file can also have different triggers, but to make it reusable one of those must be the workflow_call.

You can also pass data to a reusable workflow, via the trigger parameters which can be of 2 types:

  • inputs
  • secrets

The inputs are used to pass normal data (aka not sensitive information):

    inputs:
      image_name:
        required: true
        type: string
      tag: 
        type: string
Enter fullscreen mode Exit fullscreen mode

In the example above, where I want to use a reusable workflow as template to build and push a Docker Image to a registry, we can see that we have 2 inputs of type string, with one required and one not required.

Note: if a required input has not been passed to the reusable workflow, it will fail

Other available types are boolean and number.

The secrets, instead, as the name says, are used to pass secret values to the workflow:

    secrets:
      registry_username:
        required: true
      registry_password:
        required: true
Enter fullscreen mode Exit fullscreen mode

In this case you can see that there is no type, every secret is treated as string.

Finally, you can use those parameters in your workflow by using {{inputs.NAME_OF_THE_INPUT}} and {{secrets.NAME_OF_THE_SECRET}}.

So, in the abovementioned example where I want to use a reusable workflow to build and push a Docker image to a registry, the reusable workflow will look something like this:

name: Create and Publish Docker Image

on:
  workflow_call:
    inputs:
      image_name:
        required: true
        type: string
      tag: 
        type: string
    secrets:
      registry_username:
        required: true
      registry_password:
        required: true

jobs:
  build:
    runs-on: ubuntu-latest

    steps:      
      - uses: actions/checkout@v2

      - name: Setup BuildX
        uses: docker/setup-buildx-action@v1

      - name: Login to the Registry
        uses: docker/login-action@v1
        with:
          username: ${{secrets.registry_username}}
          password: ${{secrets.registry_password}}

      - name: Set the tag
        run: |
          if [ -z "${{inputs.tag}}" ]
          then
            echo "final_tag=latest" >> $GITHUB_ENV
          else
            echo "final_tag=${{inputs.tag}}" >> $GITHUB_ENV
          fi

      - name: Build and Push the Image
        uses: docker/build-push-action@v2
        with:
          context: .
          push: true
          tags: ${{secrets.registry_username}}/${{inputs.image_name}}:${{env.final_tag}}


  do-something-else:
    runs-on: ubuntu-latest

    steps:
    - run: echo "Hello"
Enter fullscreen mode Exit fullscreen mode

Also note that reusable workflows can have multiple jobs, as you can see in the example (where the do-something-else does nothing, but it is to show it off)

Easy right? One thing to keep in mind is that if the reusable workflow has other triggers apart from the workflow_call you may want to make sure it doesn't accidentally run multiple times.

Now that we have our reusable workflow, let's see how to use it in another workflow. And stay with me until the end because I will talk about the limitations of reusable workflows and when they can be useful.

Using a Reusable Workflow

Now that we have our reusable workflow ready, it is time to use it in another workflow.

To do so, just add it directly in a job of your workflow with this syntax:

 job_name:
    uses: USER_OR_ORG_NAME/REPO_NAME/.github/workflows/REUSABLE_WORKFLOW_FILE.yml@TAG_OR_BRANCH
Enter fullscreen mode Exit fullscreen mode

Let's analyse this:

  1. You create a job with no steps
  2. You don't add a runs-on clause, because it is contained in the reusable workflow
  3. You reference it as uses passing:
  • the name of the user or organization that owns the repo where the reusable workflow is stored
  • the repo name
  • the base folder
  • the name of the reusable workflow yaml file
  • and the tag or the branch where the file is store (if you haven't created a tag/version for it)

In my real example above, this is how I'd reference it in a job called docker:

  docker:
    uses: n3wt0n/ReusableWorkflow/.github/workflows/buildAndPublishDockerImage.yml@main
Enter fullscreen mode Exit fullscreen mode

Now of course we have to pass the parameters. Let's start with the inputs:

    with:
      image_name: my-awesome-app
      tag: $GITHUB_RUN_NUMBER
Enter fullscreen mode Exit fullscreen mode

As you can see, we just use the with clause, and we specify the name of the inputs.

Needless to say, the names have to be the same as the ones in the reusable workflow definition.

For the secrets, instead, we use a new secrets section:

    secrets:
      registry_username: ${{secrets.REGISTRY_USERNAME}}
      registry_password: ${{secrets.REGISTRY_PASSWORD}}
Enter fullscreen mode Exit fullscreen mode

And this is it. So the complete example would look like this (you can find it here):

# This is a basic workflow to showcase the use of Reusable Workflows

name: Reusable Workflow user

on:
  workflow_dispatch:

jobs:
  do-it:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v2

      - name: Run a one-line script
        run: echo Hello, world!

  docker:
    uses: n3wt0n/ReusableWorkflow/.github/workflows/buildAndPublishDockerImage.yml@main
    with:
      image_name: my-awesome-app
      tag: $GITHUB_RUN_NUMBER
    secrets:
      registry_username: ${{secrets.REGISTRY_USERNAME}}
      registry_password: ${{secrets.REGISTRY_PASSWORD}}
Enter fullscreen mode Exit fullscreen mode

Once again, as you can see the caller workflow can have multiple jobs as well.

If we run the workflow, this is what we get:

Workflow run

You can see in the image that we have the logs for the do-it job that is present in the caller, and then for both the jobs in the reusable workflow.
Since those 2 jobs are run within the docker job in the caller workflow, they are referenced in the log as docker / build and docker /do-something-else.

But apart from that, the logs are complete:

Logs

We get the full details of everything that has happened.

Limitations and Caveats

So, let's start with a few notes. First, remember that the Reusable Workflows are currently in beta, so things might change by the time they go GA.

Second, for a workflow to be able to use it, a reusable workflow must be stored in the same repo as the call, or in a public repo, or yet in an internal repo with settings that allow it to be accessed.

Repo access settings

Let's talk now about limitations. As direct result of what we have just said, reusable workflows stored in a private repository can be used only by other workflows in the same repo.

Also, Reusable workflows cannot call and consume other Reusable workflows.

Finally, and this is big one you need to remember, environment variables set at workflow level in the caller workflow are not passed to the reusable workflow. So if you need use any of those variables in the reusable workflow, you'll have to pass them to the workflow via the parameters as I've shown above.

Conclusions

Reusing workflows avoids duplication. This makes workflows easier to maintain and allows you to create new workflows more quickly by building on the work of others, just as you do with actions.

Workflow reuse also promotes best practices by helping you to use workflows that are well designed, have already been tested, and have been proved to be effective. Your organization can build up a library of reusable workflows that can be centrally maintained.

Let me know in the comment section below what you think about these new reusable workflows, if and how you plan to use them, and if there is any feature that you think is missing.

You may also want to watch this video where I talk about the Composite Actions as templates.

Like, share and follow me 🚀 for more content:

📽 YouTube
Buy me a coffee
💖 Patreon
📧 Newsletter
🌐 CoderDave.io Website
👕 Merch
👦🏻 Facebook page
🐱‍💻 GitHub
👲🏻 Twitter
👴🏻 LinkedIn
🔉 Podcast

Buy Me A Coffee

Discussion (0)