Caching dependencies on GitHub Actions
I previously wrote about caching gems on CircleCI and although this is even easier to achieve with GitHub Actions there’s still a useful approach worth applying that will minimise the overall time spent installing dependencies when a workflow contains many jobs.
The thing I want to prevent is each job having to download and install its dependencies, when this occurs one of the jobs will finish first and write to the cache and then we’ll see something like Cache hit occurred on the primary key BIG-LONG-KEY, not saving cache
from the Post Run actions/cache@v2
output in other jobs. Nothing will blow up, but all of those separate jobs installing dependencies are wasting billable time.
The simple fix is to declare an initial job whose sole purpose is to do the work of installing dependencies and to make them available for the other jobs. Rather than store the installed dependencies as a workflow artifact I prefer to treat this step as warming a cache — which may be a little more verbose but I think is simpler overall (also, with this approach, the cache is treated as a performance optimisation and each dependant job is still able to run if it isn’t present for for some reason).
Caching gem dependencies
Here’s an example of installing gems, note the job cache_gems
and how the other jobs declare it as a dependency by specifying needs: cache_gems
:
on: [push]
jobs:
cache_gems:
name: Cache gems
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
# We always need to tell ruby/setup-ruby to cache the gems for us.
bundler-cache: true
brakeman:
name: Brakeman
runs-on: ubuntu-latest
# Tell this job to wait for the `cache_gems` job to successfully complete
# before it runs.
needs: cache_gems
steps:
- uses: actions/checkout@v3
# Each dependant job still need to install Ruby and ensure that it reads
# from the cache before installing the gems (which is as simple as
# specifying `bundler-cache: true` when using ruby/setup-ruby).
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
# Now run your actual CI command.
- run: bundle exec brakeman --quiet --run-all-checks
rspec:
name: RSpec
runs-on: ubuntu-latest
needs: cache_gems
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: bundle exec rspec
rubocop:
name: Rubocop
runs-on: ubuntu-latest
needs: cache_gems
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- run: bundle exec rubocop --parallel
Caching JavaScript dependencies
I use the same technique for installing JavaScript dependencies but it’s a tiny bit more involved because while ruby/setup-ruby takes care of caching and installing for us actions/setup-node only does the caching so we have to do the install ourselves.
cache_node_modules:
name: Cache node_modules
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
# I'm using Yarn so I select the yarn caching strategy so setup-node will
# read/write from Yarn's global install location.
- uses: actions/setup-node@v3
with:
cache: 'yarn'
# Now actually install the dependencies (which actually reads from Yarn's
# global install location).
- run: yarn install --frozen-lockfile
jest:
name: Jest
runs-on: ubuntu-latest
needs: cache_node_modules
steps:
- uses: actions/checkout@v3
# Mirror the cache job's setup-node steps.
- uses: actions/setup-node@v3
with:
cache: 'yarn'
- run: yarn install --frozen-lockfile
# And here's your CI command.
- run: yarn test
prettier:
name: Prettier
runs-on: ubuntu-latest
needs: cache_node_modules
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v1
with:
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn prettier --check app/javascript
Running system tests
Running system tests may require both Ruby and JavaScript dependencies which can be achieved with the following changes:
rspec:
name: RSpec
runs-on: ubuntu-latest
- needs: cache_gems
+ needs: [cache_gems, cache_node_modules]
steps:
- uses: actions/checkout@v3
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
+ - uses: actions/setup-node@v3
+ with:
+ cache: 'yarn'
+ - run: yarn install --frozen-lockfile
- run: bundle exec rspec
Updated 2022-09-05: Updated Node examples to use actions/setup-node v3 and take advantage of its inbuilt caching.