Running GHA workflows locally with Act
Context
Act
is a local runner for GitHub Actions workflows. It works with the Docker
containerization engine to create containers as local runners for your
workflows.
It’s a good productivity tool for testing your CI/CD pipelines because instead
of creating another git branch, pushing it to the repository, and trying to
execute it using the dreaded workflow_dispatch
trigger, you can simply run it
in your CLI!
The main problem of the manual execution approach is due to pushing multiple commits to the remote repository. It may not produce the fastest iteration when trying to test workflows.
Also, consider that workflows may do things that are not expected, such as hanging or accidentally falling into an infinite loop. Doing so entails costs or using the precious free tier’s resources for testing when the actual workflow should use those.
Installing
But let’s talk about using it. For act
to work, you need docker up and running
in your local environment. Unfortunately, it cannot be another engine such as
podman
, containerd
, or other alternatives[^fn1]. After all, GitHub Actions
uses the docker backend engine to run workflows.
Use this page for guidance on installing docker.
You can check its repository on GitHub for more information on documentation and how to contribute to it. There are numerous ways of installing it, given that it’s supported on multiple platforms and as a GitHub CLI extension!
Usage
Let’s assume that you’re using something like Bash in UNIX-like OSs.
Starting out
Once installed, we use act
in our command line user interface:
It runs all your workflows that are triggered by push
under
./.github/workflows/
directory by default. Failing to have that directory
created will result in an error:
Also, note that if you’re using MacOS, docker
will run under a Virtual Machine
and act
won’t resolve its socket location in your local environment by
default.
So, you need to provide an environment variable DOCKER_HOST
that points the
location of that socket to the application. This can be achieved by using
docker’s context feature.
Running actions
Let me do a brief recap on what is and how can we work with GHA.
GHA stands for GitHub Actions, and it’s a workflow tool that can be used on your repositories that are hosted on GitHub.
The best thing, actually, is that you can create it using YAML configuration,
and it has quite a complete set of docs
that you can check in order to create your own pipelines. Also, we refer these
pipelines as workflows
.
With that said, the basics for declaring a workflow would be the following:
- Create the following path from the project’s root directory with
mkdir -p ./.github/workflows/
; - And then, you create a YAML file under the directory.
Now, assuming that we have the following workflow file under
./.github/workflows/cd.yml
:
And then, on the project’s root directory, it’s as easy as running act
.
And this is what act
does, it runs your workflows in a container so it makes
your workflow debugging life a little bit easier.
By default, when you run a bare command, act
triggers the push
action on our
repo’s default branch. So whenever a commit to the main
branch is pushed and
synced in GitHub, it’ll trigger this workflow we’ve set.
However, we only did the CD part of the CI/CD process, which involves continuous deployment of the project when changes are in fact merged in our repository.
The most important process, IMO, is the Continuous Integration, which will make sure that changes are conformal with quality standards we have set for it.
Minimally, we’d like to have our code:
- Formatted according to the programming language standards;
- Linted according to a set of rules that the code must follow;
- Tested according to assertions.
So we’ll introduce another file that’ll go under ./.github/workflows/ci.yml
:
So this workflow will emulate a CI process, and we’ll be forcing an error during the last step in the integration test.
To execute this workflow in act
, you need to specify which trigger you want to
emulate, since the bare act
command defaults to the push
trigger. In our
case, we’d like to trigger a pull_request
:
So this is the pattern that act
offers, which is to run workflows according to
triggers that they’re set for execution. Let’s see what are the other things
that it may offer to us, shall we?
Running specific jobs instead of the whole workflow
Let’s assume we have a huge workflow (10+ steps), or a step that we know it’s working fine, but it’s time-consuming.
We may want to have a shortcut to just execute a single step that’s being
problematic for us, right? It’s a lucky day for us, act
has gotten us covered
for that.
During the CI workflow, the integration test
was being a bit problematic to
us, reporting a failure that shouldn’t be there.
So let’s debug it by using the previous command, but now we’re passing the -j
flag and then the name of the step in single quotes as in the following:
By doing some adjustments in the step, you can give it a try and should be good to go!
Listing available workflows
If you want to list all the workflows along with their trigger events that are associated with the repository:
If you want to list all workflows for a specific event:
Using secrets
Usually, when we’re dealing with these workflows, we’re interacting with other services such as cloud providers, quality gating services, etc.
They use authentication to make sure that’s us doing our own thing. These
secrets can be stored on the repo’s settings in GitHub and then you can refer to
it using the ${{ secrets.<KEY> }}
directive.
However, how can we emulate it with act
, since we don’t have anywhere to store
or to access? Once again, it’s gotten us covered.
In our cd.yml
example, let’s assume that we want to push a specific build
artifact into a service that requires a secret in order to accomplish that.
If we try running the workflow without providing the secret, we won’t get past it:
There are two ways for us to pass the required secret:
Provide it through the
act
CLI flag-s
in a key-value manner, likeact -s WAT=foo
. The drawback in doing so is because the secret will be exposed in the logs, history, anything that records the inputs and outputs of the shell. So then, the safest alternative would be the following.Provide an
.env
file to the CLI option--secret-file
. This way, it’s guaranteed that nothing will be issued back tostdout
.
So, it’s as simple as creating an .env
file with a secret like WAT=foo
and
then executing act
with the file being provided in the --secret-file
option:
So the secrets will make it easier for you to interact with services outside the GHA realm so you can harnest the testing even further.
The last thing that’s missing, really, is the ability to push artifacts, and of
course, act
’s gotten us covered.
Artifacts handling
There are some workflows in which we may store artifacts of builds or anything that we intend to keep regarding a project. So how does it work? Take a look.
Assuming we have the following code:
We want to build it and store the binary in our artifacts. Let’s assume that we
have the following artifact.yml
file like this:
There’s a way to mock artifacts. We can provide a path in our file system to
store those artifacts in a directory with the following command. Afterward, when
executing the workflow, it’ll create a server that will fetch the file upload
and put it under our ./artifacts/
directory:
:)