article

Caching in GitHub Actions

Unlike Jenkins which usually runs builds in the same environment (unless configured differently, it works similar to comparable services like Travis. Every build is executed in a clean (docker) container and will checkout the projects and install required dependencies.

GitHub Actions Introduction

GitHub actions is great, and having a clean environment every time a build is started means that nothing from previous builds can affect the most recent ones.

This comes with a big price though.

Everything has to be built fresh every single time, and that takes time. Fetching dependencies, configuring gradle, fetching git-lfs files, building the project clean.

Ultimately this translate into real world cost, if the free build quota or (if you use git-lfs) the git-lfs bandwidth quota get exhausted.

Luckily there's a solution to this problem: Github Actions Caching.

Caching allows to backup any folder of your build environment to the GitHub Actions cache, and fetch it again the next time a build starts.

For the cache to properly and safely function, you associate a key which can be generated from your source with the cacheable data. When starting the next build, it will compare the cache-keys and download and unpack the cache to the old location.

This is great and will help to keep the build times fast, and safe your git-lfs quota.

One caveat of it is the current hard limit of 5GB though. (There's a plan to raise this limit though)

Enable Cache

To setup the cache the official actions/cache@v2 action from GitHub has to be used.

The action will automatically take care of fetching the cache when the build starts, and upload the cache after the build succeeded.

A great example in which the cache makes a lot of sense are for example Android Gradle projects.

- uses: actions/cache@v2
  with:
    path: |
      ~/.gradle/caches
      ~/.gradle/wrapper
      !~/.gradle/wrapper/dists/**/gradle*.zip
    key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}
    restore-keys: |
      ${{ runner.os }}-gradle-

That looks like a lot initially but let's brake it down:

We use the cache action provided by GitHub.

- uses: actions/cache@v2

We want to backup the caches and the wrapper folder.

path: |
  ~/.gradle/caches
  ~/.gradle/wrapper

But we want to exclude the zipped wrapper as the .gradle folder will contain the unpacked executable already.

  !~/.gradle/wrapper/dists/**/gradle*.zip

As the unique cache key we will use the operating system (usually caches depend on the OS they were generated on), a unique section -gradle- to indicate this cache is for the gradle files, and a generated hash based on all *.gradle files.

key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*') }}

This hash is important as it will ensure that the best fitting cache will be pulled from the caches, resulting in the cache being much more effective.

The last but also very important bit is the restore-keys

restore-keys: |
  ${{ runner.os }}-gradle-

If the cache won't find an exact match with the key, it will fallback to the restore-keys and pick the second, third, ... most fitting cache.

Assuming we have a hash of 1234 and were using linux this would result in the following order:

1) linux-gradle-1234
2) linux-gradle-

Looking for a cache with key linux-gradle-1234 we will find an exact match and pick 1, also the cache won't be uploaded after the build finished, as we already got it.

For a cache with key linux-gradle-4321, we'd fallback to 2, as no exact match is found, after a build success it will then upload the cache with this exact key, and keep it for 7 days or until 5gb cache size are reached.

GitHub Actions Cache Miss
GitHub Actions Cache Miss
GitHub Actions Cache Hit
GitHub Actions Cache Hit

Additional Cache Examples

Android Build Cache

- name: Checkout Android Cache
  uses: actions/cache@v2
  with:
    path: ~/.android/build-cache
    key: android-${{ runner.os }}-${{ hashFiles('**/*.gradle*') }}
    restore-keys: |
    android-${{ runner.os }}-

Git-Lfs Cache

- name: Create git-lfs Cache Key
  run: git lfs ls-files -l | cut -d' ' -f1 | sort > .lfs-assets-id

- name: Cache git-lfs
  uses: actions/cache@v2
  with:
    path: .git/lfs
    key: lfs-${{ runner.os }}-${{ hashFiles('.lfs-assets-id') }}
    restore-keys: |
    lfs-${{ runner.os }}-

Even more cache examples can be found in the cache action repo here.

Feedback

Got thoughts, feedback, improvements, suggestions, or comments?

Let me know @mike_penz