How to connect dispatching and dispatched workflows

Juraj SimonJuraj Simon
10 min read

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.

For dispatching I’m using actions/workflow-dispatch-and-wait action.

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 workflow

  • Pass the dispatching workflow info in JSON format through meta field

  • Print 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 inputs

  • Additionally, 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 only 9 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 is an extra (first) job mainly to keep it always at the top of the job summaries list.
  • It uses GitHub expression fromJSON to parse JSON directly so no extra action or tools like jq 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}}"
            }
#...
💡
You don’t have to use 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.

💡
To keep the workflows code clean I extracted some logic into composite actions.

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 :)

0
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... :)