Skip to content

Commit 81c13a4

Browse files
brunoborgesCopilotCopilot
authored
Cache Maven and Gradle wrapper distributions separately from the dependency cache (#1097)
* Cache Maven wrapper distribution separately from the local repository The Maven wrapper distribution (~/.m2/wrapper/dists) was cached in the same entry as the local Maven repository (~/.m2/repository), keyed on a hash of **/pom.xml (plus wrapper properties and extensions). Because pom.xml changes frequently and no restoreKeys are used (by design, #269), almost every change produces a full cache miss and the wrapper distribution is re-downloaded via mvnw — which intermittently fails due to upstream rate limiting. The wrapper distribution only depends on maven-wrapper.properties, which changes very rarely. Give it its own cache entry keyed solely on **/.mvn/wrapper/maven-wrapper.properties so it survives the frequent pom.xml changes that rotate the main dependency cache key. - Add a generic additionalCaches concept to PackageManager, restored and saved independently with name-scoped state keys. - Move ~/.m2/wrapper/dists out of the main maven path into a maven-wrapper additional cache; skip silently when the project does not use mvnw. - Keep cache-hit / cache-primary-key outputs driven by the main cache. - Update tests, docs, and rebuild dist bundles. Fixes #1095 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Cache Gradle wrapper distribution separately from the dependency cache (#1098) The Gradle wrapper distribution (~/.gradle/wrapper) only depends on gradle-wrapper.properties, which changes rarely, but it was previously cached in the same entry as ~/.gradle/caches, keyed on volatile **/*.gradle* files with no restoreKeys (issue #269). Every dependency change therefore re-downloaded the wrapper. Move ~/.gradle/wrapper into a dedicated `gradle-wrapper` additional cache keyed only on **/gradle-wrapper.properties, reusing the additionalCaches infrastructure introduced for the Maven wrapper fix. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Fix broken try/catch in saveAdditionalCache and rebuild dist The autofix commits dropped the `} catch (error) {` line in saveAdditionalCache, leaving a `try` block without a catch and a dangling `error` reference, which broke compilation and Prettier. Restore the catch clause, reformat, and regenerate the dist bundles. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Address cache.ts review feedback - saveAdditionalCache: handle @actions/cache ValidationError (thrown when the cache paths do not resolve, e.g. the wrapper distribution was never downloaded) by skipping instead of failing the post step. Add a test. - Point the Gradle wrapper cache comment at issue #269 (Gradle wrapper cache churn) instead of the Maven-specific #1095. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * Document Gradle Wrapper distribution caching in README Add a parallel "Gradle Wrapper" note alongside the Maven Wrapper note, so the new behavior for cache: 'gradle' (caching ~/.gradle/wrapper in a separate entry keyed on **/gradle-wrapper.properties) is documented. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 3c85b14 commit 81c13a4

6 files changed

Lines changed: 633 additions & 42 deletions

File tree

README.md

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

164164
The cache input is optional, and caching is turned off by default.
165165

166-
**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.
166+
**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.
167167

168168
#### Caching gradle dependencies
169169
```yaml
@@ -181,6 +181,8 @@ steps:
181181
```
182182
Using the `cache: gradle` provides a simple and effective way to cache Gradle dependencies with minimal configuration.
183183

184+
**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.
185+
184186
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).
185187

186188
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: 196 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,10 +166,7 @@ describe('dependency cache', () => {
166166

167167
await restore('maven', '');
168168
expect(spyCacheRestore).toHaveBeenCalledWith(
169-
[
170-
join(os.homedir(), '.m2', 'repository'),
171-
join(os.homedir(), '.m2', 'wrapper', 'dists')
172-
],
169+
[join(os.homedir(), '.m2', 'repository')],
173170
expect.any(String)
174171
);
175172
expect(spyGlobHashFiles).toHaveBeenCalledWith(
@@ -196,10 +193,7 @@ describe('dependency cache', () => {
196193

197194
await restore('maven', '');
198195
expect(spyCacheRestore).toHaveBeenCalledWith(
199-
[
200-
join(os.homedir(), '.m2', 'repository'),
201-
join(os.homedir(), '.m2', 'wrapper', 'dists')
202-
],
196+
[join(os.homedir(), '.m2', 'repository')],
203197
expect.any(String)
204198
);
205199
expect(spyGlobHashFiles).toHaveBeenCalledWith(
@@ -214,10 +208,7 @@ describe('dependency cache', () => {
214208

215209
await restore('maven', '');
216210
expect(spyCacheRestore).toHaveBeenCalledWith(
217-
[
218-
join(os.homedir(), '.m2', 'repository'),
219-
join(os.homedir(), '.m2', 'wrapper', 'dists')
220-
],
211+
[join(os.homedir(), '.m2', 'repository')],
221212
expect.any(String)
222213
);
223214
expect(spyGlobHashFiles).toHaveBeenCalledWith(
@@ -226,6 +217,47 @@ describe('dependency cache', () => {
226217
expect(spyWarning).not.toHaveBeenCalled();
227218
expect(spyInfo).toHaveBeenCalledWith('maven cache is not found');
228219
});
220+
it('restores the maven wrapper distribution cache independently of the main cache', async () => {
221+
createDirectory(join(workspace, '.mvn'));
222+
createDirectory(join(workspace, '.mvn', 'wrapper'));
223+
createFile(
224+
join(workspace, '.mvn', 'wrapper', 'maven-wrapper.properties')
225+
);
226+
227+
await restore('maven', '');
228+
// Main dependency cache no longer carries the wrapper dists path.
229+
expect(spyCacheRestore).toHaveBeenCalledWith(
230+
[join(os.homedir(), '.m2', 'repository')],
231+
expect.any(String)
232+
);
233+
expect(spyCacheRestore).toHaveBeenCalledWith(
234+
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
235+
expect.stringContaining('maven-wrapper')
236+
);
237+
expect(spyGlobHashFiles).toHaveBeenCalledWith(
238+
'**/.mvn/wrapper/maven-wrapper.properties'
239+
);
240+
});
241+
it('skips the maven wrapper cache when no wrapper properties exist', async () => {
242+
createFile(join(workspace, 'pom.xml'));
243+
spyGlobHashFiles.mockImplementation((pattern: string) =>
244+
Promise.resolve(
245+
pattern === '**/.mvn/wrapper/maven-wrapper.properties'
246+
? ''
247+
: 'hash-stub'
248+
)
249+
);
250+
251+
await restore('maven', '');
252+
// Only the main dependency cache is restored; the wrapper cache path is
253+
// never touched because the project does not use mvnw.
254+
expect(spyCacheRestore).toHaveBeenCalledTimes(1);
255+
expect(spyCacheRestore).toHaveBeenCalledWith(
256+
[join(os.homedir(), '.m2', 'repository')],
257+
expect.any(String)
258+
);
259+
expect(spyWarning).not.toHaveBeenCalled();
260+
});
229261
});
230262
describe('for gradle', () => {
231263
it('throws error if no build.gradle found', async () => {
@@ -282,6 +314,43 @@ describe('dependency cache', () => {
282314
expect(spyWarning).not.toHaveBeenCalled();
283315
expect(spyInfo).toHaveBeenCalledWith('gradle cache is not found');
284316
});
317+
it('restores the gradle wrapper distribution cache independently of the main cache', async () => {
318+
createFile(join(workspace, 'build.gradle'));
319+
320+
await restore('gradle', '');
321+
// Main dependency cache no longer carries the wrapper path.
322+
expect(spyCacheRestore).toHaveBeenCalledWith(
323+
[join(os.homedir(), '.gradle', 'caches')],
324+
expect.any(String)
325+
);
326+
// Wrapper distribution is restored on its own, keyed only on the
327+
// wrapper properties file.
328+
expect(spyCacheRestore).toHaveBeenCalledWith(
329+
[join(os.homedir(), '.gradle', 'wrapper')],
330+
expect.stringContaining('setup-java-')
331+
);
332+
expect(spyGlobHashFiles).toHaveBeenCalledWith(
333+
'**/gradle-wrapper.properties'
334+
);
335+
});
336+
it('skips the gradle wrapper cache when no wrapper properties exist', async () => {
337+
createFile(join(workspace, 'build.gradle'));
338+
spyGlobHashFiles.mockImplementation((pattern: string) =>
339+
Promise.resolve(
340+
pattern === '**/gradle-wrapper.properties' ? '' : 'hash-stub'
341+
)
342+
);
343+
344+
await restore('gradle', '');
345+
// Only the main dependency cache is restored; the wrapper cache path is
346+
// never touched because the project does not use the gradle wrapper.
347+
expect(spyCacheRestore).toHaveBeenCalledTimes(1);
348+
expect(spyCacheRestore).toHaveBeenCalledWith(
349+
[join(os.homedir(), '.gradle', 'caches')],
350+
expect.any(String)
351+
);
352+
expect(spyWarning).not.toHaveBeenCalled();
353+
});
285354
});
286355
describe('for sbt', () => {
287356
it('throws error if no build.sbt found', async () => {
@@ -457,6 +526,77 @@ describe('dependency cache', () => {
457526
expect.stringMatching(/^Cache saved with the key:.*/)
458527
);
459528
});
529+
it('saves the maven wrapper distribution cache under its own key', async () => {
530+
createFile(join(workspace, 'pom.xml'));
531+
(core.getState as jest.Mock<any>).mockImplementation((name: any) => {
532+
switch (name) {
533+
case 'cache-primary-key':
534+
return 'setup-java-cache-primary-key';
535+
case 'cache-matched-key':
536+
return 'setup-java-cache-matched-key';
537+
case 'cache-primary-key-maven-wrapper':
538+
return 'setup-java-maven-wrapper-key';
539+
default:
540+
return '';
541+
}
542+
});
543+
544+
await save('maven');
545+
expect(spyCacheSave).toHaveBeenCalledWith(
546+
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
547+
'setup-java-maven-wrapper-key'
548+
);
549+
expect(spyWarning).not.toHaveBeenCalled();
550+
});
551+
it('does not save the maven wrapper cache on an exact wrapper hit', async () => {
552+
createFile(join(workspace, 'pom.xml'));
553+
(core.getState as jest.Mock<any>).mockImplementation((name: any) => {
554+
switch (name) {
555+
case 'cache-primary-key':
556+
return 'setup-java-cache-primary-key';
557+
case 'cache-matched-key':
558+
return 'setup-java-cache-matched-key';
559+
case 'cache-primary-key-maven-wrapper':
560+
case 'cache-matched-key-maven-wrapper':
561+
return 'setup-java-maven-wrapper-key';
562+
default:
563+
return '';
564+
}
565+
});
566+
567+
await save('maven');
568+
expect(spyCacheSave).not.toHaveBeenCalledWith(
569+
[join(os.homedir(), '.m2', 'wrapper', 'dists')],
570+
expect.any(String)
571+
);
572+
});
573+
it('does not fail the post step when the wrapper distribution path is missing', async () => {
574+
createFile(join(workspace, 'pom.xml'));
575+
(core.getState as jest.Mock<any>).mockImplementation((name: any) => {
576+
switch (name) {
577+
case 'cache-primary-key':
578+
return 'setup-java-cache-primary-key';
579+
case 'cache-matched-key':
580+
return 'setup-java-cache-matched-key';
581+
case 'cache-primary-key-maven-wrapper':
582+
return 'setup-java-maven-wrapper-key';
583+
default:
584+
return '';
585+
}
586+
});
587+
spyCacheSave.mockImplementation((paths: string[]) =>
588+
paths.includes(join(os.homedir(), '.m2', 'wrapper', 'dists'))
589+
? Promise.reject(
590+
new cache.ValidationError(
591+
'Path Validation Error: Path(s) specified in the action for caching do(es) not exist'
592+
)
593+
)
594+
: Promise.resolve(0)
595+
);
596+
597+
await expect(save('maven')).resolves.toBeUndefined();
598+
expect(spyWarning).not.toHaveBeenCalled();
599+
});
460600
});
461601
describe('for gradle', () => {
462602
it('uploads cache even if no build.gradle found', async () => {
@@ -509,6 +649,50 @@ describe('dependency cache', () => {
509649
expect.stringMatching(/^Cache saved with the key:.*/)
510650
);
511651
});
652+
it('saves the gradle wrapper distribution cache under its own key', async () => {
653+
createFile(join(workspace, 'build.gradle'));
654+
(core.getState as jest.Mock<any>).mockImplementation((name: any) => {
655+
switch (name) {
656+
case 'cache-primary-key':
657+
return 'setup-java-cache-primary-key';
658+
case 'cache-matched-key':
659+
return 'setup-java-cache-matched-key';
660+
case 'cache-primary-key-gradle-wrapper':
661+
return 'setup-java-gradle-wrapper-key';
662+
default:
663+
return '';
664+
}
665+
});
666+
667+
await save('gradle');
668+
expect(spyCacheSave).toHaveBeenCalledWith(
669+
[join(os.homedir(), '.gradle', 'wrapper')],
670+
'setup-java-gradle-wrapper-key'
671+
);
672+
expect(spyWarning).not.toHaveBeenCalled();
673+
});
674+
it('does not save the gradle wrapper cache on an exact wrapper hit', async () => {
675+
createFile(join(workspace, 'build.gradle'));
676+
(core.getState as jest.Mock<any>).mockImplementation((name: any) => {
677+
switch (name) {
678+
case 'cache-primary-key':
679+
return 'setup-java-cache-primary-key';
680+
case 'cache-matched-key':
681+
return 'setup-java-cache-matched-key';
682+
case 'cache-primary-key-gradle-wrapper':
683+
case 'cache-matched-key-gradle-wrapper':
684+
return 'setup-java-gradle-wrapper-key';
685+
default:
686+
return '';
687+
}
688+
});
689+
690+
await save('gradle');
691+
expect(spyCacheSave).not.toHaveBeenCalledWith(
692+
[join(os.homedir(), '.gradle', 'wrapper')],
693+
expect.any(String)
694+
);
695+
});
512696
});
513697
describe('for sbt', () => {
514698
it('uploads cache even if no build.sbt found', async () => {

0 commit comments

Comments
 (0)