IMPORTANT: Developer documentation for the current development branch. This content is unreleased, may change without notice, and must not be treated as Buildish release documentation.

Maven Caching Patterns

How to get the most out of Maven caching — what is portable in the local repository, why certain files are excluded, and how to structure workflows for maximum cache effectiveness.

What gets cached

The Maven action caches the local repository (~/.m2/repository by default, covered by the repository partition) and, when the Maven Wrapper is in use, the wrapper distribution downloads (~/.m2/wrapper/dists, covered by the wrapper-dists partition).

The local repository is content-addressed: each artifact sits at a path derived from its group ID, artifact ID, and version, and the file content is identical regardless of which runner downloaded it. This makes it straightforwardly portable across runners, operating systems, and architectures — a cache entry saved on one runner is fully usable on another.

Default timestamp garbage collection

Maven local repositories tend to grow monotonically because old dependency versions remain valid even after the project stops using them. The action therefore enables cache-gc-mode: timestamp by default for both Maven and Gradle.

For Maven this means managed local-repository files are pruned before standalone or distributed-aggregator jobs save the base cache when both their modification time and effective access time are older than cache-gc-older-than-days (14 by default). This keeps GitHub Actions cache entries from growing indefinitely while still allowing Maven to redownload an old artifact if the build needs it again.

If a workflow intentionally relies on a large offline-style local repository, either increase the cutoff or disable GC:

1cache-gc-older-than-days: '30'
2# or:
3cache-gc-mode: off

Why certain files are always excluded

Four categories of file are unconditionally excluded from every cache partition because they carry per-runner or per-session state that corrupts the build when shared:

****/\*.lastUpdated** — written by Maven each time it checks a remote repository for an artifact or metadata update. The file records the timestamp of the check and the outcome. When restored on a different runner, Maven skips the remote check because the file is present, potentially missing a newer version that has since been published. Always excluding these files forces Maven to perform fresh remote checks rather than relying on stale markers.

**/resolver-status.properties — written by Maven Resolver (Aether) at the group/artifact metadata level (e.g. org/apache/maven/plugins/resolver-status.properties) to track which remote repositories have been contacted and what status each returned. It has the same per-runner semantics as .lastUpdated. In distributed multi-job builds it is particularly problematic: every worker job that invokes any Maven goal also resolves Maven plugins, so every worker writes its own version of this file for the plugins group. Because the files differ in content across workers, the aggregator cannot merge them safely — they appear as a conflict. Excluding them prevents the conflict from arising.

**/\_remote.repositories — records, for each artifact in the local repository, which remote repository it was originally resolved from. If the new runner’s settings.xml references different repositories, the marker can cause Maven to silently skip re-resolving an artifact it would otherwise re-download from the correct source. Excluding these markers keeps resolution honest.

****/\*.lock** — transient file locks used by the Maven resolver to serialize concurrent access. A lock file restored from cache references a stale PID and will cause hangs or silent failures when the resolver tries to acquire it.

Maximizing cache hit rates

Commit your POM files and nothing else

The cache key includes a fingerprint of the active partition layout. The content of your POMs does not factor into the key (unlike Gradle’s dependency locking files). This means:

  • Cold starts occur only when the partition layout changes, not on every dependency update.
  • Warming the cache is fastest when the first run resolves all dependencies end-to-end.
  • Running mvn dependency:go-offline as a dedicated pre-build step is not needed and slows down the first run unnecessarily — let the build resolve naturally and the action will cache whatever lands in the local repository.

Use mvn verify or mvn install, not lifecycle fragments

Maven’s build lifecycle writes many artifacts to the local repository only during specific phases. Running mvn compile alone will not populate the repository with as many artifacts as mvn verify or mvn install. Use the same goal in your caching workflow that you would use locally to ensure the cache is populated completely on the first warm run.

Multi-module projects and parallel goals

For multi-module projects, the entire local repository is captured as a single delta after the build completes. You do not need to structure goal invocations specially — just ensure the final invocation runs all modules (mvn -T 1C verify or similar) so every module’s output reaches the local repository before the finalize step runs.

If you use the distributed multi-job mode (separate modules in separate jobs), each worker captures only the repository changes it produced. The aggregator merges all worker deltas into the next cache entry, so every module’s downloaded dependencies are available on the next run regardless of which worker downloaded them.

Structuring staged workflows

Some projects run goals in stages — for example, build and test in parallel, then deploy separately. A typical pattern:

 1jobs:
 2  build-and-test:
 3    runs-on: ubuntu-latest
 4    permissions:
 5      contents: read
 6    steps:
 7      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
 8      - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
 9        with:
10          distribution: temurin
11          java-version: '21'
12      - uses: buildish-tooling/buildish-mammoth-cache/actions/github/maven@<commit-sha>
13      - run: mvn verify
14
15  deploy:
16    needs: build-and-test
17    runs-on: ubuntu-latest
18    permissions:
19      contents: read
20    steps:
21      - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd
22      - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
23        with:
24          distribution: temurin
25          java-version: '21'
26      - uses: buildish-tooling/buildish-mammoth-cache/actions/github/maven@<commit-sha>
27        with:
28          read-only: true
29      - run: mvn deploy -DskipTests

The deploy job uses read-only: true so it benefits from the warm cache populated by build-and-test without writing a competing cache entry.

Goal-level incremental caching

Maven does not have a built-in equivalent to Gradle’s task-output caching. Plugin-based extensions exist that add goal-level incremental caching to Maven, analogous to Gradle’s build cache. These operate independently of this action and require separate configuration. This action is compatible with such extensions — the files they produce in the local repository are captured and cached alongside regular dependency artifacts.