Github has really improved lately with the addition of Github Actions. I’ll just refer to them as GA from here on. It’s very easy to add continuous integration tests and builds of your code if you’re using one of their supported languages like Node.js, Python or Ruby. But what if you want to test your Nim project? What are your options?

Compile nim from scratch

GA’s are declared in YAML and let you run arbitrary commands. We could download and compile a version of Nim and use it for testing. Let’s see what that looks like.

Downloading nim

env:
  NIM_VERSION: 1.2.0

steps:
- Install Nim
  run: |
    export CHOOSENIM_CHOOSE_VERSION="$NIM_VERSION"
    curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh
    sh init.sh -y

This will download and install the version of nim we specify. If you’re not familiar with choosenim, you’re missing out. Rather than having a single version of nim installed globally, it manages versions and lets you easily switch between them.

In our next step we can go ahead and run tests for our project. I’m assuming you’ve got a nimble project. If you have something different, you might have to tweak these instructions.

steps:
# [...]
- name: Run Tests
  run: |
    export PATH=$HOME/.nimble/bin:$PATH
    echo $PATH
    nimble test -y

We have to add the recently installed nim compiler and nimble executable to our path. GA’s don’t maintain environment variable state between steps so we have to do this in every step that uses nim or nimble.

We echo our current path so it shows up in the logs. This will help us track down any problems. Finally we actually run the tests using nimble and pass in -y to let it automatically download any nim module dependencies.

Speaking of dependencies, choosenim depends on openssl and curl being installed on the system. I’ve found that running on ubuntu-18.04 or ubuntu-latest worked fine as they already had everything I needed.

If we save this file in .github/workflows/testing-native-1.yml, then our project will run it every time we push code to the server.

After a few times of running this, you’ll notice there is a problem. We are downloading and building the nim compiler each and every time we push code to do testing. It would be great if we could cache the compiler we built last time and then reuse it.

Caching

There is a GA that enables caching a directory to a key. On subsequent runs we can download the compiler using that same key.

- name: Cache choosenim
  id: cache-choosenim
  uses: actions/cache@v1
  with:
    path: ~/.choosenim
    key: ${{ runner.os }}-choosenim-${{ env.NIM_VERSION }}

So we’re saying that we want to cache the ~/.choosenim/ directory under that long key. The key will end up looking like ubuntu-18.04-choosenim-1.2.0. That way if we change the OS or our version of nim, we’ll get a cache miss and we’ll download and build the version we need.

Now some of you may have remembered that we had set our path variable to point to $HOME/.nimble/bin before. But we’re not caching that directory here. You’re right. We need to cache that directory as well.

- name: Cache nimble
  id: cache-nimble
  uses: actions/cache@v1
  with:
    path: ~/.nimble
    key: ${{ runner.os }}-nimble-${{ env.NIM_VERSION }}

How would you know which directories you need to cache? In the case of nim, at the time I’m writing this, you just need to cache these two directories. But you know, things change. Who knows in the future? For me this was a lot of trial and error as I reviewed the GA log to chase down what was missing when things didn’t work. You may have to do something similar.

We’re now caching two directories. The next step is we need to modify our step that installs nim to use this new fancy cache we’ve defined.

- name: Install Nim
  if: steps.cache-choosenim.outputs.cache-hit != 'true' || steps.cache-nimble.outputs.cache-hit != 'true'
  run: |
    export CHOOSENIM_CHOOSE_VERSION="$NIM_VERSION"
    curl https://nim-lang.org/choosenim/init.sh -sSf > init.sh
    sh init.sh -y

I didn’t talk about the id fields we defined on our caches earlier. They’re used here to decide whether we have a cache hit and can just skip the install. Basically if either of the keys gets a cache miss, then do the full install.

That’s it. You can save the full source to .github/workflows/testing-native-2.yml and try it yourself.

Docker

In this age of containers some of you have to be wondering, “what about docker”? GA is flexible and lets us use Docker containers. Furthermore, moigagoo is providing prebuilt nim docker images for ubuntu and alpine.

That makes our GA file a lot simpler. We can rely on docker to download the image and enable us to run testing inside of it.

- name: Test
  run: >
    docker run -v `pwd`:/usr/src/app -w /usr/src/app
    nimlang/nim:$NIM_VERSION-$OS_VERSION
    nimble test -y

Save this file as .github/workflows/testing-docker-1.yml and try it out. It’s a great simple GA.

Turn it up to 11

Great, now we’re testing nim 1.2.0 on ubuntu. What about if we want to support multiple versions of nim? What if we want to support alpine as well? Are we going to have to write more GA files? No, we don’t have to. GA has a cool feature where you can parameterize the file and it will generate jobs on the fly.

Let’s change our file so that we’re testing older nim versions and whatever the latest is as well.

strategy:
  matrix:
    NIM_VERSION: [1.0.2, 1.0.6, 1.2.0, latest]
    OS_VERSION: [ubuntu, alpine]

[...]

- name: Test
  run: >
    docker run -v `pwd`:/usr/src/app -w /usr/src/app
    nimlang/nim:${{ matrix.NIM_VERSION }}-${{ matrix.OS_VERSION }}
    nimble test -y

Now when this GA runs, instead of a single job it will turn into 8 actual jobs. Two OS versions times four versions of nim equals eight jobs.

Save this file as .github/workflows/testing-docker-2.yml and see it in action. It’s glorious!

Oh and it will eat up your build minutes as well, so turn off what you don’t need.

Troubleshooting

moigagoo is building the docker images with a set of OS dependencies such as openssl, postgresql, etc. Your needs may differ as you may need more libraries, different versions or want fewer ones to reduce the footprint. Either way, you may have to run apt-get on your image to add/substract packages.

Summary

You have a couple options for testing nim code on Github Actions today. You can compile nim yourself or you can use a docker image. Either way, it’s not a lot of code. use caches or restricting how much you use matrix testing so you don’t burn through your build minutes quickly.

Resources

Source code

External