Skip to content

Commit 03ad4de

Browse files
brunoborgesCopilot
andauthored
Backport #1097/#1098: cache Maven and Gradle wrapper distributions separately (#1122)
Backports the wrapper caching fix to the v5 release line. The Maven wrapper distribution (~/.m2/wrapper/dists) and Gradle wrapper distribution (~/.gradle/wrapper) were cached in the same entry as the dependency cache, keyed on the volatile dependency-file hashes (**/pom.xml, **/*.gradle*). With no restoreKeys fallback (#269), almost every dependency change was a full cache miss, forcing ./mvnw and ./gradlew to re-download the build tool distribution and hitting intermittent rate-limit failures. Each wrapper distribution now lives in its own additional cache entry keyed only on the rarely-changing wrapper properties file (**/.mvn/wrapper/maven-wrapper.properties, **/gradle-wrapper.properties), so it survives dependency changes. Optional-cache saves swallow ValidationError and ReserveCacheError so a project without a wrapper never fails the post step. Fixes: #1095 Copilot-Session: ea015caa-980a-4e0a-af20-3fc9f39c77fd Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent d229d2e commit 03ad4de

5 files changed

Lines changed: 581 additions & 40 deletions

File tree

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,7 @@ The workflow output `cache-primary-key` exposes the primary cache key computed b
149149

150150
The cache input is optional, and caching is turned off by default.
151151

152-
**Maven Wrapper:** when `cache: 'maven'` is enabled, the action also caches and restores the Maven Wrapper distribution downloaded to `~/.m2/wrapper/dists` (in addition to the local repository), so wrapper-based (`./mvnw`) builds don't re-download the wrapper on every run. This is keyed on `**/.mvn/wrapper/maven-wrapper.properties` as shown above.
152+
**Maven Wrapper:** when `cache: 'maven'` is enabled, the action also caches and restores the Maven Wrapper distribution downloaded to `~/.m2/wrapper/dists` (in addition to the local repository), so wrapper-based (`./mvnw`) builds don't re-download the Maven distribution. The wrapper distribution is stored in a **separate** cache entry keyed only on `**/.mvn/wrapper/maven-wrapper.properties`, so it stays cached across the frequent `pom.xml` changes that rotate the main dependency cache key.
153153

154154
#### Caching gradle dependencies
155155
```yaml
@@ -167,6 +167,8 @@ steps:
167167
```
168168
Using the `cache: gradle` provides a simple and effective way to cache Gradle dependencies with minimal configuration.
169169

170+
**Gradle Wrapper:** when `cache: 'gradle'` is enabled, the action also caches and restores the Gradle Wrapper distribution downloaded to `~/.gradle/wrapper` (in addition to the Gradle caches), so wrapper-based (`./gradlew`) builds don't re-download the Gradle distribution. The wrapper distribution is stored in a **separate** cache entry keyed only on `**/gradle-wrapper.properties`, so it stays cached across the frequent `*.gradle*` changes that rotate the main dependency cache key.
171+
170172
For projects that require more advanced `Gradle` caching features, such as caching build outputs, support for Gradle configuration cache, encrypted cache storage, fine-grained cache control (including options to enable or disable the cache, set it to read-only or write-only, perform automated cleanup, and define custom cache rules), or optimized performance for complex CI workflows, consider using [`gradle/actions/setup-gradle`](https://github.com/gradle/actions/tree/main/setup-gradle).
171173

172174
For setup details and a comprehensive overview of all available features, visit the [setup-gradle documentation](https://github.com/gradle/actions/blob/main/docs/setup-gradle.md).

__tests__/cache.test.ts

Lines changed: 137 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,10 +113,7 @@ describe('dependency cache', () => {
113113

114114
await restore('maven', '');
115115
expect(spyCacheRestore).toHaveBeenCalledWith(
116-
[
117-
join(os.homedir(), '.m2', 'repository'),
118-
join(os.homedir(), '.m2', 'wrapper', 'dists')
119-
],
116+
[join(os.homedir(), '.m2', 'repository')],
120117
expect.any(String)
121118
);
122119
expect(spyGlobHashFiles).toHaveBeenCalledWith(
@@ -143,10 +140,7 @@ describe('dependency cache', () => {
143140

144141
await restore('maven', '');
145142
expect(spyCacheRestore).toHaveBeenCalledWith(
146-
[
147-
join(os.homedir(), '.m2', 'repository'),
148-
join(os.homedir(), '.m2', 'wrapper', 'dists')
149-
],
143+
[join(os.homedir(), '.m2', 'repository')],
150144
expect.any(String)
151145
);
152146
expect(spyGlobHashFiles).toHaveBeenCalledWith(
@@ -161,10 +155,7 @@ describe('dependency cache', () => {
161155

162156
await restore('maven', '');
163157
expect(spyCacheRestore).toHaveBeenCalledWith(
164-
[
165-
join(os.homedir(), '.m2', 'repository'),
166-
join(os.homedir(), '.m2', 'wrapper', 'dists')
167-
],
158+
[join(os.homedir(), '.m2', 'repository')],
168159
expect.any(String)
169160
);
170161
expect(spyGlobHashFiles).toHaveBeenCalledWith(
@@ -173,6 +164,39 @@ describe('dependency cache', () => {
173164
expect(spyWarning).not.toHaveBeenCalled();
174165
expect(spyInfo).toHaveBeenCalledWith('maven cache is not found');
175166
});
167+
it('restores the maven wrapper distribution cache independently of the main cache', async () => {
168+
createFile(join(workspace, 'pom.xml'));
169+
createDirectory(join(workspace, '.mvn'));
170+
createDirectory(join(workspace, '.mvn', 'wrapper'));
171+
createFile(
172+
join(workspace, '.mvn', 'wrapper', 'maven-wrapper.properties')
173+
);
174+
175+
await restore('maven', '');
176+
// Main cache no longer includes the wrapper distribution.
177+
expect(spyCacheRestore).toHaveBeenCalledWith(
178+
[join(os.homedir(), '.m2', 'repository')],
179+
expect.any(String)
180+
);
181+
// Wrapper distribution is restored on its own, keyed only on the
182+
// maven-wrapper.properties hash.
183+
expect(spyCacheRestore).toHaveBeenCalledWith(
184+
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
185+
expect.any(String)
186+
);
187+
expect(spyGlobHashFiles).toHaveBeenCalledWith(
188+
'**/.mvn/wrapper/maven-wrapper.properties'
189+
);
190+
});
191+
it('skips the maven wrapper cache when no maven-wrapper.properties exists', async () => {
192+
createFile(join(workspace, 'pom.xml'));
193+
194+
await restore('maven', '');
195+
expect(spyCacheRestore).not.toHaveBeenCalledWith(
196+
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
197+
expect.any(String)
198+
);
199+
});
176200
});
177201
describe('for gradle', () => {
178202
it('throws error if no build.gradle found', async () => {
@@ -228,6 +252,30 @@ describe('dependency cache', () => {
228252
expect(spyWarning).not.toHaveBeenCalled();
229253
expect(spyInfo).toHaveBeenCalledWith('gradle cache is not found');
230254
});
255+
it('restores the gradle wrapper distribution cache independently of the main cache', async () => {
256+
createFile(join(workspace, 'build.gradle'));
257+
createDirectory(join(workspace, 'gradle'));
258+
createDirectory(join(workspace, 'gradle', 'wrapper'));
259+
createFile(
260+
join(workspace, 'gradle', 'wrapper', 'gradle-wrapper.properties')
261+
);
262+
263+
await restore('gradle', '');
264+
// Main cache no longer includes the wrapper distribution.
265+
expect(spyCacheRestore).toHaveBeenCalledWith(
266+
[join(os.homedir(), '.gradle', 'caches')],
267+
expect.any(String)
268+
);
269+
// Wrapper distribution is restored on its own, keyed only on the
270+
// gradle-wrapper.properties hash.
271+
expect(spyCacheRestore).toHaveBeenCalledWith(
272+
[join(os.homedir(), '.gradle', 'wrapper')],
273+
expect.any(String)
274+
);
275+
expect(spyGlobHashFiles).toHaveBeenCalledWith(
276+
'**/gradle-wrapper.properties'
277+
);
278+
});
231279
});
232280
describe('for sbt', () => {
233281
it('throws error if no build.sbt found', async () => {
@@ -399,6 +447,40 @@ describe('dependency cache', () => {
399447
expect.stringMatching(/^Cache saved with the key:.*/)
400448
);
401449
});
450+
it('saves the maven wrapper distribution cache under its own key', async () => {
451+
createFile(join(workspace, 'pom.xml'));
452+
createStateForWrapperRestore('maven-wrapper', false);
453+
454+
await save('maven');
455+
expect(spyCacheSave).toHaveBeenCalledWith(
456+
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
457+
'setup-java-maven-wrapper-primary-key'
458+
);
459+
});
460+
it('does not save the maven wrapper cache on an exact wrapper hit', async () => {
461+
createFile(join(workspace, 'pom.xml'));
462+
createStateForWrapperRestore('maven-wrapper', true);
463+
464+
await save('maven');
465+
expect(spyCacheSave).not.toHaveBeenCalledWith(
466+
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
467+
expect.any(String)
468+
);
469+
});
470+
it('does not fail the post step when the wrapper distribution path is missing', async () => {
471+
createFile(join(workspace, 'pom.xml'));
472+
createStateForWrapperRestore('maven-wrapper', false);
473+
spyCacheSave.mockImplementation((paths: string[], key: string) => {
474+
if (paths.includes(join(os.homedir(), '.m2', 'wrapper', 'dists'))) {
475+
return Promise.reject(
476+
new cache.ValidationError('Path Validation Error')
477+
);
478+
}
479+
return Promise.resolve(0);
480+
});
481+
482+
await expect(save('maven')).resolves.not.toThrow();
483+
});
402484
});
403485
describe('for gradle', () => {
404486
it('uploads cache even if no build.gradle found', async () => {
@@ -451,6 +533,26 @@ describe('dependency cache', () => {
451533
expect.stringMatching(/^Cache saved with the key:.*/)
452534
);
453535
});
536+
it('saves the gradle wrapper distribution cache under its own key', async () => {
537+
createFile(join(workspace, 'build.gradle'));
538+
createStateForWrapperRestore('gradle-wrapper', false);
539+
540+
await save('gradle');
541+
expect(spyCacheSave).toHaveBeenCalledWith(
542+
[join(os.homedir(), '.gradle', 'wrapper')],
543+
'setup-java-gradle-wrapper-primary-key'
544+
);
545+
});
546+
it('does not save the gradle wrapper cache on an exact wrapper hit', async () => {
547+
createFile(join(workspace, 'build.gradle'));
548+
createStateForWrapperRestore('gradle-wrapper', true);
549+
550+
await save('gradle');
551+
expect(spyCacheSave).not.toHaveBeenCalledWith(
552+
[join(os.homedir(), '.gradle', 'wrapper')],
553+
expect.any(String)
554+
);
555+
});
454556
});
455557
describe('for sbt', () => {
456558
it('uploads cache even if no build.sbt found', async () => {
@@ -528,6 +630,29 @@ function createStateForSuccessfulRestore() {
528630
});
529631
}
530632

633+
/**
634+
* Create states to emulate a restore process where an additional (wrapper)
635+
* cache was restored. When `hit` is true the matched key equals the primary
636+
* key, emulating an exact wrapper cache hit.
637+
*/
638+
function createStateForWrapperRestore(wrapperName: string, hit: boolean) {
639+
const primaryKey = `setup-java-${wrapperName}-primary-key`;
640+
jest.spyOn(core, 'getState').mockImplementation(name => {
641+
switch (name) {
642+
case 'cache-primary-key':
643+
return 'setup-java-cache-primary-key';
644+
case 'cache-matched-key':
645+
return 'setup-java-cache-matched-key';
646+
case `cache-primary-key-${wrapperName}`:
647+
return primaryKey;
648+
case `cache-matched-key-${wrapperName}`:
649+
return hit ? primaryKey : '';
650+
default:
651+
return '';
652+
}
653+
});
654+
}
655+
531656
function createFile(path: string) {
532657
core.info(`created a file at ${path}`);
533658
fs.writeFileSync(path, '');

0 commit comments

Comments
 (0)