When we’re on client engagements, we bring our expertise, and we also learn from our clients. One project has exposed us to the concept of reusing GitHub workflows. One of the things we commonly use in our repos is code coverage, and we post the code coverage output to a PR. So in this post, I’m going to talk through how we moved our Comment on PR to reusable workflow.
The Original Comment on PR Workflow
I’ve blogged about this process in the past in our post on Using workflow_run
in GitHub Actions. In htat post, we talked about why comment-on-pr is a separate workflow.
This was the original comment-on-pr.yml GitHub Actions workflow that we started with:
name: Comment on the Pull Request
# read-write repo token
# See: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
on:
workflow_run:
workflows: [".NET Core"]
types:
- completed
jobs:
comment:
runs-on: ubuntu-latest
# Only comment on the PR if this is a PR event
if: github.event.workflow_run.event == 'pull_request'
steps:
- name: Get the PR Number artifact
uses: dawidd6/action-download-artifact@v2
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
workflow_conclusion: ""
name: pr-number
- name: Read PR Number into GitHub environment variables
run: echo "PR_NUMBER=$(cat pr-number.txt)" >> $GITHUB_ENV
- name: Confirm the PR Number (Debugging)
run: echo $PR_NUMBER
- name: Get the code coverage results file
uses: dawidd6/action-download-artifact@v2
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
workflow_conclusion: ""
name: code-coverage-results
- name: Add Coverage PR Comment
uses: marocchino/sticky-pull-request-comment@v2
with:
number: ${{ env.PR_NUMBER }}
recreate: true
path: code-coverage-results.md
We used this in eShopOnWeb and also in its NServiceBus-demo copy known as eShopOnNServiceBus. Since we may have other code repos that could use this, we moved it to our new workflows repo and reference it in our other workflows.
Refactoring the Workflow for Reusability
When I first evaluated how we use the workflow, I realized that there are high dependencies between our build workflow’s artifacts and the comment-on-pr workflow. So I identified what pieces we needed to pass in:
- PR Number - which PR gets the comment
- Code Coverage artifact name - as one of the plugins requires the artifact name used in the build workflow
- Code Coverage artifact path - used with the comment plugin
Once I identified these parts, I could extract them as workflow inputs.
We changed the top on
statement from workflow_run
- being dependent on a caller workflow by name - to workflow_call
, which is preferred for reusable workflows. In the workflow_call
section, we can define the inputs
to this workflow that get passed in a with
block in the caller workflow.
This is what the reusable comment-on-pr workflow looks like:
name: Comment on the Pull Request
on:
workflow_call:
inputs:
pr-number:
required: true
type: string
code-coverage-artifact-name:
required: true
type: string
code-coverage-artifact-path:
required: true
type: string
jobs:
comment:
runs-on: ubuntu-latest
# Only comment on the PR if this is a PR event
if: ${{ github.event_name }} == 'pull_request'
steps:
- name: Get the code coverage results file
uses: dawidd6/action-download-artifact@v6
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
workflow_conclusion: ""
name: ${{ inputs.code-coverage-artifact-name }}
- name: Add Coverage PR Comment
uses: marocchino/sticky-pull-request-comment@v2
with:
number: ${{ inputs.pr-number }}
recreate: true
path: ${{ inputs.code-coverage-artifact-path }}
Now that the inputs
are determined, let’s look at calling this from another repo. Reusable workflows are used within jobs, so this is the job we would use:
add-code-coverage-to-pr:
uses: NimblePros/NimblePros.GitHub.Workflows/.github/workflows/comment-on-pr.yml@main
with:
pr-number: YOUR_PR_NUMBER
code-coverage-artifact-name: YOUR_CODE_COVERAGE_ARTIFACT_NAME
code-coverage-artifact-path: YOUR_CODE_COVERAGE_ARTIFACT_PATH
But It Isn’t Necessarily That Straightforward
Passing some variables around shouldn’t be hard. It looks straightforward, right? 🤔
Unfortunately, when implemented this in eShopOnNServiceBus, I realized that it’s a bit more complicated. This is what the workflow looks like:
Let me address the questions you might be wondering.
Why do you have to have the middle workflow?
As I mentioned in the first blog post, comment-on-pr needs to separate reads and writes. I wrote that blog post awhile ago, so I might have forgotten about it. So I had this job working in the the dotnetcore.yml workflow. But I separated it out again.
Why do you create files for the artifacts?
I create files for the artifacts because that is how I learned to copy details from one workflow to another workflow.
Why do you use another artifact downloader?
I use dawidd6/action-download-artifact because it allows us to get the artifacts directly from a specific workflow. It’s a GitHub Action that allows us to get artifacts from a workflow for specific criteria.
Why do you have 2 jobs in the middle Comment on PR?
One of the jobs gets the artifacts from the previous workflow. Then, it reads in the values and stores them in output variables for the job, to be passed to the job that calls the reusable workflow. The reusable workflow can be called at the job level. I also make use of needs
in the second job, so that they execute sequentially.
Conclusion
Now that I’ve implemented this once, I can call the reusable workflow from another repo. Seeing how reusable workflows work, I look forward to identifying other processes that we consistently use and try to remove some of the duplication where it makes sense. If you find yourself using the same workflows over and over, consider - on a case-by-case basis - whether reusing workflows makes sense for you!