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 dotnet-cli during 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 env collection.

DOTNET_SYSTEM_GLOBALIZATION_INVARIANT: 1

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 ubuntu-latest.

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 act command.

> 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

Conclusion

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.