Ever felt like you were missing something? One of the most frustrating things about software development is often trying to understand the dependencies required to build or deploy a project. Most projects require one or more command line tools to support the build/test/deploy cycle. These tools are often installed globally, which can lead to conflicts and confusion. This is especially true with GitHub Actions runners and is a common reason for build failures in CI/CD pipelines. Too many processes rely on a specific version of a tool being globally available, and they fail if the version changes or is not available.
Using a global version can also lead to a breaking change when the host OS is updated. For example, the new Ubuntu 24.04 image lists some breaking changes. For example, Java will default to version 17 instead of version 11. The default version of Node.js is changing from 18 to 20. And for .NET, only version 8 will be installed (rather than 6, 7, and 8). If you are building your code based on the assumption you can rely on the ambient environment, these changes can cause your build to fail.
This is where GitHub Tools come in. They provide a way to define and manage the dependencies of a project and ensure your environment remains consistent and stable.
What is a GitHub tool
With GitHub, “tools” are a general way of referring to command line applications that are installed by an Action. Instead of relying on a global version, they allow users to specify a specific version of a CLI to use. Instead of providing inputs and a wrapper around the CLI, they install it and let the user invoke it directly. Most tools will have “setup” in their name. Some common tools:
Action | Description |
---|---|
actions/setup-node | Installs Node.js and NPM |
actions/setup-python | Installs Python |
actions/setup-java | Installs the Java JDK and Gradle/Maven |
actions/setup-dotnet | Installs the .NET SDK |
docker/setup-qemu-action | Installs the QEMU emulator, enabling cross-platform builds |
docker/setup-buildx-action | Installs and configures the Buildx plugin for Docker |
hashicorp/setup-terraform | Installs Terraform |
How GitHub tools work
A tool Action follows the same basic pattern:
- Using a user-provided configuration file or the
with
input on the Action, determine the version of the tool to install. This may be a semver range that is resolved to a specific version or a specific version number. - Look to see if the requested version of the tool already exists in the cache. This is the directory pointed to by the environment variable
RUNNER_TOOL_CACHE
(or the_tools
directory in the Runner’s_work
folder). - If the version doesn’t exist, download it and place it in the tool cache.
- Add the path to the requested version of the tool to the
$GITHUB_PATH
. By adding it to the path, the specific version being requested will always be used on that job. - Optionally, configure caching, and update any default settings.
Once configured, the version remains active until the end of the job. The great thing about this approach is that it also makes it easy to switch between versions within an Actions job. Calling the setup action with a different version will replace the existing version in the path, allowing a different version to be used.
Why use GitHub tools
It makes your build more declarative and self-contained. It makes the dependencies clear, including the specific versions that are required. Consider the following example:
1- run: dotnet build
Which version of .NET is this build step using? It relies on the global version on the runner, making it indeterminate. While a team may standardize on a specific version, it is not clear what version the code requires without looking at the code. This can lead to broken builds. It can also make it challenging for multiple versions to coexist on the same runner, leading to multiple images being created.
By comparison, if you see:
1- uses: actions/setup-dotnet@v4
2 with:
3 dotnet-version: '3.1.x'
4- run: dotnet build
In this case, you know exactly which version is required for a successful build. The build is also less likely to break compared to a shared global version. For teams that declare the version in their project’s global.json
, there they can reference the file to get the same benefits:
1- uses: actions/setup-dotnet@v4
2 with:
3 global-json-file: global.json
4- run: dotnet build
The version will be defined in the global.json file, so both the development environment and GitHub runner will use the specific version needed for the project.
What about downloads?
A common concern with this approach is that it increases the size of the image or adds the time (and bandwidth) to download the tool. In truth, that’s not always the case. For the GitHub-hosted runners, several versions of the common tools are downloaded into the tool cache when the image is built. This makes those versions immediately available (since the Action just needs to update the Path environment variable).
For self-hosted runners, you need to create a tool cache, which was covered in a previous post. By building it into the base image, you can avoid the need to download the tools for each runner. A common fear is that including tools will make the image excessively large. However, this fear is misplaced. The Actions Runner Controller (ARC) relies on Kubernetes, which downloads the image once per node. Once the image is downloaded, it is shared by all of the runner pods on that node. This means that the tools are only downloaded once, irrespective of the number of pods that use that image. That’s generally better than having each pod download the tools individually.
There are also other options available for self-hosted runners, such as using a volume (and mount) that can be shared between the runner pods. All solutions have some tradeoffs. For example, volumes require management to keep them up-to-date, considerations for the data transfer, and possible differences in the performance of a share compared to a disk.
Conclusion
If you’re relying on a command line tool for your build or deployment, consider using a GitHub tool to ensure the specific version you are using. This will improve the reliability of the system and make the dependency chain for your project more explicit. It will also give you an easy way to update the versions of the tools you are using for a job without impacting other processes that rely on the same runner image.