Skip to content

About

GitHub action to save / restore the coursier cache of your build

Resources

Stars

65 stars

Watchers

3 watching

Forks

Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

coursier cache action

A GitHub action that saves and restores the caches of Scala builds: the coursier cache, and, depending on the build tool your repository uses, the sbt or mill caches.

It looks at your build files to decide which caches to handle, and derives the cache keys from their content, so that the caches are updated when your build (and so, likely, its dependencies) changes, and only then.

Usage

Add a coursier/cache-action@v9 step to your workflow, after checking out your repository and before running your build:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: coursier/cache-action@v9
      - uses: coursier/setup-action@v3   # installs a JVM and sbt, if your runner doesn't have them
        with:
          apps: sbt
      - run: sbt test

The caches are restored when the step runs, and saved at the end of the job, if the job succeeds. Placing the step before the ones that install tools with coursier (like coursier/setup-action) lets them benefit from the cache too.

This action only handles caches: it doesn't install a JVM, sbt or mill itself.

No configuration is needed in most cases. The inputs let you

Cached directories

Cache Directories Cached when Output
Coursier ~/.cache/coursier/v1 (Linux), ~/Library/Caches/Coursier/v1 (macOS), ~\AppData\Local\Coursier\Cache\v1 (Windows), or COURSIER_CACHE / path if set Always cache-hit-coursier
sbt ~/.sbt, ~/.ivy2/cache sbt build files are found cache-hit-sbt-ivy2-cache
mill ~/.cache/mill mill files are found cache-hit-mill

Build files are looked for in the root directory, which is the root of the repository by default.

Coursier cache

Always cached. Only the cache itself is (v1, in the default OS-dependent location, or wherever the COURSIER_CACHE environment variable or the path input point), not the directories next to it, like the archive cache (arc, whose archives get extracted again from the ones in v1) or the privileged archive cache (priv, that has files only root can read).

Its key takes into account the build files of all the caches below (sbt and mill ones), as well as extraFiles.

sbt: ~/.sbt and ~/.ivy2/cache

Cached when any of these sbt files are found:

  • *.sbt
  • project/**.sbt
  • project/**.scala
  • project/build.properties

or when files matching extraSbtFiles are found.

Files that sbt and Ivy re-generate on each run (~/.ivy2/cache/**/ivydata-*.properties, and ~/.sbt/**/*.lock) are removed before saving the cache.

mill: ~/.cache/mill

Cached when any of these mill files are found:

  • build.mill, build.mill.yaml, build.mill.scala
  • mill-build/build.mill, mill-build/build.mill.yaml
  • .mill-* (like .mill-version)
  • .config/mill-* (like .config/mill-version)
  • mill, mill.bat, millw (mill launcher scripts)

or when files matching extraMillFiles are found.

Its key also takes into account the mill build definition files: *.mill, *.mill.yaml, *.mill.scala, **/package.mill, **/package.mill.yaml, **/package.mill.scala, and *.sc (for older mill versions).

How cache keys work

Each cache has its own key, made of the following parts, separated by -:

  1. the cache name (coursier, sbt-ivy2-cache or mill), followed, for all caches but the coursier one, by the OS and CPU architecture of the runner (like linux-x64 or darwin-arm64),
  2. the job id (see job and ignoreJob),
  3. matrix- followed by a hash of the matrix values, if the job uses a matrix (see matrix and ignoreMatrix),
  4. extraKey, if set,
  5. the cache-specific extra key (extraSbtKey, …), if set,
  6. a hash of the build files listed above, of extraFiles, and of extraHashedContent.

Parts that don't apply are left out. For example, the sbt cache of a test job with a matrix gets a key like

sbt-ivy2-cache-linux-x64-test-matrix-<matrix hash>-<build files hash>

When the job starts:

  • if a cache with that exact key exists, it is restored, the cache-hit-… output is true, and the cache isn't saved again at the end of the job, as it is up-to-date already,
  • else, the action falls back on the most recent cache whose key shares the longest prefix with that key, dropping the parts above from the end one by one (sbt-ivy2-cache-linux-x64-test-matrix-<matrix hash>-, then sbt-ivy2-cache-linux-x64-test-, then sbt-ivy2-cache-linux-x64-). The cache-hit-… output is false, and, as new dependencies may get downloaded during the job, the cache is saved under the new key at the end of the job. Set disableFallback to true to start from an empty cache instead.

Build files are hashed with LF line endings, so that checking them out with CRLF ones (core.autocrlf, on Windows) doesn't change the keys.

Caches are only saved if the job succeeds, and only if the cache mode allows it.

The action logs the key and the fallback keys of each cache, and which one was restored, if any, in the output of its step. If a cache isn't restored the way you expect, comparing these between runs usually shows which part of the key changed.

Inputs

All inputs are optional.

Input Default Description
root . Directory containing the build definition
path Path of the coursier cache, if not the default one
extraFiles Extra files to hash in all cache keys
extraSbtFiles Extra sbt build files
extraMillFiles Extra mill build files
extraKey Extra value added to all cache keys
extraCoursierKey, extraSbtKey, extraMillKey Extra value added to a single cache key
extraHashedContent Extra content to hash in all cache keys
extraCoursierHashedContent, extraSbtHashedContent, extraMillHashedContent Extra content to hash in a single cache key
read-cs-cache-files true Read extra files from .github/cs-cache-files
job ${{ github.job }} Job name, added to the cache keys
matrix ${{ toJson(matrix) }} Matrix values, hashed in the cache keys
ignoreJob false Leave the job name out of the cache keys
ignoreMatrix false Leave the matrix values out of the cache keys
disableFallback false Only restore caches whose key matches exactly
cacheMode Restrict cache access for this step (read, write, write-only, none)

root

Directory containing the build definition sources (build.sbt, build.mill, etc.), relative to the repository root. Defaults to the repository root.

If the sbt or mill build definition files are in a sub-directory, pass the path to this sub-directory here:

- uses: coursier/cache-action@v9
  with:
    root: backend

path

Path of the coursier cache.

By default, the coursier cache is assumed to be at the location the COURSIER_CACHE environment variable points to, if it is set when the action runs, else in the default OS-dependent location. Set this input to override that. The action then also sets the COURSIER_CACHE environment variable for the subsequent steps, so that you don't have to set it yourself.

extraFiles

Extra files to take into account in the keys of all caches.

Pass either

  • a single path or glob as a string,
  • multiple paths or globs in a JSON array, encoded in a string.

Globs are processed by @actions/glob. Paths are relative to the repository root, not to root.

- uses: coursier/cache-action@v9
  with:
    extraFiles: '["dependencies.json", "scripts/*.sh"]'

Files can also be listed, one path or glob per line, in a .github/cs-cache-files file in your repository. If that file exists, its entries are added to extraFiles, unless read-cs-cache-files is false.

extraSbtFiles

Extra sbt build files, same format as extraFiles. They are taken into account in the sbt and coursier cache keys, and finding any of them enables the sbt cache.

extraMillFiles

Extra mill build files, same format as extraFiles. They are taken into account in the mill and coursier cache keys, and finding any of them enables the mill cache.

extraKey

Extra value added as is to the keys of all caches. See How cache keys work.

Use it to have separate caches for jobs that would get the same keys otherwise. For values that don't fit in a cache key, or that you'd rather not show in the logs, use extraHashedContent.

extraCoursierKey, extraSbtKey, extraMillKey

Same as extraKey, but only added to the key of the coursier, sbt or mill cache.

extraHashedContent

Extra content to take into account in the keys of all caches. It is hashed along with the build files, so changing it gives new caches, initialized from the previous ones (see How cache keys work).

extraCoursierHashedContent, extraSbtHashedContent, extraMillHashedContent

Same as extraHashedContent, but only taken into account in the key of the coursier cache (extraCoursierHashedContent), or in the key of the sbt or mill cache and in the key of the coursier cache (extraSbtHashedContent, extraMillHashedContent).

read-cs-cache-files

Default: true

Set to false to ignore the .github/cs-cache-files file (see extraFiles).

job

Job name, added to the cache keys, so that each job has its own caches. Defaults to the id of the current job (${{ github.job }}). See also ignoreJob.

matrix

Matrix values, hashed and added to the cache keys, so that each instance of a matrix job (one per Scala version, JDK, OS, …) has its own caches, and those caches don't collide. Defaults to the matrix values of the current job (${{ toJson(matrix) }}), so that you don't need to set it in most cases. See also ignoreMatrix.

ignoreJob

Default: false

Set to true to leave the job name out of the cache keys, so that different jobs share their caches.

ignoreMatrix

Default: false

Set to true to leave the matrix values out of the cache keys, so that the instances of a matrix job share their caches.

disableFallback

Default: false

Set to true to only restore caches whose key matches exactly. By default, when there's no exact match, the action restores the closest cache, which can come from another job or matrix instance (see How cache keys work). Disabling the fallback prevents sharing caches across jobs or matrix instances with different dependencies, at the cost of starting from an empty cache whenever the build files change.

cacheMode

Default: empty (use the workflow / job cache-mode as is)

Cache access for this step: one of read, write, write-only, or none. This can only narrow the access granted by the workflow or job cache-mode, never widen it. For example, pass read to restore caches without saving them.

Outputs

Output Description
cache-hit-coursier true if the coursier cache was restored from an exact key match
cache-hit-sbt-ivy2-cache true if the sbt cache was restored from an exact key match
cache-hit-mill true if the mill cache was restored from an exact key match

Each output is false when no cache or only a fallback one was restored, and empty when the corresponding cache isn't handled (no sbt files for cache-hit-sbt-ivy2-cache, for example).

Skipping steps based on cache-hit

Using the cache-hit-… outputs, subsequent steps can be skipped when a cache was restored from an exact key match. Give the action step an id, and refer to it in the if condition of the steps to skip:

steps:
  - uses: actions/checkout@v7

  - uses: coursier/cache-action@v9
    id: coursier-cache

  - name: Fetch dependencies
    if: steps.coursier-cache.outputs.cache-hit-coursier != 'true'
    run: sbt +update

Cache access (cache-mode)

GitHub Actions lets workflows restrict their access to the cache with the cache-mode key, set at the workflow level, at the job level, or both (a job-level value overrides the workflow-level one). This action honors it, like actions/cache does:

cache-mode Caches restored Caches saved
write Yes Yes
read Yes No
write-only No Yes
none No No

When cache-mode isn't set, GitHub picks write for trusted triggers (push, schedule, workflow_dispatch, …) and read for low-trust ones (pull_request_target, issue_comment, workflow_run, …).

When the mode doesn't permit an operation, the action logs a message and moves on, and the job doesn't fail. A skipped restore counts as a cache miss, so the cache-hit-… outputs are false. A skipped save just doesn't happen.

For example, to have a job restore caches without ever saving them:

jobs:
  test:
    runs-on: ubuntu-latest
    cache-mode: read
    steps:
      - uses: actions/checkout@v7
      - uses: coursier/cache-action@v9
      - run: sbt test

To restrict only this action, and leave other caching steps of the job unaffected, use the cacheMode input instead:

steps:
  - uses: actions/checkout@v7
  - uses: coursier/cache-action@v9
    with:
      cacheMode: read   # restore the coursier / sbt / mill caches, but never save them

cacheMode can only narrow what the workflow or job grants. Combining cacheMode: read with a job cache-mode: write-only, for example, gives none. The input also works on runners that don't support the cache-mode key, such as older GitHub Enterprise Server releases.

Read-only caches in pull requests, read / write on main

Caches saved by pull_request runs are only visible to that pull request, but they still count towards the repository cache quota, and can get caches from main evicted. To have pull requests reuse the caches of main without saving their own, while main keeps updating them, compute cacheMode from the branch:

on:
  push:
    branches: [main]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: coursier/cache-action@v9
        with:
          cacheMode: ${{ github.ref == 'refs/heads/main' && 'write' || 'read' }}
      - run: sbt test

On main, the caches are restored, then saved if their key changed. In pull requests (github.ref is refs/pull/<number>/merge there), the caches of main are restored, but nothing is saved. Pull requests whose build files differ from main fall back on the closest cache from main, as described in How cache keys work.

GitHub doesn't document ${{ … }} expressions as supported in the cache-mode key, so getting the same behavior with it means separate workflows, such as one triggered on push to main (where the default is write) and another one triggered on pull_request that sets cache-mode: read at the top level.

Cache invalidation

As a cache restored from a fallback key is saved again under the new key with everything it already had, caches only grow over time, and keep dependencies your build doesn't use anymore. Invalidating them from time to time gets rid of those.

To force the caches to be saved again without changing your build files, set the COURSIER_CACHE_ACTION_CACHE_VERSION environment variable in your workflow. Changing its value changes the keys of all caches, so that they're saved under new keys at the end of the job.

Like for any other key change, the previous caches are still restored as a fallback when the job starts. To start from empty caches, also set disableFallback to true for one run.

The variable can be set at the workflow level so that it applies to every step:

env:
  COURSIER_CACHE_ACTION_CACHE_VERSION: 1   # increment this value to invalidate the caches

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: coursier/cache-action@v9

Or it can be scoped to only the coursier/cache-action step:

steps:
  - uses: actions/checkout@v7
  - uses: coursier/cache-action@v9
    env:
      COURSIER_CACHE_ACTION_CACHE_VERSION: 1   # increment this value to invalidate the caches

This is equivalent to passing the version as extraHashedContent, but more convenient, as it can be set for the whole workflow (or even as a repository variable or secret) without touching the individual steps.

About

GitHub action to save / restore the coursier cache of your build

Resources

Stars

65 stars

Watchers

3 watching

Forks

Releases

Contributors

Languages