How my experience with GitHub Actions made my local and remote build pipeline predictable, fast to change & to test. I recommend a strategy to get more re-use out of GitHub Actions, but it wasn’t all plain sailing either.

There’s an old saying that goes something like this: “just because you can, doesn’t mean you have to”.

Wise words.

Lets look at a common app developer scenario. I wrote an app (yay!), and for various technical reasons it cannot be distributed on the Windows app store (boo). To ensure my users get updates I built the NetSparkle framework into the app (yay!); but the build process needs various assets that are not checked into source control (boo).

“What’s this have to do with GitHub Actions?” I hear you ask.

My build & release process was entirely manual (sound familiar?). I wanted to move the build & release system for my Windows app away from my dev machine and achieve all of the qualities / functionality below.

In my mind the build pipeline must:

  1. be repeatable & reliable
  2. run my unit tests - failing loudly as required
  3. not depend on my machine, nor on me as a person
  4. use the same build techniques on the dev machine as for a production release - aka the principle of “least surprise”
  5. enable a fast change & test cycle

All of this really just boils down to making it easier for me to achieve more over the long term. The overhead isn’t overwhelming and once in place this kind of work starts paying dividends immediately, and continously.

Enter GitHub Actions

The GitHub Actions product is extremely cool. It’s free to start using it, you get a decent quota of runtime minutes, the ability to run matrices and a large set of capabilities on different runtime platforms, as well as excellent documentation - it’s an attractive proposition for anyone that wants to create a build pipeline.

What’s not to like about all that?

Not much - but there are ways to make it harder than it needs to be.

My Experience

When I first encountered GitHub Actions I took the approach of rewriting my build & publish pipeline entirely.

So I started by porting the commands in my existing build pipeline into GitHub Actions.

There are some aspects of using GitHub Actions that I quickly learned were “less than optimal”, and some things that I was doing just plain wrong. Hindsight is always 20/20 and now I really like what I’ve got as a build system.

So what can be improved?

Cycle Time

Firstly; the long code / test cycle time of GitHub Actions is an enourmous barrier to getting results quickly (or to achieving flow).

This is because to test any change you must check the change into source control, push the branch, wait for the action to be scheduled in github and only then do you discover if you got it right - or not.

That’s 4 minutes if you are lucky, or 4 minutes + however long the rest of your build script takes to run if you are not so lucky.

Which leads to Johns’s GitHub Actions Rule #1:

'As fast as possible' turnaround time and GitHub Actions do not fit in the same sentence.

My local build script takes about 35 seconds to build, test and create packages.

I want to love GitHub Actions; but when it takes 4+ minutes to test a single change in the best case - that just doesn’t cut it I’m afraid, that’s way too long - I really want to get instant feedback - results measured in seconds please!

The cost of iterations. Look at the red underlined timings - minium 4:50 - all the way up to over 7m!

how long does it really take?

Granularity & Purpose

Second problem; granularity. In most cases this is good, i.e. the linux command line comes to mind instantly - there you can combine small things via a pipeline and the limit is the lesser of your creativity and shell command kung fu abilities.

However in this case, I’d say granularity works directly against - so here’s Johns’ GitHub Actions Rule #2:

Don't be tempted to write your entire build/publish pipeline using GitHub Actions - use the actions to set up the environment only; and then call your build/publish scripts from there.

Consistency

The other important point to consider here is consistency: is my local build system the same as what I will use in production?

While the GitHub Actions solution I had just created was using the same commands, programs and inputs, and in theory was creating exactly the package - it wasn’t exactly the same build system I was using locally - and I know from first hand experience where this can lead.

My pre-GitHub Actions build system was a bash script. So at this stage, on one hand I had a build script which was a bit (cough cough) of a monster… it made the build, ran the tests, created the Windows package and re-created the appcast, then stored assets in Azure. The only thing it wasn’t doing was making me breakfast in the morning. It worked, but it was never going to win a beauty contest.

And now I had this new fangled GitHub Actions thing which did the same thing, just using GitHub Actions!

This is just… bad… two implementations for the same goal. I don’t need more work, I want reliability, predicatbility. Build and deployment systems are not the places I seek excitement and surprise in my life. I want boring for my build system.

So, its: sudden realization / refactor time!

Solution

Here’s the point where I realized with a groan that I’d dug myself a hole. The problems at this stage were:

  1. Two build systems - one specifically for GitHub, and an old monster of a bash script
  2. Both were difficult to code/test & debug
  3. GitHub Actions isn’t giving me the best turnaround time
  4. The bash script was clearly doing too much

So, I did the following - and would do the same in future projects (aka best practise?):

  1. I re-wrote the build / packaging system using PowerShell - not because I love PowerShell in particular, but because it can easily run anywhere, it’s I/O model uses real objects (it’s hard to overstate how cool that is) and is thus more predictable, and I can debug it using Visual Studio Code locally. I’m used to debugging bash scripts with print statements - having a real debugger was absolutely, totally, wonderful.
  2. I split the appcast generation / publish stage out as a separate PowerShell script.
  3. I deleted most of the GitHub Actions build steps; and replaced them with a single line that called a new PowerShell build script.

Benefits & Summary

The result is fantastic.

All the goals have been met!

  1. I can push to build on master and on a feature branch - builds, tests and packages “will just happen”.
  2. Unit tests are always being run, and I don’t have to remember to do it.
  3. The build / test / package script runs locally within about 40 seconds - so I can test that part locally as well as being safe in the knowledge that the same processing will be used in production.
  4. The entire deployment system is independant of any computer I regularly use - there is no “configuration drift”. This eliminates statements that contain anything like: “it worked on my machine” and the like. If a new developer were to come on board - they could check out the code and get running in minutes.

GitHub Actions is staying in my workflow - as a way to construct the build / packaging environment, PowerShell is a pleasure to work with especially if you have done lots of bash work where the output of command is text and you use a technique called “luck” to parse information.

While the total run time of the build pipeline is still over 5 minutes - I get the best of both worlds.

– John Clayton