
Mastering Reusable Workflows in GitHub Actions
- Published on
- Authors
- Author
- Ram Simran G
- twitter @rgarimella0124
In the world of DevOps, efficiency comes from not reinventing the wheel. If you’ve used Jenkins, you’re probably familiar with shared libraries—those handy collections of reusable pipeline code that teams can pull into their jobs to standardize processes and reduce duplication. GitHub Actions offers something similar through reusable workflows. These allow you to define a workflow once in a YAML file and call it from multiple other workflows across repositories. This promotes consistency, makes maintenance easier, and speeds up development.
In this lengthy blog post, we’ll explore reusable workflows in depth. We’ll start with the basics, walk through the process of creating and invoking them, discuss mandatory and non-mandatory parameters, and provide sample code. We’ll also cover advanced topics like using a central repository for all your workflows and how to reference them from other repos. By the end, you’ll have everything you need to implement this in your projects, complete with an example graph in Markdown to visualize the flow.
Why Use Reusable Workflows? The Jenkins Analogy
In Jenkins, shared libraries let you store common pipeline steps, like building a Docker image or running security scans, in a central place. You load them into your Jenkinsfile with a simple directive, passing parameters as needed. This keeps your pipelines DRY (Don’t Repeat Yourself) and ensures everyone follows best practices.
GitHub Actions reusable workflows work similarly but are native to GitHub’s ecosystem. Instead of scripting in Groovy, you use YAML. A reusable workflow is a standalone YAML file that other workflows can “call” like a function. It’s triggered by the workflow_call
event, and you can pass inputs, secrets, and even get outputs back. This is perfect for standardizing CI/CD steps across teams or projects.
Benefits include:
- Reduced Duplication: Write once, use everywhere.
- Easier Updates: Change the reusable workflow, and all callers benefit.
- Better Security: Centralize sensitive logic, like secret handling.
- Scalability: Ideal for monorepos or multi-repo setups in organizations.
However, there are limits: You can’t nest more than four levels of workflows, and reusable files must be in the .github/workflows
directory—no subfolders allowed. Also, callers and callees must be in the same organization for private repos, unless you adjust permissions.
The Process: Creating a Reusable Workflow
Creating a reusable workflow is straightforward. Follow these steps:
Set Up a Repository: Create or use an existing GitHub repo. Place your workflow YAML in
.github/workflows/
. For shared use, dedicate a repo just for workflows (more on this later).Define the Workflow: Start with the
on
trigger set toworkflow_call
. This makes it callable.Add Inputs and Secrets: Inputs are like parameters—mandatory (required: true) or optional (required: false). Types can be string, number, or boolean. Secrets are passed securely and are often mandatory for auth tasks.
Define Jobs and Steps: Build your logic inside jobs, just like a regular workflow. Reference inputs with
${{ inputs.name }}
.Handle Outputs (Optional): If the workflow produces data for the caller, define outputs at the workflow level, mapping from job outputs.
Commit and Push: Once saved, it’s ready to be called.
Best practices: Keep workflows focused on one task, document inputs/outputs in the YAML, and use descriptive names.
Sample Code: A Reusable Workflow for Building and Testing
Here’s a sample reusable workflow called build-test.yml
. It handles building a Node.js app, running tests, and outputting coverage stats. It has mandatory inputs (like repo path) and optional ones (like Node version).
name: Reusable Build and Test
on:
workflow_call:
inputs:
repo-path:
description: 'Path to the repository root'
required: true
type: string
default: '.'
node-version:
description: 'Node.js version to use'
required: false
type: string
default: '20'
run-tests:
description: 'Whether to run tests'
required: false
type: boolean
default: true
secrets:
npm-token:
description: 'NPM auth token for private packages'
required: false
outputs:
coverage:
description: 'Test coverage percentage'
value: ${{ jobs.build-test.outputs.coverage }}
jobs:
build-test:
runs-on: ubuntu-latest
outputs:
coverage: ${{ steps.test.outputs.coverage }}
steps:
- name: Checkout Code
uses: actions/checkout@v4
with:
path: ${{ inputs.repo-path }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
registry-url: 'https://registry.npmjs.org'
- name: Install Dependencies
run: npm ci
working-directory: ${{ inputs.repo-path }}
env:
NODE_AUTH_TOKEN: ${{ secrets.npm-token }}
- name: Build Application
run: npm run build
working-directory: ${{ inputs.repo-path }}
- name: Run Tests
id: test
if: ${{ inputs.run-tests }}
run: |
npm test -- --coverage
COVERAGE=$(cat coverage/lcov-report/index.html | grep -oP '(?<=<span class="strong">)d+.d+(?=% <span class="quiet">Lines</span>)')
echo "coverage=$COVERAGE" >> $GITHUB_OUTPUT
working-directory: ${{ inputs.repo-path }}
In this example:
- Mandatory Parameter:
repo-path
must be provided; it hasrequired: true
. - Non-Mandatory Parameters:
node-version
andrun-tests
have defaults, so they’re optional.npm-token
secret is optional too. - Output:
coverage
is produced if tests run, for use in the caller.
Invoking the Reusable Workflow: The Caller Side
To invoke (call) a reusable workflow:
Create a Caller Workflow: In your project’s
.github/workflows/
directory, make a new YAML file, likeci-pipeline.yml
.Trigger It Normally: Use standard triggers like
push
orpull_request
.Call the Reusable One: In a job, use
uses: {owner}/{repo}/.github/workflows/{filename}@{ref}
. The ref can be a branch (e.g., main), tag (e.g., v1), or SHA.Pass Parameters: Under
with:
, provide inputs. For secrets, usesecrets:
orsecrets: inherit
to pass all from the caller.Use Outputs: Reference them with
${{ needs.job-name.outputs.name }}
.Handle Permissions: Ensure the caller has read access to the reusable repo. For private repos, use organization-level sharing.
Invoking triggers the reusable workflow as part of the caller’s execution. If it fails, the caller fails too.
Sample Code: A Caller Workflow Invoking the Reusable One
Here’s ci-pipeline.yml
in your app repo, calling the above reusable workflow. It also includes a deploy job that uses the output.
name: CI Pipeline with Reusable Workflow
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build-and-test:
uses: your-org/shared-workflows/.github/workflows/build-test.yml@main
with:
repo-path: '.' # Mandatory input
node-version: '18' # Optional, overriding default
run-tests: true # Optional
secrets:
npm-token: ${{ secrets.NPM_TOKEN }} # Optional secret
deploy:
needs: build-and-test
runs-on: ubuntu-latest
if: ${{ needs.build-and-test.outputs.coverage > 80 }} # Using output
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Deploy to Production
run: echo "Deploying with coverage ${{ needs.build-and-test.outputs.coverage }}%"
# Add actual deploy steps here
To invoke this pipeline:
- Push code to the main branch or open a PR—it runs automatically.
- Manually trigger via GitHub UI with
workflow_dispatch
if added toon
. - From CLI: Use
gh workflow run ci-pipeline.yml
with GitHub CLI.
If using secrets: inherit
, the caller passes all its secrets without listing them—great for simplicity, but be cautious with exposure.
Mandatory vs. Non-Mandatory Parameters
- Mandatory (Required): Set
required: true
in the reusable workflow. The caller must provide these, or the workflow fails. Example: A build path that’s essential. - Non-Mandatory (Optional):
required: false
, often with adefault
value. Callers can omit or override. Example: A debug flag defaulting to false.
Always document them with description
for clarity. Types enforce data correctness—e.g., a boolean input rejects strings.
Using a Central Repository for All Workflows
Yes, it’s common to have a dedicated repository (e.g., your-org/shared-workflows
) housing all reusable workflows in one .github/workflows/
folder. This acts like a “library” repo, similar to Jenkins’ global shared libraries.
How it works:
- Setup: Create the repo, add YAML files like
build-test.yml
,deploy.yml
, etc., all in.github/workflows/
. No subfolders—GitHub doesn’t support them for workflows. - Versioning: Use tags (e.g., v1.0) for stable versions. Callers reference specific refs to avoid breaking changes.
- Permissions: Make it private for orgs, or public for open-source. Ensure calling repos have access via org membership or PATs.
- Invoking from Your Repo: In your app’s workflow, use
uses: your-org/shared-workflows/.github/workflows/build-test.yml@v1.0
. This pulls from the central repo at runtime. - Updates: Push changes to the central repo. Callers auto-use the new version if referencing a branch like main, or update the ref manually for tags.
- Best Practices: Include a README in the central repo documenting each workflow’s inputs/outputs. Use semantic versioning for tags.
This central approach is ideal for enterprises with many teams. For example, repos like GitHub’s own actions/reusable-workflows
provide templates for action development.
Advanced Topics: Matrix Strategies, Nesting, and Limitations
- Matrix Strategies: Reusable workflows support matrices for parallel runs (e.g., testing multiple OS). Define in the caller or reusable.
- Nesting: Call one reusable from another, up to 4 levels deep.
- Limitations: No environment variables in
workflow_call
; max 100 reusable calls per workflow; private reusables require same org. - Error Handling: If a reusable fails, debug via GitHub’s action logs. Use
continue-on-error
in callers for non-critical calls.
Visualizing the Flow: An Example Graph
Here’s a Markdown graph showing a caller workflow invoking multiple reusables from a central repo, with parameters and outputs.
+-------------------+ +-----------------------------+
| Caller Repo | | Central Shared Repo |
| - ci-pipeline.yml | --> | - .github/workflows/ |
| (Your App) | | - build-test.yml |
+-------------------+ | - deploy.yml |
+-----------------------------+
|
v
+-------------------+ +-------------------+ +-------------------+
| Triggers | --> | Call Reusable | --> | Pass Parameters |
| - Push/PR | | - uses: ...@ref | | - Mandatory: path |
| - Workflow Dispatch| | | | - Optional: version|
+-------------------+ +-------------------+ | - Secrets: token |
|
v
+-------------------+ +-------------------+ +-------------------+
| Execute Jobs | --> | Handle Outputs | --> | Next Steps |
| - Build & Test | | - Coverage % | | - Deploy if OK |
| - Reference Inputs| | | | - Rollback on Fail|
+-------------------+ +-------------------+ +-------------------+
|
v
+-------------------+
| Environments |
| - Dev/Staging/Prod|
+-------------------+
This graph illustrates the flow: Triggers in the caller invoke reusables from the central repo, passing params, executing logic, and using outputs for decisions.
Conclusion: Elevate Your GitHub Actions with Reusables
Reusable workflows bring the power of shared libraries to GitHub Actions, making your automation modular and maintainable. By creating them with clear parameters, invoking via uses
, and centralizing in a dedicated repo, you streamline processes across projects. Start with a simple one, like the build-test example, and expand. Experiment in a test repo—GitHub’s free tier makes it easy. With these tools, your team can focus on innovation, not repetition. Happy automating!
Cheers,
Sim