Reusable workflows are GitHub Actions’ answer to code duplication, letting you define a workflow once and call it from multiple repositories.
Let’s say you have a standard CI process that builds, tests, and lints your Python projects. Instead of copying and pasting that workflow file into every new Python project, you can create a reusable workflow in a central repository and then call it from your individual project repositories.
Here’s a simple example of a reusable workflow that just echoes a message. This would live in a file like .github/workflows/greet.yml in your central repository (let’s call it my-shared-workflows).
# .github/workflows/greet.yml
name: Greet Workflow
on:
workflow_call:
inputs:
name:
description: 'Who to greet'
required: true
type: string
default: 'World'
jobs:
greet:
runs-on: ubuntu-latest
steps:
- name: Send greeting
run: echo "Hello ${{ inputs.name }}!"
Now, in a different repository (say, my-python-app), you can create a workflow file (e.g., .github/workflows/main.yml) that calls this reusable workflow:
# .github/workflows/main.yml
name: Main CI
on:
push:
branches: [ main ]
jobs:
build-and-test:
uses: my-org/my-shared-workflows/.github/workflows/greet.yml@v1
with:
name: ${{ github.actor }}
When main.yml runs on a push event in my-python-app, it will trigger the greet job defined in the greet.yml workflow located in my-shared-workflows. The uses keyword points to the remote workflow, and the with clause passes the name input.
This is incredibly powerful for standardizing practices. Think about security scanning, dependency updates, or deployment steps. You define it once, ensure it’s secure and correct, and then all your projects can leverage it.
The on: workflow_call: trigger is what makes a workflow reusable. It signifies that this workflow isn’t meant to be triggered by repository events like push or pull_request directly, but rather by another workflow explicitly calling it. You can define inputs here, which are parameters that the calling workflow must provide. These inputs are strongly typed and can have defaults, making them robust.
When you call a reusable workflow using uses: owner/repository/.github/workflows/path/to/workflow.yml@ref, the ref can be a tag, a branch, or a commit SHA. Using a tag (like @v1) is highly recommended for stability, as it pins the reusable workflow to a specific, released version, preventing unexpected changes from affecting your consuming repositories.
The jobs.<job_id>.uses syntax is how you invoke a reusable workflow. You can pass inputs using jobs.<job_id>.with. If the reusable workflow has secrets defined in its on: workflow_call: block, you can also pass secrets to it using jobs.<job_id>.secrets. These secrets are passed explicitly, rather than being automatically available, which enhances security.
Consider a more complex reusable workflow for building a Python application.
# .github/workflows/python-build.yml
name: Python Build and Test
on:
workflow_call:
inputs:
python-version:
description: 'Python version to use'
required: false
type: string
default: '3.10'
lint-only:
description: 'Run only linting checks'
required: false
type: boolean
default: false
outputs:
build-artifact-name:
description: "Name of the built artifact"
type: string
jobs:
build:
runs-on: ubuntu-latest
outputs:
build-artifact-name: ${{ steps.build_step.outputs.artifact_name }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- name: Install dependencies
run: pip install -r requirements.txt
- name: Lint code
run: flake8 .
- name: Run tests
if: ${{ !inputs.lint-only }}
run: pytest
- name: Build package
id: build_step
if: ${{ !inputs.lint-only }}
run: |
echo "Building package..."
# Imagine a real build command here
echo "artifact_name=my_app_v1.0.0.tar.gz" >> $GITHUB_OUTPUT
lint:
runs-on: ubuntu-latest
needs: build
if: ${{ inputs.lint-only }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
- name: Install dependencies
run: pip install -r requirements.txt
- name: Run linting
run: flake8 .
And here’s how a consuming repository might use it:
# .github/workflows/app-ci.yml
name: App CI Pipeline
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
ci:
uses: my-org/my-shared-workflows/.github/workflows/python-build.yml@v1
with:
python-version: '3.11'
lint-only: false
secrets: inherit # Or specify secrets: { MY_API_KEY: ${{ secrets.MY_API_KEY }} }
lint_check:
uses: my-org/my-shared-workflows/.github/workflows/python-build.yml@v1
with:
python-version: '3.9'
lint-only: true
Notice how python-build.yml has both inputs and outputs. Outputs from a reusable workflow job can be accessed by subsequent jobs in the calling workflow. For example, if ci in app-ci.yml completed and produced an artifact, another job could reference needs.ci.outputs.build-artifact-name.
When you define secrets: inherit in the jobs.<job_id> block of the calling workflow, it means all secrets available to the calling workflow will be automatically passed to the reusable workflow. This is convenient but can be less secure if the reusable workflow doesn’t strictly need all of them. You can also explicitly pass specific secrets by naming them, which is a more secure practice.
The outputs from a reusable workflow are defined within the on: workflow_call: block and are populated by step.outputs in the reusable workflow jobs. This allows you to pass computed values or results back to the workflow that invoked it, enabling more complex orchestration.
Many developers overlook the fact that a reusable workflow can be called from another reusable workflow. This creates a hierarchical structure, allowing you to build complex, modular CI/CD pipelines where smaller, focused workflows are composed into larger ones. For instance, a "deploy" reusable workflow might call a "build" reusable workflow, which in turn calls a "lint" reusable workflow.
The next logical step after mastering reusable workflows is to explore environment protection rules and deployment strategies that integrate with these shared pipelines.