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 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:
- Explicitly triggered workflows (i.e. workflows triggered by a
workflow_dispatch
event) - Re-usable workflows (i.e. workflows triggered by a
workflow_call
event) - Composite actions
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:
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' }}