Featured image of post How to reuse workflows and steps in GitHub Actions (2024)

How to reuse workflows and steps in GitHub Actions (2024)

How to create reusable workflows and reusable steps in GitHub Actions

Introduction

GitHub Actions is a way to automate your software development workflows. The approach is similar to CI/CD tools like Jenkins, CircleCI, and TravisCI. However, GitHub Actions are built into GitHub.

High level diagram of GitHub Actions

High level diagram of GitHub Actions

The entry point for GitHub Actions is the .github/workflows directory in your repository. This directory contains one or more YAML files that define your workflows. A workflow is an automated process made up of one or more jobs. Each job runs on a separate runner. A runner is a server that runs the job. A job contains one or more steps. Each step runs a separate command.

Why reuse?

Code reuse is a fundamental principle of software development. Reusing GitHub Actions code allows you to:

  • Improve maintainability by keeping common code in one place and reducing the amount of code
  • Increase consistency since multiple workflows can use the same code
  • Promote best practices
  • Increase productivity
  • Reduce errors

Examples of reusable GitHub Actions code include:

  • Code signing
  • Uploading artifacts to cloud services
  • Security checks
  • Notifications and reports
  • Data processing
  • and many others

Reusable workflows

A reusable workflow replaces a job in the main workflow.

GitHub Actions reusable workflow

GitHub Actions reusable workflow

A reusable workflow may be shared across repositories and run on a different platform than the main workflow.

For file sharing, ‘build artifacts’ must be used to share files with the main workflow. The reusable workflow does not inherit environment variables. However, it accepts inputs and secrets from the calling workflow and may use outputs to pass data back to the main workflow.

Here is an example of a reusable workflow. It uses the same schema as a regular workflow.

name: Reusable workflow

on:
  workflow_call:
    inputs:
      reusable_input:
        description: 'Input to the reusable workflow'
        required: true
        type: string
      filename:
        required: true
        type: string
    secrets:
      HELLO_WORLD_SECRET:
        required: true
    outputs:
      # Map the workflow output(s) to job output(s)
      reusable_output:
        description: 'Output from the reusable workflow'
        value: ${{ jobs.reusable-workflow-job.outputs.job_output }}

defaults:
  run:
    shell: bash

jobs:
  reusable-workflow-job:
    runs-on: ubuntu-20.04
    # Map the job output(s) to step output(s)
    outputs:
      job_output: ${{ steps.process-step.outputs.step_output }}
    steps:
      - name: Process reusable input
        id: process-step
        env:
          HELLO_WORLD_SECRET: ${{ secrets.HELLO_WORLD_SECRET }}
        run: |
          echo "reusable_input=${{ inputs.reusable_input }}"
          echo "HELLO_WORLD_SECRET=${HELLO_WORLD_SECRET}"
          echo "step_output=${{ inputs.reusable_input }}_processed" >> $GITHUB_OUTPUT          
      - uses: actions/download-artifact@v4
        with:
          name: input_file
      - name: Process file
        run: |
          echo "Processing file: ${{ inputs.filename }}"
          echo "file processed" >> ${{ inputs.filename }}          
      - uses: actions/upload-artifact@v4
        with:
          name: output_file
          path: ${{ inputs.filename }}

The reusable workflow is triggered on: workflow_call. It accepts an input called reusable_input and generates an output called reusable_output. It also downloads an artifact called input_file, processes a file, and uploads an artifact called output_file.

The main workflow calls the reusable workflow using the uses keyword.

  job-2:
    needs: job-1
    # We do not need to check out the repository to use the reusable workflow
    uses: ./.github/workflows/reusable-workflow.yml
    with:
      reusable_input: "job-2-input"
      filename: "input.txt"
    secrets:
      # Can also implicitly pass the secrets with: secrets: inherit
      HELLO_WORLD_SECRET: TERCES_DLROW_OLLEH

A successful run of the main workflow looks like this on GitHub:

GitHub Actions reusable workflow success

GitHub Actions reusable workflow success

Reusable steps (composite action)

Reusable steps replace a regular step in a job. We will use a composite action for reusable steps in our example.

GitHub Actions reusable steps (composite action)

GitHub Actions reusable steps (composite action)

Like a reusable workflow, a composite action may be shared across repositories, it accepts inputs, and it may use outputs to pass data back to the main workflow.

Unlike a reusable workflow, a composite action inherits environment variables. However, it does not inherit secrets. Secrets must be passed explicitly as inputs or environment variables. Also, there is no need to use ‘build artifacts’ to share files since the reusable steps run on the same runner and in the same work area as the main job.

Here is an example of a composite action. It uses a different schema than a workflow. Also, the file must be named action.yml or similar.

name: Reusable steps (AKA composite action)
description: Demonstrate how to use reusable steps in a workflow
# Schema: https://json.schemastore.org/github-action.json

inputs:
  reusable_input:
    description: 'Input to the reusable workflow'
    required: true
  filename:
    required: true
outputs:
  # Map the action output(s) to step output(s)
  reusable_output:
    description: 'Output from the reusable workflow'
    value: ${{ steps.process-step.outputs.step_output }}

runs:
  using: 'composite'
  steps:
    - name: Process reusable input
      id: process-step
      # Shell must explicitly specify the shell for each step. https://github.com/orgs/community/discussions/18597
      shell: bash
      run: |
        echo "reusable_input=${{ inputs.reusable_input }}"
        echo "HELLO_WORLD_SECRET=${HELLO_WORLD_SECRET}"
        echo "step_output=${{ inputs.reusable_input }}_processed" >> $GITHUB_OUTPUT        
    - name: Process file
      shell: bash
      run: |
        echo "Processing file: ${{ inputs.filename }}"
        echo "file processed" >> ${{ inputs.filename }}        

The composite action is called via the uses setting on a step. Our action accepts an input called reusable_input and generates an output called reusable_output. It also processes a file called filename.

The following code snippet shows how to use the composite action in a job.

  - name: Use reusable steps
    id: reusable-steps
    uses: ./.github/reusable-steps # To use this syntax, we must have the repository checked out
    with:
      reusable_input: "job-2-input"
      filename: "input.txt"
    env:
      HELLO_WORLD_SECRET: TERCES_DLROW_OLLEH

A successful run of the main workflow with reusable steps looks like this on GitHub:

GitHub Actions composite action success

GitHub Actions composite action success

For a reusable TypeScript action example, see the How to create a custom GitHub Action using TypeScript article.

Conclusion

Reusable workflows and steps are powerful tools for improving the maintainability, consistency, and productivity of your GitHub Actions. They allow you to reuse code across repositories and workflows and promote best practices. They are a great way to reduce errors and increase productivity.

For larger units of work, a reusable workflow should be used. A composite action should be used for smaller units of work that may run on the same runner and share the same work area.

Example code on GitHub

The example code is available on GitHub at: https://github.com/getvictor/github-reusable-workflows-and-steps

GitHub Actions reusable workflows and steps video

Note: If you want to comment on this article, please do so on the YouTube video.