Skip to content
James Dawson By James Dawson Principal I
Adventures in GitHub Actions: Episode 1 - Goodbye Azure DevOps, Hello GitHub Actions

At endjin we have been long time users of Azure DevOps, but after Microsoft bought GitHub we noted the focus of their CI/CD investments being directed at GitHub rather than Azure DevOps. Whilst there is still an actively maintained roadmap for Azure DevOps, we've ultimately concluded that the direction of travel is towards GitHub Actions; albeit over what is likely to be a lengthy time period so as to not undermine the enterprise adoption of Azure DevOps.

From a source control perspective, we took the decision several years ago to migrate all our open source repositories to GitHub and, all things being equal, it also became our first choice for new repositories, open source or otherwise. Today this means that our CI/CD pipelines in Azure DevOps are linked to the repositories in GitHub and we rely on the various integration mechanisms (on both sides of the fence) to ensure that it all 'just works'.

In recent months we've encountered sporadic issues with some repositories losing the ability to either trigger pipelines in Azure DevOps or not being able to see the status of those pipeline runs. There are various methods of connecting Azure DevOps to Github (Personal access token, OAuth, GitHub App), each supporting integration with different combinations of GitHub APIs. For us, having to sink time into troubleshooting such issues was the straw that broke the camel's back and we decided that 2023 should be the year where we make a concerted effort to migrate our CI/CD workloads to GitHub Actions.

We have been using GitHub Actions for the last couple of years across a small set of open source and internal projects and this has helped us grow our understanding of the product as updates have been released. The main features we had been waiting for were in the 're-useability' area. We currently have a set of Azure DevOps pipeline templates that model our opinionated views on a best practise .NET build process and this is used across all our open source repos. This gives great consistency and requires minimal effort to setup a pipeline for a new repository - in fact most of the effort goes into setting-up the Azure DevOps/GitHub integration plumbing than the pipeline itself!

The Introduction to Rx.NET 2nd Edition (2024) Book, by Ian Griffiths & Lee Campbell, is now available to download for FREE.

The release of Reusable Workflows and Composite Actions have meant our last big feature gaps have been plugged and we now expect to be able to develop an equivalent shared pipeline approach on GitHub Actions. We'll be using this series to talk about how that migration process pans out in practise and highlight any differences or gotchas we come across.

Introduction over, so let's start the series with an initial nugget that we've already encountered.

Inputs received via workflow_dispatch events are always treated as strings. The type information included in their definition is only used to render the appropriate UI controls when manually triggering the workflow via the web site (e.g. check-box, text-box, drop-down selection etc.). If any such inputs are passed to other strongly-typed workflows or actions, then you are responsible for casting them from a string into the required type.

Using types with GitHub Actions workflow inputs

GitHub Actions supports the concept of inputs in a number of scenarios:

  1. Explicitly triggered workflows (i.e. workflows triggered by a workflow_dispatch event)
  2. Re-usable workflows (i.e. workflows triggered by a workflow_call event)
  3. Composite actions
Programming C# 12 Book, by Ian Griffiths, published by O'Reilly Media, is now available to buy.

For the purposes of this post I'm going to gloss over the what each of those are, as they each deserve their own post.

GitHub Actions Inputs are conceptually equivalent to Azure DevOps Template Parameters.

All of the above scenarios allow you to define metadata for each input, including to define its expected type. How you define those inputs in YAML is very similar for each scenario and you would be forgiven for thinking they are treated the same.

Below is a example definition for an input that is being used as a control flag:

inputs:
  doSomething:
    description: When true, something happens, otherwise something doesn't happen
    type: boolean
    required: false
    default: false

Let's consider the first scenario where we are using the above input in what I think of as a 'top-level' or 'entrypoint' workflow; that is to say it's a workflow that is an initiation point in a way that the other 2 scenarios are not. The other scenarios are only executed when they are referenced in another workflow that is already running.

For our example, consider the following 'top-level' workflow run-dostuff.yml, it implements an imaginary process that needs be initiated by a human.

on:
  workflow_dispatch:
    inputs:
      doSomething:
        description: Tick this box if you need to do something
        required: false
        default: false
        type: boolean

jobs:
  dostuff:
    uses: ./.github/workflows/dostuff.yml
    with:
      doSomething: ${{ github.event.inputs.doSomething }}

To more easily illustrate the difference between scenarios 1 & 2, the above example simply calls a re-usable workflow which also has a doSomething input. We need to ensure that its value matches the intent of the person triggering the top-level workflow.

Here is the definition of that re-usable workflow dostuff.yml:

on:
  workflow_call:
    inputs:
      doSomething:
        description: When true, something happens, otherwise something doesn't happen
        required: false
        default: false
        type: boolean

jobs:
  dostuff:
    name: Do Stuff
    runs-on: ubuntu-latest
    steps:
    - name: Do the stuff
      run: |
        echo "Doing the stuff..."
        echo "'doSomething' is set to '${{ inputs.doSomething }}'"
      shell: bash
    - name: Do something
      if: inputs.doSomething
      run: |
        echo "Doing something..."
      shell: bash

Given the simple nature of this, we might imagine that everything is 'good to go' - let's see.

We can issue a workflow_dispatch event by triggering the workflow via the GitHub site:

Manually triggering a workflow with inputs

Whether we trigger it with the checkbox ticked or not, we get the following error:

.github/workflows/run-dostuff.yml: .github#L1
The template is not valid. .github/workflows/run-dostuff.yml (Line: 14, Col: 20): Unexpected type of value 'true', expected type: Boolean.

This is indicating that we have some kind of type mismatch between the value we are passing to the re-usable workflow and the value received as input to the top-level workflow. This seems odd because both are defined as boolean so what's happening?

It turns out that whilst the two inputs look to be very similar, they are in fact handled very differently.

The error message shows that the value is a 'string' and whilst a lot of type coercion happens in GitHub Actions this is not one of those situations. Therefore we need to find a way to satisfy the type constraints using the available expression support.

Fixing 'Unexpected type of value' errors in GitHub Actions workflows

All inputs to a workflow_dispatch event are treated as strings and the type information is only used by the UI to render the appropriate controls when triggering the workflow (e.g. check-box, text-box, drop-down selection etc.).

As part of solving this we discovered that GitHub Actions doesn't support explicit casting or ternary expressions (which might be useful in situations where 'null' input values need to be treated differently). The solution was to use a condition expression to evaluate the input value, which we can rely on to return an actual boolean that will satisfy the type constraint on the reusable workflow input.

This changes the run-dostuff.yml workflow as shown below:

on:
  workflow_dispatch:
    inputs:
      doSomething:
        description: Tick this box if you need to do something
        required: false
        default: false
        type: boolean

jobs:
  dostuff:
    uses: ./.github/workflows/dostuff.yml
    with:
      doSomething: ${{ github.event.inputs.doSomething == 'true' }}

FAQs

Are the input values for GitHub Actions workflows triggered via the workflow_dispatch event strongly-typed? No they are all treated as strings, any type information included in their definition is only used by the UI to render appropriate controls.
Can I share GitHub Actions workflow / pipeline definitions across multiple projects / repositories? GitHub Actions now has comprehensive support for reusability with its 'Reusable Workflows' and 'Composite Actions'.

James Dawson

Principal I

James Dawson

James is an experienced consultant with a 20+ year history of working across such wide-ranging fields as infrastructure platform design, internet security, application lifecycle management and DevOps consulting - both technical and in a coaching capacity. He enjoys solving problems, particularly those that reduce friction for others or otherwise makes them more effective.