One of the advantages of moving to the latest versions of .NET (Core and 5) is building our applications on many different operating systems. The most significant benefits come in our choices for build agents in our continuous integration tools like GitHub Actions. One of the more popular options is Ubuntu, a Linux variant.
For the most part, the images up on GitHub work with .NET Core, but recently I’ve also been using a local runner named Act. The tool works by utilizing docker container images that mimic the GitHub build agents. Adopting Act allows me to test build workflows locally, avoiding the constant edit, push, and pray feedback loop associated with GitHub Actions.
I am using Act’s Medium image for my local workflow build agent, which is about 500 MB and has a good cross-section of dependencies.
A .NET Focused GitHub Workflow
While a great tool, it’s not perfect, as I recently found when building a .NET Core focused workflow. I have the following workflow in my .NET repository:
name: "Build" on: push: branches: - main paths-ignore: - '**/*.md' - '**/*.gitignore' - '**/*.gitattributes' workflow_dispatch: branches: - main paths-ignore: - '**/*.md' - '**/*.gitignore' - '**/*.gitattributes' jobs: build: if: github.event_name == 'push' && contains(toJson(github.event.commits), '***NO_CI***') == false && contains(toJson(github.event.commits), '[ci skip]') == false && contains(toJson(github.event.commits), '[skip ci]') == false name: Build runs-on: ubuntu-latest env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true DOTNET_GENERATE_ASPNET_CERTIFICATE: false DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false DOTNET_MULTILEVEL_LOOKUP: 0 steps: - uses: actions/checkout@v2 - name: Setup .NET Core SDK uses: actions/setup-dotnet@v1 with: dotnet-version: 5.0.x - name: Restore run: dotnet restore - name: Build run: dotnet build --configuration Release --no-restore - name: Test run: dotnet test
Running the following workflow from the command-line using
act returns the following build output:
[Build/Build] ⭐ Run Restore | Process terminated. Couldn't find a valid ICU package installed on the system. Set the configuration flag System.Globalization.Invariant to true if you want to run with no globalization support. | at System.Environment.FailFast(System.String) | at System.Globalization.GlobalizationMode.GetGlobalizationInvariantMode() | at System.Globalization.GlobalizationMode..cctor() | at System.Globalization.CultureData.CreateCultureWithInvariantData() | at System.Globalization.CultureData.get_Invariant() | at System.Globalization.TextInfo..cctor() | at System.String.ToLowerInvariant() | at Microsoft.DotNet.Cli.Utils.EnvironmentProvider.GetEnvironmentVariableAsBool(System.String, Boolean) | at Microsoft.DotNet.Cli.Program.ProcessArgs(System.String, Microsoft.DotNet.Cli.Telemetry.ITelemetry) | at Microsoft.DotNet.Cli.Program.Main(System.String) | /github/workflow/2: line 1: 234 Aborted dotnet restore [Build/Build] ❌ Failure - Restore Error: exit with `FAILURE`: 134
Yikes! Failure, specifically on the first call to the
restore. What’s the issue? Ah, the image we used does not have the ICU package installed. There are three solutions to get past this particular issue.
Disabling Globalization and Use Invariant Everything
We can set an environment variable and disable the need for globalization in our build workflow. Add the following line to the end of our
Below is the updated
env collection with the new environment variable.
jobs: build: if: github.event_name == 'push' && contains(toJson(github.event.commits), '***NO_CI***') == false && contains(toJson(github.event.commits), '[ci skip]') == false && contains(toJson(github.event.commits), '[skip ci]') == false name: Build runs-on: ubuntu-latest env: DOTNET_CLI_TELEMETRY_OPTOUT: 1 DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 1 DOTNET_NOLOGO: true DOTNET_GENERATE_ASPNET_CERTIFICATE: false DOTNET_ADD_GLOBAL_TOOLS_TO_PATH: false DOTNET_MULTILEVEL_LOOKUP: 0 DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1
I do not recommend taking this approach, as it can have ramifications on unit tests and integration tests run in the context of the build agent.
If you’d like to learn more about this particular flag, the .NET team has documented the advantages and disadvantages in this GitHub document.
Installing ICU Using Apt-Get
Another option is to install the missing ICU package. We can successfully build our workflow after adding the following commands. Remember, we are running on an Ubuntu image.
- name: Install ICU packages run: | sudo apt-get update sudo apt-get -y install libicu-dev
It’s important to add
-y to the second call to
apt-get to avoid our workflow from waiting for human confirmation. This step adds additional build time to our workflow and might not be completely necessary since GitHub’s build agent works without this step using
Use The “Large” Build Image
As I mentioned in the introduction, I used the
Medium container image, which is about 500MB in size. Switching to the
Large container image doesn’t require adding the additional workflow step. The
Large image does have one big drawback: the images whopping 18 gigabytes of disk space. That’s one chonky boi.
To switch to the larger build image, remove the
.actrc file and rerun the
> rm ~/.actrc > act
Which should re-prompt Act to ask you to choose the image again.
? Please choose the default image you want to use with act: - Large size image: +20GB Docker image, includes almost all tools used on GitHub Actions (only ubuntu-latest/ubuntu-18.04 platform is available) - Medium size image: ~500MB, includes only necessary tools to bootstrap actions and aims to be compatible with all actions - Micro size image: <200MB, contains only NodeJS required to bootstrap actions, doesn't work with all actions Default image and other options can be changed manually in ~/.actrc (please refer to https://github.com/nektos/act#configuration for additional information about file structure) [Use arrows to move, type to filter, ? for more help] Large > Medium Micro
The Act tool is fantastic for testing GitHub Action workflows locally, but it does have some edge cases. Notably, the smaller images don’t have all the features a GitHub build image would have. We say that we can mitigate the ICU issue using three approaches: Disable globalization and adopt an invariant environment. Install an ICU package. Use the largest build agent image. If you are building .NET applications on other Linux build agents, you will likely run into a similar issue. Adopting the fixes into the workflow definition might result in the most consistent results across build agent variants, with some overhead on build times.
I hope you found this post helpful, and please leave a comment as to which approach you ended up choosing. As always, thanks for reading.