A few weeks ago, I briefly discussed the importance of automating tedious but essential tasks in software development to ensure that source code follows a project’s conventions. For example, code in the main branch should not contain TODO comments since they highlight unfinished tasks that should not be released. Writing these workflows quickly uncovers how many of them contain shared steps, and today’s article explains how to extract common CI/CD tasks into reusable workflows and reuse them across multiple pipelines in GitHub actions.
Jobs vs. Steps in GitHub Actions
In the TODO GitHub action article, I introduced a simple action with a single job comprising two steps:
jobs:
check-todos:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Check for TODO Comments
run: |
if grep --exclude="*.md" --exclude-dir={.git,.github,.mvn,.idea,doc,resources} -rE "TODO|FIXME"; then
exit 1
else
echo "No TODO or FIXME comments found!"
fi
The truncated example defines a job called “check-todos” which contains two steps: “Checkout Code” and “Check for TODO Comments”.
The main difference between the two is that jobs generally run in parallel, while the steps within a job are executed sequentially. Subsequent steps within a job can use data generated by previous steps. Similarly, a later step can access values stored in environment variables by an earlier step. Finally, it’s worth noting that jobs can depend on each other, forcing the CI/CD pipeline to wait for a job to succeed when another one depends on it.
Making the TODO Action Reusable
All that’s required to make a GitHub action reusable is adding the “workflow_call” callback handle to the list of events that trigger a workflow as a child of the “on” node. For example:
on:
pull_request:
branches:
- "main"
workflow_call:
A workflow file with these handles will run whenever there is a pull request on the main branch and whenever it’s called by another workflow.
Referencing Reusable Workflows in GitHub Actions
All that’s required to call a reusable workflow in another action is to use it in a job like so:
name: Master Pull Request Pipeline
on:
pull_request:
branches:
- "main"
jobs:
comment-checks:
uses: ./.github/workflows/check-todos.yml
build:
needs: "comment-checks"
uses: ./.github/workflows/maven-build-and-test.yml
Here, the “comment-checks” job uses the reusable TODO workflow from before. You can reference workflows in the same repository by stating the file location. You can also supply a URL to another git repository to load and use a workflow from another repository (given that you have sufficient privileges to access the file):
uses: octo-org/example-repo/.github/workflows/reusable-workflow.yml@main
secrets:
token: $
As this example shows, you can supply custom credentials if needed. Similarly, the “with” note can pass additional parameters to the reusable workflow.
Running Jobs in Sequential Order
Note how the “Master Pull Request Pipeline” job from before uses two reusable workflow files in the same repository. The second job, called “build”, should not run before the “comments-check” job succeeds to prevent the build pipeline from running unnecessarily. The “needs” keyword lets you define which jobs must be finished before a job can start. It takes a single string or an array of job names.
Limitations of Reusable Workflows
The main limitation I’ve encountered when working with GitHub Actions is that each reusable workflow must be executed in a separate job, and you can’t define them as tasks to be executed one after the other. Using the “needs” keyword lets you work around this limitation and execute all reusable workflows sequentially, almost as if they were tasks within the same job.
Jobs are also isolated and can’t directly access data generated by other jobs (since they could run in any order).
Finally, all workflow files must be located directly within the .github/workflows folder; adding subfolders is not allowed.