Introduction
GitHub Actions provide two ways of storing files: caching for things like dependencies
and artifacts
for the results of a job, such as logs or binaries. Although they sound similar, they are used for different purposes. So, we will use caching to speed up our workflow runs.
Another thing to know is that cache access will be restricted to only a few branches: the current branch
, the base branch
for pull requests
, and the default branch
. Caches created in unrelated branches won't be available, but that's not something that will affect us in most cases, since we usually operate on either current or base branches.
Proper use of caching can help reduce build times, especially for projects whose dependencies do not change very often.
Choosing the Right Cache Key
We can create metadata-based cache keys, such as OS or commit hashes, that let us only reuse dependencies when absolutely needed. We can also make use of restore keys to get near matches for caches that help minimize rebuild times.
Generally speaking, for such workflows running on multi-OS environments, it is also a good practice to isolate the caches per-OS to avoid cross-platform unnecessary rebuilds. Sometimes we might even use temporary caches valid for only one run, which is useful when we do not need long-term reuse.
- uses: actions/cache@v4
with:
path: path/to/dependencies
# generate a cache key based on the hash of lockfiles
key: cache-${{ hashFiles('**/lockfiles') }}
- uses: actions/cache@v4
with:
path: path/to/dependencies
key: cache-${{ hashFiles('**/lockfiles') }}
# restore keys for closest matches. This minimizes the time spent downloading newer dependencies
restore-keys: |
cache-npm-
- uses: actions/cache@v4
with:
path: path/to/dependencies
#key: cache-${{ hashFiles('**/lockfiles') }}
# OS-specific caches to avoid unnecessary rebuilds across different platforms
key: ${{ runner.os }}-cache
restore-keys: |
cache-npm-
- uses: actions/cache@v4
with:
path: path/to/dependencies
#key: cache-${{ hashFiles('**/lockfiles') }}
#key: ${{ runner.os }}-cache
# short-lived caches for one-time use
key: cache-${{ github.run_id }}-${{ github.run_attempt }}
restore-keys: |
cache-npm-
We can also share caches across jobs, centralizing cache creation to save time, and ensure caches are saved even if a build fails by using the always()
condition.
# Centralized cache reuse
- id: cache-primes-save
uses: actions/cache/save@v4 # Save cache
with:
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
- uses: actions/cache/restore@v4 # Restore cache
with:
key: ${{ runner.os }}-${{ hashFiles('**/lockfiles') }}
# Save cache after build failure
- uses: actions/cache@v4
with:
path: path/to/dependencies
key: cache-${{ hashFiles('**/lockfiles') }}
if: always()
GitHub Actions Package Manager Caching Examples
I've assembled some sample caching for the various package managers in use: npm
, pip
, Maven
, and NuGet
. The following configuration should avoid the redownload of dependencies in each workflow run. For example, we do not cache node_modules in the case of npm in order to avoid the case where different Node versions will create problems.
Instead, we cache npm
itself dynamically. Pip's cache must be different depending on the OS, and we do the same on Maven
and NuGet
for their respective repository paths and lock files.
# Cache npm dependencies
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-node-
# Cache pip dependencies
- uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
# Cache Maven dependencies
- uses: actions/cache@v4
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
# Cache NuGet dependencies
- uses: actions/cache@v4
with:
path: ~/.nuget/packages
key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }}
restore-keys: ${{ runner.os }}-nuget-
Conclusion
Caching in GitHub Actions means how to optimize the continuous integration workflow by basically avoiding duplicate tasks, for example, re-downloading the dependencies. Carefully selecting metadata, operating system, or lock file-based cache keys, finding close matches with restore keys, and keeping the number less will highly reduce build times. Keep separate caches at different operating systems and use transient caches for one-time build.