How to connect dispatching and dispatched workflows
In the last article in this series I mentioned that by switching from reusable workflows to dispatching we can lost some navigation clarity. Dispatched workflow has no connection about its dispatcher, so it might be difficult to search what dispatched what.
To improve this, I reserved one input field called meta
in dispatching workflow inputs for metadata. The dispatching workflow then sends specific data about itself in JSON format. The dispatched workflow then prints URL link(s) of dispatcher workflow(s) easily to the job summary..
This doesn't replace the “call map” that renders with reusable workflows like this one bellow:
But it's the best solution I've come up with so far. It makes it easier for me to find out which workflow dispatched the currently opened one through the GitHub Actions
page.
The Problem
Let’s have one main workflow, dispatching some other workflow.
Dispatching workflow steps might look like this:
#...
steps:
- uses: the-actions-org/workflow-dispatch@v4
id: dispatch
with:
workflow: app1.yaml
token: ${{ secrets.GITHUB_TOKEN }}
wait-for-completion: true
display-workflow-run-url: true
wait-for-completion-interval: 10s
inputs: |-
{
"build": "true",
"test": "true"
}
In the successful run, the only option to navigate to the dispatched workflow is the link in run logs of the dispatching workflow:
And there is no option or link anywhere to jump back form dispatched workflow to dispatching one. Except maybe manual search through Actions
page.
However, if there are more historical runs, choosing the right one becomes difficult, and your only option is to roughly match it by time. This is not ideal.
The solution
So what I did was:
Add new field
meta
to dispatched workflowPass the dispatching workflow info in JSON format through
meta
fieldPrint the dispatched workflow link to job summary (so I don’t have to expand the logs every time I need to jump directly to the dispatched workflow)
Print the link back to the dispatcher from dispatched workflow in job summary - using extra
parent info
job
Dispatching workflow
My original dispatched example now looks like this (full workflow here):
# snip...
jobs:
main-job:
name: Main dispatch job
runs-on: ubuntu-latest
steps:
# constructing (multiline) metadata
# without help of JS actions is this ugly
- name: Meta data
id: meta
run: echo 'json={
\"parent-workflow-name\":\"${{github.workflow}}\",
\"parent-workflow-url\":\"${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}/attempts/${{github.run_attempt}}\"
}' >> $GITHUB_OUTPUT
#disaptch action
- uses: the-actions-org/workflow-dispatch@v4
id: dispatch
with:
workflow: ex1-app1.yaml
token: ${{ secrets.GITHUB_TOKEN }}
wait-for-completion: true
display-workflow-run-url: true
wait-for-completion-interval: 10s
inputs: |-
{
"build": "true",
"test": "true",
"meta": "${{steps.meta.outputs.json}}"
}
# print the link to (and status of)
# the of dispatched workflow directly to the job summary
- name: Summary
if: ${{ always() }}
run: |
echo 'Dispatched [Example 1 - App 1](${{steps.dispatch.outputs.workflow-url}}). Result: `${{steps.dispatch.outputs.workflow-conclusion}}`' >> $GITHUB_STEP_SUMMARY
What I did here:
- I'm constructing a stringified JSON object like this:
{
"parent-workflow-name":"DISPATCHING_WORKFLOW_NAME",
"parent-workflow-url":"CONSTTRUCTED_WORKFLOW_SUMMARY_URL"
}
I’m adding it as an extra parameter
meta
to the dispatched workflow inputsAdditionally, I'm printing the link to the dispatched workflow in the job summary, so I don't have to search for it in the logs.
Quirks:
It is all pretty simple, but unnecessary verbose and code is ugly.
That YAML-JSON
mess there:
run: echo 'json={
\"parent-workflow-name\":\"${{github.workflow}}\",
\"parent-workflow-url\":\"${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}/attempts/${{github.run_attempt}}\"
}' >> $GITHUB_OUTPUT
#...
inputs: |-
{
"build": "true",
"test": "true",
"meta": "${{steps.meta.outputs.json}}"
}
Is there only because that dispatch action actions/workflow-dispatch-and-wait supports sending inputs only as JSON, which is pretty inconvenient but okay for now.
Dispatcher summary now looks like this:
✅ Great. I no longer need to check the logs whenever I just want to click through to the dispatched workflow.
Dispatched workflow
My updated dispatched workflow now looks like this (full example here):
name: "Example 1 - App 1"
on:
workflow_dispatch:
inputs:
build:
description: "Build the library"
required: true
default: "true"
test:
description: "Run the tests"
required: true
default: "true"
meta: #<--------------------new meta field
description: "Metadata"
required: false
default: "" #<----- default value have to be valid json string
jobs:
# new extra job that will parse metadata and
# print url back to the dispatcher
parent-info:
name: Parent Info
# run this job only if there are any meta
if: ${{ inputs.meta != '' && fromJSON(inputs.meta).parent-workflow-url != '' }}
runs-on: ubuntu-latest
steps:
- name: Print parent workflow url
run: |
echo 'Dispatched from workflow: [${{fromJSON(inputs.meta).parent-workflow-name}}](${{ fromJSON(inputs.meta).parent-workflow-url }})' >> $GITHUB_STEP_SUMMARY
# other jobs
#snip...
What I did here:
Added a new
meta
field in the inputs. Yes, now I can use only9
other inputs.Added a new job
Parent info
that will parse incoming metadata, if there are any, and prints link back to the dispatcher to the job summary.
- It uses GitHub expression
fromJSON
to parse JSON directly so no extra action or tools likejq
are necessary
Dispatched workflow summary now contains:
✅ Now I can navigate both ways easily between the dispatching and dispatched workflows by clicking on links in the summaries.
Improvements
So with examples above I have working solution. There are few things that can be improved though. Such as:
support dispatching workflows in another repository
multi level dispatch
code simplification
Dispatching a workflow in a different repository
One thing I needed was to dispatch a workflow in a different repository. Luckily, the action actions/workflow-dispatch-and-wait supports this out of the box. I just needed to make small adjustments to the dispatcher workflow (see the full workflow file here):
# multi repo dispatch example
name: Example 2 - Main
on:
workflow_dispatch:
inputs: {}
# <-------------- notice that permissions section is gone
defaults:
run:
shell: bash
jobs:
main-job:
name: Main dispatch job
runs-on: ubuntu-latest
steps:
- name: Meta data
id: meta
# in meta I added one extra field for repository name
# now it looks even more ugly :)
run: echo 'json={
\"parent-workflow-name\":\"${{github.workflow}}\",
\"parent-workflow-url\":\"${{github.server_url}}/${{github.repository}}/actions/runs/${{github.run_id}}/attempts/${{github.run_attempt}}\",
\"parent-workflow-repo\":\"${{github.repository}}\"
}' >> $GITHUB_OUTPUT
- uses: the-actions-org/workflow-dispatch@v4
id: dispatch
with:
workflow: ex2-app1.yaml
# when dispatching in the other repo I have to
# provide specific PAT token with extra permissions
token: ${{ secrets.ALL_REPOS_PAT }}
wait-for-completion: true
display-workflow-run-url: true
# and specify repository name in extra param
repo: roamingowl/gh-workflow-dispatch-navigation-second-example
wait-for-completion-interval: 10s
inputs: |-
{
"build": "true",
"test": "true",
"meta": "${{steps.meta.outputs.json}}"
}
#...
repository_dispatch
event to dispatch some workflow in another repository. workflow_dispatch
with action actions/workflow-dispatch-and-wait is perfectly enough.What I did here:
- When dispatching to another repository, you can't use the automatically provided
secrets.GITHUB_TOKEN
in the workflow because it doesn't have permission to access other repositories. You need to create a new PAT with the following scopes:
Because I’m using PAT, section with permissions can be removed
I added dispatching workflow repository name to the metadata under
parent-workflow-repo
key
In dispatched workflow I just print the repo name to the summary in Parent Info
job (full workflow file here):
#...
jobs:
parent-info:
name: Parent Info
if: ${{ inputs.meta != '' && fromJSON(inputs.meta).parent-workflow-url != '' }}
runs-on: ubuntu-latest
env:
PARENT_WORKFLOW_REPO: ${{ fromJSON(inputs.meta).parent-workflow-repo }}
steps:
- name: Print parent workflow url
# print the repo name only if the key parent-workflow-repo
# exists in the metadata (and is different from the
# current repo name)
run: |
if [[ -z $PARENT_WORKFLOW_REPO ]] || [[ $PARENT_WORKFLOW_REPO == $GITHUB_REPOSITORY ]]; then
echo 'Dispatched from workflow: [${{fromJSON(inputs.meta).parent-workflow-name}}](${{ fromJSON(inputs.meta).parent-workflow-url }})' >> $GITHUB_STEP_SUMMARY
else
echo 'Dispatched from workflow: [${{fromJSON(inputs.meta).parent-workflow-name}}](${{ fromJSON(inputs.meta).parent-workflow-url }}) in repo [${{env.PARENT_WORKFLOW_REPO}}](${{github.server_url}}/${{env.PARENT_WORKFLOW_REPO}})' >> $GITHUB_STEP_SUMMARY
fi
#...
✅ When dispatched from different repo, summary now contains dispatcher repository name:
Multi-level dispatch
In some cases, like complex workflows or monorepos, I needed to support multiple levels of dispatches. For instance, when the main repository workflow triggered a workflow for an app or library, and those, in turn, triggered their own builds or complex tests.
I also wanted to view all the parent dispatchers for each workflow, not just the nearest one.
Dispatcher workflow is now as simple as (full workflow here):
#...
jobs:
main-job:
name: Main dispatch job
runs-on: ubuntu-latest
steps:
# when using local actions I have to checkout whole repo first
- uses: actions/checkout@v4
# all logic with metadata construction, dispatching
# and summary printing is now inside reusable action
# keeps workflow code clean
- name: Dispatch
uses: ./.github/actions/dispatch-with-summary
with:
# only passing here workflow inputs,
# meta will be handled automatically
workflow-inputs: |
{
"app1": "true"
}
workflow-name: 'Example 4 - Middle'
token: ${{ secrets.GITHUB_TOKEN }}
workflow-file: ex4-middle.yaml
What I did here:
- All logic and summary printing went to action, which can be reused now in all dispatching workflows.
- Workflow code is now much cleaner
Reusable action dispatch-with-summary
looks like this (full action here):
#...
inputs:
meta:
required: false
default: ""
workflow-file:
required: true
workflow-name:
required: true
# token have to be sent through input
token:
required: true
workflow-inputs:
required: false
default: ""
runs:
# basically just extracted those three steps from
# the dispatching worflow to this composite action,
# so the code can be reused easily
using: "composite"
steps:
- name: Prepare meta data
id: meta
# multi level support is inside another action here
uses: ./.github/actions/prepare-meta
with:
meta: ${{ inputs.meta }}
# some JS magic here to do a JSON merge
# can't be done with just GH expressions
- name: Merge inputs
id: merge-inputs
env:
META_JSON: ${{ steps.meta.outputs.json }}
WORKFLOW_INPUTS_JSON: ${{ inputs.workflow-inputs }}
uses: actions/github-script@v7
with:
script: |
const { META_JSON, WORKFLOW_INPUTS_JSON } = process.env;
const parsedInputs = JSON.parse(WORKFLOW_INPUTS_JSON);
const parsedMeta = JSON.parse(JSON.parse(META_JSON));
const inputs = {...parsedInputs, meta: JSON.stringify({...parsedMeta})};
return JSON.stringify(inputs);
# original dispatch
- uses: the-actions-org/workflow-dispatch@v4
id: dispatch
with:
workflow: ${{ inputs.workflow-file }}
token: ${{ inputs.token }}
wait-for-completion: true
display-workflow-run-url: true
wait-for-completion-interval: 10s
inputs: |-
${{ fromJSON(steps.merge-inputs.outputs.result) }}
# print the link to the job summary
- name: Summary
shell: bash
run: |
echo 'Dispatched [${{inputs.workflow-name}}](${{steps.dispatch.outputs.workflow-url}}). Result: `${{steps.dispatch.outputs.workflow-conclusion}}`' >> $GITHUB_STEP_SUMMARY
So nothing new here:
I just extracted some steps out of the workflow to this composite action
To support multi-dispatch I’m processing metadata in another action
prepare-meta
Metadata is now an object with a "call stack" stored in an array under the
workflows
key. Each dispatch will simply add a new item with workflow information to this array.
//metadata before
{
"parent-workflow-name":"main workflow",
"parent-workflow-url":"some-url",
"parent-workflow-repo":"repo1"
}
//metatdata supporting multi-level dispatch
{
"workflows": [
{
"parent-workflow-name":"main workflow",
"parent-workflow-url":"some-url",
"parent-workflow-repo":"repo1"
}
]
}
With some help from actions/github-script action I was able to put it together easily (full action file here):
#...
inputs:
meta:
description: "Existing metadata JSON string"
required: false
default: ""
outputs:
json:
# output is stringified JSON which can be then
# merged with other params and sent to dispatched workflow
value: ${{ steps.script.outputs.result }}
runs:
using: "composite"
steps:
- id: script
uses: actions/github-script@v7
env:
META: ${{ inputs.meta }}
with:
# switching to node was easier for complex
# string manipulation
# basic functionality is just:
# - if there are no exiting meta
# - create an empty array under workflows key
# - add info about current workflow to it
# - return meta as stringified JSON
script: |
let { META } = process.env
let metaData;
if (META === '') {
metaData = {};
} else {
metaData = JSON.parse(META);
}
if (!Array.isArray(metaData.workflows)) {
metaData.workflows = []
}
let attempt = context.runAttempt;
if (typeof attempt === 'undefined') {
attempt = 1;
}
metaData.workflows.push(
{
"parent-workflow-name":`${context.workflow}`,
"parent-workflow-url":`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/attempts/${attempt}`,
"parent-workflow-repo":`${context.repo.owner}/${context.repo.repo}`
}
);
return JSON.stringify(metaData);
The action is straightforward:
It checks if there is any metadata in the input (from the previous dispatching workflow).
If not, a new array is created with information about the current dispatcher.
If yes, the data is simply added as a new item.
Parent info
job in didspatcing workflow was refactored to use composite action parent-info to decode it and print a list of dispatching workflows.
✅ Summary now contains info about all previous dispatchers (left: just one parent, right: full dispatch list):
More improvements
There are many more ways to improve this even further:
Go beyond JSON and extend the action actions/workflow-dispatch-and-wait to include all the functionality described above and more:
No more TS-in-YAML issues in workflows - clean code.
A separate action can be unit-tested easily - safe code.
If the action is in another repo and the dispatching job only performs dispatch, there is no need to call checkout as the first step (useful in large monorepos).
In deep dispatches, be careful not to exceed the input limit of 65,535 characters.
Pass data back from the dispatched workflow to the dispatching one to get and print test results overviews, for example.
I’ll return to these improvements in some of the next articles in this series.
That is all for now. Thank you. 👋
Happy coding :)
Subscribe to my newsletter
Read articles from Juraj Simon directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Juraj Simon
Juraj Simon
DevOps/FullStack node.js developer. Passionate about automation and efficiency. Learning how to write... :)