Restore dependency and wrapper caches concurrently (#1174)

* Restore dependency and wrapper caches concurrently

Run primary dependency and wrapper cache restores in parallel while preserving existing outputs and save semantics.

Add unit and E2E coverage for concurrent restore behavior, wrapper cache validation, and additional-cache error handling.

Include a manual benchmark workflow for baseline-vs-candidate restore timing comparisons across OSes.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d43ec3dd-96c7-4eb3-909e-b8123cd12d3c

* Address PR review comments

Use env variables for cache-hit values in benchmark record steps to avoid expression expansion in run commands, and rename E2E restore step labels for clarity.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: d43ec3dd-96c7-4eb3-909e-b8123cd12d3c

* Stabilize wrapper cache restore checks

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: d43ec3dd-96c7-4eb3-909e-b8123cd12d3c
This commit is contained in:
Bruno Borges
2026-07-29 13:41:24 -04:00
committed by GitHub
parent ec4dbbe20d
commit 9f43141311
9 changed files with 683 additions and 59 deletions
@@ -0,0 +1,209 @@
name: Benchmark cache restore
on:
workflow_dispatch:
inputs:
baseline-ref:
description: Git ref containing the sequential restore implementation
required: true
default: main
type: string
candidate-ref:
description: Git ref containing the concurrent restore implementation (defaults to the dispatched ref)
required: false
type: string
permissions:
contents: read
defaults:
run:
shell: bash
jobs:
warm-caches:
name: Warm ${{ matrix.tool }} ${{ matrix.profile }} caches (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-15-intel, windows-latest, ubuntu-latest]
tool: [maven, gradle]
profile: [small, large]
steps:
- name: Checkout benchmark workflow
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Checkout baseline
uses: actions/checkout@v7
with:
path: baseline
persist-credentials: false
ref: ${{ inputs.baseline-ref }}
- name: Checkout candidate
uses: actions/checkout@v7
with:
path: candidate
persist-credentials: false
ref: ${{ inputs.candidate-ref || github.ref }}
- name: Prepare benchmark inputs
run: bash __tests__/benchmark-cache-restore.sh prepare "${{ matrix.tool }}" "${{ matrix.profile }}"
- name: Prepare cache save
uses: ./candidate
with:
distribution: temurin
java-version: '17'
cache: ${{ matrix.tool }}
cache-dependency-path: benchmark/${{ matrix.tool == 'maven' && 'pom.xml' || 'build.gradle' }}
- name: Populate benchmark caches
run: |
bash __tests__/benchmark-cache-restore.sh reset "${{ matrix.tool }}"
bash __tests__/benchmark-cache-restore.sh populate "${{ matrix.tool }}" "${{ matrix.profile }}"
benchmark:
name: Benchmark ${{ matrix.tool }} ${{ matrix.profile }} (${{ matrix.os }})
needs: warm-caches
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [macos-15-intel, windows-latest, ubuntu-latest]
tool: [maven, gradle]
profile: [small, large]
steps:
- name: Checkout benchmark workflow
uses: actions/checkout@v7
with:
persist-credentials: false
- name: Checkout baseline
uses: actions/checkout@v7
with:
path: baseline
persist-credentials: false
ref: ${{ inputs.baseline-ref }}
- name: Checkout candidate
uses: actions/checkout@v7
with:
path: candidate
persist-credentials: false
ref: ${{ inputs.candidate-ref || github.ref }}
- name: Prepare benchmark inputs
run: bash __tests__/benchmark-cache-restore.sh prepare "${{ matrix.tool }}" "${{ matrix.profile }}"
- name: Reset caches for baseline iteration 1
run: bash __tests__/benchmark-cache-restore.sh reset "${{ matrix.tool }}"
- name: Start baseline iteration 1 timer
run: bash __tests__/benchmark-cache-restore.sh start "${{ matrix.tool }}"
- name: Restore with baseline iteration 1
id: baseline-1
uses: ./baseline
with:
distribution: temurin
java-version: '17'
cache: ${{ matrix.tool }}
cache-dependency-path: benchmark/${{ matrix.tool == 'maven' && 'pom.xml' || 'build.gradle' }}
cache-read-only: true
- name: Record baseline iteration 1
env:
CACHE_HIT: ${{ steps.baseline-1.outputs.cache-hit }}
run: bash __tests__/benchmark-cache-restore.sh record "${{ matrix.tool }}" "${{ matrix.os }}" "${{ matrix.profile }}" baseline 1 "$CACHE_HIT"
- name: Reset caches for candidate iteration 1
run: bash __tests__/benchmark-cache-restore.sh reset "${{ matrix.tool }}"
- name: Start candidate iteration 1 timer
run: bash __tests__/benchmark-cache-restore.sh start "${{ matrix.tool }}"
- name: Restore with candidate iteration 1
id: candidate-1
uses: ./candidate
with:
distribution: temurin
java-version: '17'
cache: ${{ matrix.tool }}
cache-dependency-path: benchmark/${{ matrix.tool == 'maven' && 'pom.xml' || 'build.gradle' }}
cache-read-only: true
- name: Record candidate iteration 1
env:
CACHE_HIT: ${{ steps.candidate-1.outputs.cache-hit }}
run: bash __tests__/benchmark-cache-restore.sh record "${{ matrix.tool }}" "${{ matrix.os }}" "${{ matrix.profile }}" candidate 1 "$CACHE_HIT"
- name: Reset caches for candidate iteration 2
run: bash __tests__/benchmark-cache-restore.sh reset "${{ matrix.tool }}"
- name: Start candidate iteration 2 timer
run: bash __tests__/benchmark-cache-restore.sh start "${{ matrix.tool }}"
- name: Restore with candidate iteration 2
id: candidate-2
uses: ./candidate
with:
distribution: temurin
java-version: '17'
cache: ${{ matrix.tool }}
cache-dependency-path: benchmark/${{ matrix.tool == 'maven' && 'pom.xml' || 'build.gradle' }}
cache-read-only: true
- name: Record candidate iteration 2
env:
CACHE_HIT: ${{ steps.candidate-2.outputs.cache-hit }}
run: bash __tests__/benchmark-cache-restore.sh record "${{ matrix.tool }}" "${{ matrix.os }}" "${{ matrix.profile }}" candidate 2 "$CACHE_HIT"
- name: Reset caches for baseline iteration 2
run: bash __tests__/benchmark-cache-restore.sh reset "${{ matrix.tool }}"
- name: Start baseline iteration 2 timer
run: bash __tests__/benchmark-cache-restore.sh start "${{ matrix.tool }}"
- name: Restore with baseline iteration 2
id: baseline-2
uses: ./baseline
with:
distribution: temurin
java-version: '17'
cache: ${{ matrix.tool }}
cache-dependency-path: benchmark/${{ matrix.tool == 'maven' && 'pom.xml' || 'build.gradle' }}
cache-read-only: true
- name: Record baseline iteration 2
env:
CACHE_HIT: ${{ steps.baseline-2.outputs.cache-hit }}
run: bash __tests__/benchmark-cache-restore.sh record "${{ matrix.tool }}" "${{ matrix.os }}" "${{ matrix.profile }}" baseline 2 "$CACHE_HIT"
- name: Reset caches for baseline iteration 3
run: bash __tests__/benchmark-cache-restore.sh reset "${{ matrix.tool }}"
- name: Start baseline iteration 3 timer
run: bash __tests__/benchmark-cache-restore.sh start "${{ matrix.tool }}"
- name: Restore with baseline iteration 3
id: baseline-3
uses: ./baseline
with:
distribution: temurin
java-version: '17'
cache: ${{ matrix.tool }}
cache-dependency-path: benchmark/${{ matrix.tool == 'maven' && 'pom.xml' || 'build.gradle' }}
cache-read-only: true
- name: Record baseline iteration 3
env:
CACHE_HIT: ${{ steps.baseline-3.outputs.cache-hit }}
run: bash __tests__/benchmark-cache-restore.sh record "${{ matrix.tool }}" "${{ matrix.os }}" "${{ matrix.profile }}" baseline 3 "$CACHE_HIT"
- name: Reset caches for candidate iteration 3
run: bash __tests__/benchmark-cache-restore.sh reset "${{ matrix.tool }}"
- name: Start candidate iteration 3 timer
run: bash __tests__/benchmark-cache-restore.sh start "${{ matrix.tool }}"
- name: Restore with candidate iteration 3
id: candidate-3
uses: ./candidate
with:
distribution: temurin
java-version: '17'
cache: ${{ matrix.tool }}
cache-dependency-path: benchmark/${{ matrix.tool == 'maven' && 'pom.xml' || 'build.gradle' }}
cache-read-only: true
- name: Record candidate iteration 3
env:
CACHE_HIT: ${{ steps.candidate-3.outputs.cache-hit }}
run: bash __tests__/benchmark-cache-restore.sh record "${{ matrix.tool }}" "${{ matrix.os }}" "${{ matrix.profile }}" candidate 3 "$CACHE_HIT"
- name: Summarize benchmark
run: bash __tests__/benchmark-cache-restore.sh summarize "${{ matrix.tool }}" "$GITHUB_STEP_SUMMARY"
- name: Upload raw timings
uses: actions/upload-artifact@v6
with:
name: cache-restore-${{ matrix.os }}-${{ matrix.tool }}-${{ matrix.profile }}
path: .benchmark-results/timings.csv
if-no-files-found: error
+20
View File
@@ -42,7 +42,10 @@ jobs:
# https://github.com/actions/cache/issues/454#issuecomment-840493935
run: |
gradle downloadDependencies --no-daemon -p __tests__/cache/gradle1
mkdir -p "$HOME/.gradle/wrapper/dists/setup-java-e2e"
echo "gradle wrapper cache" > "$HOME/.gradle/wrapper/dists/setup-java-e2e/payload"
bash __tests__/check-dir.sh "$HOME/.gradle/caches"
bash __tests__/check-dir.sh "$HOME/.gradle/wrapper/dists"
gradle-restore:
runs-on: ${{ matrix.os }}
strategy:
@@ -65,6 +68,8 @@ jobs:
cache-read-only: true
- name: Confirm that ~/.gradle/caches directory has been made
run: bash __tests__/check-dir.sh "$HOME/.gradle/caches"
- name: Confirm that the Gradle Wrapper cache has been restored
run: bash __tests__/check-dir.sh "$HOME/.gradle/wrapper/dists"
maven-save:
runs-on: ${{ matrix.os }}
strategy:
@@ -86,7 +91,10 @@ jobs:
- name: Create files to cache
run: |
mvn verify -f __tests__/cache/maven/pom.xml
mkdir -p "$HOME/.m2/wrapper/dists/setup-java-e2e"
echo "maven wrapper cache" > "$HOME/.m2/wrapper/dists/setup-java-e2e/payload"
bash __tests__/check-dir.sh "$HOME/.m2/repository"
bash __tests__/check-dir.sh "$HOME/.m2/wrapper/dists"
maven-restore:
runs-on: ${{ matrix.os }}
strategy:
@@ -109,6 +117,8 @@ jobs:
cache-read-only: true
- name: Confirm that ~/.m2/repository directory has been made
run: bash __tests__/check-dir.sh "$HOME/.m2/repository"
- name: Confirm that the Maven Wrapper cache has been restored
run: bash __tests__/check-dir.sh "$HOME/.m2/wrapper/dists"
sbt-save:
runs-on: ${{ matrix.os }}
defaults:
@@ -206,7 +216,10 @@ jobs:
# https://github.com/actions/cache/issues/454#issuecomment-840493935
run: |
gradle downloadDependencies --no-daemon -p __tests__/cache/gradle1
mkdir -p "$HOME/.gradle/wrapper/dists/setup-java-e2e-gradle1"
echo "gradle wrapper cache gradle1" > "$HOME/.gradle/wrapper/dists/setup-java-e2e-gradle1/payload"
bash __tests__/check-dir.sh "$HOME/.gradle/caches"
bash __tests__/check-dir.sh "$HOME/.gradle/wrapper/dists"
gradle1-restore:
runs-on: ${{ matrix.os }}
strategy:
@@ -229,6 +242,8 @@ jobs:
cache-dependency-path: __tests__/cache/gradle1/*.gradle*
- name: Confirm that ~/.gradle/caches directory has been made
run: bash __tests__/check-dir.sh "$HOME/.gradle/caches"
- name: Confirm that the Gradle Wrapper cache has been restored
run: bash __tests__/check-dir.sh "$HOME/.gradle/wrapper/dists"
gradle2-restore:
runs-on: ${{ matrix.os }}
strategy:
@@ -273,7 +288,10 @@ jobs:
- name: Create files to cache
run: |
mvn verify -f __tests__/cache/maven/pom.xml
mkdir -p "$HOME/.m2/wrapper/dists/setup-java-e2e-maven1"
echo "maven wrapper cache maven1" > "$HOME/.m2/wrapper/dists/setup-java-e2e-maven1/payload"
bash __tests__/check-dir.sh "$HOME/.m2/repository"
bash __tests__/check-dir.sh "$HOME/.m2/wrapper/dists"
maven1-restore:
runs-on: ${{ matrix.os }}
strategy:
@@ -296,6 +314,8 @@ jobs:
cache-dependency-path: __tests__/cache/maven/pom.xml
- name: Confirm that ~/.m2/repository directory has been made
run: bash __tests__/check-dir.sh "$HOME/.m2/repository"
- name: Confirm that the Maven Wrapper cache has been restored
run: bash __tests__/check-dir.sh "$HOME/.m2/wrapper/dists"
maven2-restore:
runs-on: ${{ matrix.os }}
strategy:
+135
View File
@@ -0,0 +1,135 @@
#!/usr/bin/env bash
set -euo pipefail
command=${1:?command is required}
tool=${2:?tool is required}
case "$tool" in
maven)
dependency_cache="$HOME/.m2/repository"
wrapper_cache="$HOME/.m2/wrapper/dists"
dependency_file="benchmark/pom.xml"
wrapper_file="benchmark/.mvn/wrapper/maven-wrapper.properties"
;;
gradle)
dependency_cache="$HOME/.gradle/caches"
wrapper_cache="$HOME/.gradle/wrapper"
dependency_file="benchmark/build.gradle"
wrapper_file="benchmark/gradle/wrapper/gradle-wrapper.properties"
;;
*)
echo "Unsupported tool: $tool" >&2
exit 1
;;
esac
case "$command" in
prepare)
profile=${3:?profile is required}
mkdir -p "$(dirname "$dependency_file")" "$(dirname "$wrapper_file")"
printf '// setup-java cache benchmark v1: %s\n' "$profile" > "$dependency_file"
printf '# setup-java cache benchmark v1: %s\n' "$profile" > "$wrapper_file"
;;
reset)
rm -rf "$dependency_cache" "$wrapper_cache"
;;
populate)
profile=${3:?profile is required}
case "$profile" in
small)
dependency_megabytes=8
wrapper_megabytes=2
;;
large)
dependency_megabytes=128
wrapper_megabytes=32
;;
*)
echo "Unsupported profile: $profile" >&2
exit 1
;;
esac
mkdir -p "$dependency_cache/setup-java-benchmark"
mkdir -p "$wrapper_cache/setup-java-benchmark"
dd if=/dev/urandom \
of="$dependency_cache/setup-java-benchmark/payload" \
bs=1048576 count="$dependency_megabytes" 2>/dev/null
dd if=/dev/urandom \
of="$wrapper_cache/setup-java-benchmark/payload" \
bs=1048576 count="$wrapper_megabytes" 2>/dev/null
;;
start)
node -e "require('fs').writeFileSync('.benchmark-start', String(Date.now()))"
;;
record)
os=${3:?os is required}
profile=${4:?profile is required}
implementation=${5:?implementation is required}
iteration=${6:?iteration is required}
cache_hit=${7:?cache-hit output is required}
if [ "$cache_hit" != "true" ]; then
echo "Expected an exact dependency-cache hit for $implementation" >&2
exit 1
fi
test -f "$dependency_cache/setup-java-benchmark/payload"
started=$(cat .benchmark-start)
finished=$(node -e "process.stdout.write(String(Date.now()))")
elapsed=$((finished - started))
mkdir -p .benchmark-results
printf '%s,%s,%s,%s,%s,%s\n' \
"$os" "$tool" "$profile" "$implementation" "$iteration" "$elapsed" \
>> .benchmark-results/timings.csv
;;
summarize)
summary_file=${3:?summary file is required}
results_file=".benchmark-results/timings.csv"
node --input-type=module - "$results_file" "$summary_file" <<'NODE'
import fs from 'node:fs';
const [, , resultsFile, summaryFile] = process.argv;
const rows = fs
.readFileSync(resultsFile, 'utf8')
.trim()
.split('\n')
.map(line => {
const [os, tool, profile, implementation, iteration, elapsed] =
line.split(',');
return {os, tool, profile, implementation, iteration, elapsed: +elapsed};
});
const average = implementation => {
const values = rows
.filter(row => row.implementation === implementation)
.map(row => row.elapsed);
if (values.length === 0) {
throw new Error(`No ${implementation} benchmark results were recorded`);
}
return Math.round(values.reduce((sum, value) => sum + value, 0) / values.length);
};
const baseline = average('baseline');
const candidate = average('candidate');
const change = (((candidate - baseline) / baseline) * 100).toFixed(1);
const {os, tool, profile} = rows[0];
const lines = [
`### ${tool} ${profile} cache restore on ${os}`,
'',
'| Implementation | Iteration | Wall time (ms) |',
'| --- | ---: | ---: |',
...rows.map(
row =>
`| ${row.implementation} | ${row.iteration} | ${row.elapsed} |`
),
`| **baseline average** | | **${baseline}** |`,
`| **candidate average** | | **${candidate}** |`,
'',
`Candidate change from baseline: **${change}%**`,
''
];
fs.appendFileSync(summaryFile, `${lines.join('\n')}\n`);
NODE
;;
*)
echo "Unsupported command: $command" >&2
exit 1
;;
esac
+161
View File
@@ -237,6 +237,83 @@ describe('dependency cache', () => {
expect(spyGlobHashFiles).toHaveBeenCalledWith(
'**/.mvn/wrapper/maven-wrapper.properties'
);
expect(spyInfo).toHaveBeenCalledWith(
'maven-wrapper cache is not found'
);
});
it('starts maven dependency and wrapper restores before either completes', async () => {
createDirectory(join(workspace, '.mvn'));
createDirectory(join(workspace, '.mvn', 'wrapper'));
createFile(
join(workspace, '.mvn', 'wrapper', 'maven-wrapper.properties')
);
const dependencyRestore = deferred<string | undefined>();
const wrapperRestore = deferred<string | undefined>();
const bothRestoresStarted = deferred<void>();
let restoreCount = 0;
spyCacheRestore.mockImplementation((paths: string[]) => {
restoreCount++;
if (restoreCount === 2) {
bothRestoresStarted.resolve();
}
return paths.includes(join(os.homedir(), '.m2', 'repository'))
? dependencyRestore.promise
: wrapperRestore.promise;
});
const restorePromise = restore('maven', '');
await bothRestoresStarted.promise;
expect(spyCacheRestore).toHaveBeenCalledTimes(2);
expect(spySaveState).toHaveBeenCalledWith(
'cache-primary-key',
expect.any(String)
);
expect(spySaveState).toHaveBeenCalledWith(
'cache-primary-key-maven-wrapper',
expect.any(String)
);
wrapperRestore.resolve('maven-wrapper-hit');
dependencyRestore.resolve('maven-dependency-hit');
await restorePromise;
expect(spySaveState).toHaveBeenCalledWith(
'cache-matched-key-maven-wrapper',
'maven-wrapper-hit'
);
expect(spySaveState).toHaveBeenCalledWith(
'cache-matched-key',
'maven-dependency-hit'
);
expect(spySetOutput).toHaveBeenCalledWith('cache-hit', false);
});
it('propagates a wrapper restore failure after starting both restores', async () => {
createDirectory(join(workspace, '.mvn'));
createDirectory(join(workspace, '.mvn', 'wrapper'));
createFile(
join(workspace, '.mvn', 'wrapper', 'maven-wrapper.properties')
);
const dependencyRestore = deferred<string | undefined>();
const wrapperRestore = deferred<string | undefined>();
const bothRestoresStarted = deferred<void>();
let restoreCount = 0;
spyCacheRestore.mockImplementation((paths: string[]) => {
restoreCount++;
if (restoreCount === 2) {
bothRestoresStarted.resolve();
}
return paths.includes(join(os.homedir(), '.m2', 'repository'))
? dependencyRestore.promise
: wrapperRestore.promise;
});
const restorePromise = restore('maven', '');
await bothRestoresStarted.promise;
wrapperRestore.reject(new Error('wrapper restore failed'));
dependencyRestore.resolve(undefined);
await expect(restorePromise).rejects.toThrow('wrapper restore failed');
});
it('skips the maven wrapper cache when no wrapper properties exist', async () => {
createFile(join(workspace, 'pom.xml'));
@@ -333,6 +410,50 @@ describe('dependency cache', () => {
'**/gradle-wrapper.properties'
);
});
it('starts gradle dependency and wrapper restores before either completes', async () => {
createFile(join(workspace, 'build.gradle'));
createFile(join(workspace, 'gradle-wrapper.properties'));
const dependencyRestore = deferred<string | undefined>();
const wrapperRestore = deferred<string | undefined>();
const bothRestoresStarted = deferred<void>();
let restoreCount = 0;
spyCacheRestore.mockImplementation((paths: string[]) => {
restoreCount++;
if (restoreCount === 2) {
bothRestoresStarted.resolve();
}
return paths.includes(join(os.homedir(), '.gradle', 'caches'))
? dependencyRestore.promise
: wrapperRestore.promise;
});
const restorePromise = restore('gradle', '');
await bothRestoresStarted.promise;
expect(spyCacheRestore).toHaveBeenCalledTimes(2);
expect(spySaveState).toHaveBeenCalledWith(
'cache-primary-key',
expect.any(String)
);
expect(spySaveState).toHaveBeenCalledWith(
'cache-primary-key-gradle-wrapper',
expect.any(String)
);
dependencyRestore.resolve('gradle-dependency-hit');
wrapperRestore.resolve('gradle-wrapper-hit');
await restorePromise;
expect(spySaveState).toHaveBeenCalledWith(
'cache-matched-key',
'gradle-dependency-hit'
);
expect(spySaveState).toHaveBeenCalledWith(
'cache-matched-key-gradle-wrapper',
'gradle-wrapper-hit'
);
expect(spySetOutput).toHaveBeenCalledWith('cache-hit', false);
});
it('skips the gradle wrapper cache when no wrapper properties exist', async () => {
createFile(join(workspace, 'build.gradle'));
spyGlobHashFiles.mockImplementation((pattern: string) =>
@@ -609,6 +730,36 @@ describe('dependency cache', () => {
);
expect(spyWarning).not.toHaveBeenCalled();
});
it('continues with primary cache save when additional cache save fails unexpectedly', async () => {
createFile(join(workspace, 'pom.xml'));
(core.getState as jest.Mock<any>).mockImplementation((name: any) => {
switch (name) {
case 'cache-primary-key':
return 'setup-java-cache-primary-key';
case 'cache-matched-key':
return 'setup-java-cache-matched-key';
case 'cache-primary-key-maven-wrapper':
return 'setup-java-maven-wrapper-key';
default:
return '';
}
});
spyCacheSave.mockImplementation((paths: string[], key: string) => {
if (paths[0] === 'wrapper-path') {
return Promise.reject(new Error('wrapper save exploded'));
}
return Promise.resolve(0);
});
await expect(save('maven')).resolves.toBeUndefined();
expect(spyWarning).toHaveBeenCalledWith(
'Failed to save maven-wrapper cache: wrapper save exploded. Continuing with primary cache save.'
);
expect(spyCacheSave).toHaveBeenCalledWith(
[join(os.homedir(), '.m2', 'repository')],
'setup-java-cache-primary-key'
);
});
});
describe('for gradle', () => {
it('uploads cache even if no build.gradle found', async () => {
@@ -817,6 +968,16 @@ function createFile(path: string) {
fs.writeFileSync(path, '');
}
function deferred<T>() {
let resolve!: (value: T | PromiseLike<T>) => void;
let reject!: (reason?: unknown) => void;
const promise = new Promise<T>((promiseResolve, promiseReject) => {
resolve = promiseResolve;
reject = promiseReject;
});
return {promise, resolve, reject};
}
function createDirectory(path: string) {
core.info(`created a directory at ${path}`);
fs.mkdirSync(path);
@@ -0,0 +1 @@
distributionUrl=https\://services.gradle.org/distributions/gradle-9.4.1-bin.zip
@@ -0,0 +1 @@
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
+43 -18
View File
@@ -97899,10 +97899,23 @@ async function computeAdditionalCacheKey(additionalCache) {
*/
async function restore(id, cacheDependencyPath) {
const packageManager = findPackageManager(id);
const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath);
const [primaryKey, preparedAdditionalCaches] = await Promise.all([
computeCacheKey(packageManager, cacheDependencyPath),
prepareAdditionalCaches(packageManager.additionalCaches ?? [])
]);
core.debug(`primary key is ${primaryKey}`);
core.saveState(STATE_CACHE_PRIMARY_KEY, primaryKey);
core.setOutput(STATE_CACHE_PRIMARY_KEY, primaryKey);
for (const preparedCache of preparedAdditionalCaches) {
core.debug(`${preparedCache.cache.name} primary key is ${preparedCache.primaryKey}`);
core.saveState(additionalCachePrimaryKeyState(preparedCache.cache.name), preparedCache.primaryKey);
}
await Promise.all([
restorePrimaryCache(packageManager, primaryKey),
...preparedAdditionalCaches.map(preparedCache => restoreAdditionalCache(preparedCache))
]);
}
async function restorePrimaryCache(packageManager, primaryKey) {
// No "restoreKeys" is set, to start with a clear cache after dependency update (see https://github.com/actions/setup-java/issues/269)
const matchedKey = await cache.restoreCache(packageManager.path, primaryKey);
if (matchedKey) {
@@ -97914,29 +97927,35 @@ async function restore(id, cacheDependencyPath) {
core.setOutput('cache-hit', false);
core.info(`${packageManager.id} cache is not found`);
}
for (const additionalCache of packageManager.additionalCaches ?? []) {
await restoreAdditionalCache(additionalCache);
}
}
/**
* Restore an additional cache (e.g. a build-tool wrapper distribution) that is
* keyed independently of the main dependency cache so that it survives changes
* to volatile dependency files. Skips silently when the project does not use
* the corresponding feature.
* Compute keys for additional caches (e.g. build-tool wrapper distributions).
* Additional caches without a matching configuration file are omitted.
*/
async function restoreAdditionalCache(additionalCache) {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core.debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return;
}
core.debug(`${additionalCache.name} primary key is ${primaryKey}`);
core.saveState(additionalCachePrimaryKeyState(additionalCache.name), primaryKey);
async function prepareAdditionalCaches(additionalCaches) {
const preparedCaches = await Promise.all(additionalCaches.map(async (additionalCache) => {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core.debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return undefined;
}
return { cache: additionalCache, primaryKey };
}));
return preparedCaches.filter((preparedCache) => preparedCache !== undefined);
}
/**
* Restore an additional cache keyed independently of the main dependency cache.
*/
async function restoreAdditionalCache(preparedCache) {
const { cache: additionalCache, primaryKey } = preparedCache;
const matchedKey = await cache.restoreCache(additionalCache.path, primaryKey);
if (matchedKey) {
core.saveState(additionalCacheMatchedKeyState(additionalCache.name), matchedKey);
core.info(`${additionalCache.name} cache restored from key: ${matchedKey}`);
}
else {
core.info(`${additionalCache.name} cache is not found`);
}
}
/**
* Save the dependency cache
@@ -97948,7 +97967,13 @@ async function save(id) {
// Inputs are re-evaluated before the post action, so we want the original key used for restore
const primaryKey = getState(STATE_CACHE_PRIMARY_KEY);
for (const additionalCache of packageManager.additionalCaches ?? []) {
await saveAdditionalCache(packageManager, additionalCache);
try {
await saveAdditionalCache(packageManager, additionalCache);
}
catch (error) {
const err = error;
warning(`Failed to save ${additionalCache.name} cache: ${err.message}. Continuing with primary cache save.`);
}
}
if (!primaryKey) {
warning('Error retrieving key from state.');
@@ -98030,7 +98055,7 @@ async function saveAdditionalCache(packageManager, additionalCache) {
}
else {
if (isProbablyGradleDaemonProblem(packageManager, err)) {
warning('Failed to save Gradle cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with `--no-daemon` option. Refer to https://github.com/actions/cache/issues/454 for details.');
warning(`Failed to save ${additionalCache.name} cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with \`--no-daemon\` option. Refer to https://github.com/actions/cache/issues/454 for details.`);
}
throw error;
}
+43 -18
View File
@@ -129154,10 +129154,23 @@ async function computeAdditionalCacheKey(additionalCache) {
*/
async function restore(id, cacheDependencyPath) {
const packageManager = findPackageManager(id);
const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath);
const [primaryKey, preparedAdditionalCaches] = await Promise.all([
computeCacheKey(packageManager, cacheDependencyPath),
prepareAdditionalCaches(packageManager.additionalCaches ?? [])
]);
core_debug(`primary key is ${primaryKey}`);
saveState(STATE_CACHE_PRIMARY_KEY, primaryKey);
setOutput(STATE_CACHE_PRIMARY_KEY, primaryKey);
for (const preparedCache of preparedAdditionalCaches) {
core_debug(`${preparedCache.cache.name} primary key is ${preparedCache.primaryKey}`);
saveState(additionalCachePrimaryKeyState(preparedCache.cache.name), preparedCache.primaryKey);
}
await Promise.all([
restorePrimaryCache(packageManager, primaryKey),
...preparedAdditionalCaches.map(preparedCache => restoreAdditionalCache(preparedCache))
]);
}
async function restorePrimaryCache(packageManager, primaryKey) {
// No "restoreKeys" is set, to start with a clear cache after dependency update (see https://github.com/actions/setup-java/issues/269)
const matchedKey = await restoreCache(packageManager.path, primaryKey);
if (matchedKey) {
@@ -129169,29 +129182,35 @@ async function restore(id, cacheDependencyPath) {
setOutput('cache-hit', false);
info(`${packageManager.id} cache is not found`);
}
for (const additionalCache of packageManager.additionalCaches ?? []) {
await restoreAdditionalCache(additionalCache);
}
}
/**
* Restore an additional cache (e.g. a build-tool wrapper distribution) that is
* keyed independently of the main dependency cache so that it survives changes
* to volatile dependency files. Skips silently when the project does not use
* the corresponding feature.
* Compute keys for additional caches (e.g. build-tool wrapper distributions).
* Additional caches without a matching configuration file are omitted.
*/
async function restoreAdditionalCache(additionalCache) {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core_debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return;
}
core_debug(`${additionalCache.name} primary key is ${primaryKey}`);
saveState(additionalCachePrimaryKeyState(additionalCache.name), primaryKey);
async function prepareAdditionalCaches(additionalCaches) {
const preparedCaches = await Promise.all(additionalCaches.map(async (additionalCache) => {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core_debug(`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`);
return undefined;
}
return { cache: additionalCache, primaryKey };
}));
return preparedCaches.filter((preparedCache) => preparedCache !== undefined);
}
/**
* Restore an additional cache keyed independently of the main dependency cache.
*/
async function restoreAdditionalCache(preparedCache) {
const { cache: additionalCache, primaryKey } = preparedCache;
const matchedKey = await restoreCache(additionalCache.path, primaryKey);
if (matchedKey) {
saveState(additionalCacheMatchedKeyState(additionalCache.name), matchedKey);
info(`${additionalCache.name} cache restored from key: ${matchedKey}`);
}
else {
info(`${additionalCache.name} cache is not found`);
}
}
/**
* Save the dependency cache
@@ -129203,7 +129222,13 @@ async function save(id) {
// Inputs are re-evaluated before the post action, so we want the original key used for restore
const primaryKey = core.getState(STATE_CACHE_PRIMARY_KEY);
for (const additionalCache of packageManager.additionalCaches ?? []) {
await saveAdditionalCache(packageManager, additionalCache);
try {
await saveAdditionalCache(packageManager, additionalCache);
}
catch (error) {
const err = error;
core.warning(`Failed to save ${additionalCache.name} cache: ${err.message}. Continuing with primary cache save.`);
}
}
if (!primaryKey) {
core.warning('Error retrieving key from state.');
@@ -129285,7 +129310,7 @@ async function saveAdditionalCache(packageManager, additionalCache) {
}
else {
if (isProbablyGradleDaemonProblem(packageManager, err)) {
core.warning('Failed to save Gradle cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with `--no-daemon` option. Refer to https://github.com/actions/cache/issues/454 for details.');
core.warning(`Failed to save ${additionalCache.name} cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with \`--no-daemon\` option. Refer to https://github.com/actions/cache/issues/454 for details.`);
}
throw error;
}
+70 -23
View File
@@ -36,6 +36,11 @@ interface AdditionalCache {
pattern: string[];
}
interface PreparedAdditionalCache {
cache: AdditionalCache;
primaryKey: string;
}
interface PackageManager {
id: 'maven' | 'gradle' | 'sbt';
/**
@@ -189,11 +194,37 @@ async function computeAdditionalCacheKey(
*/
export async function restore(id: string, cacheDependencyPath: string) {
const packageManager = findPackageManager(id);
const primaryKey = await computeCacheKey(packageManager, cacheDependencyPath);
const [primaryKey, preparedAdditionalCaches] = await Promise.all([
computeCacheKey(packageManager, cacheDependencyPath),
prepareAdditionalCaches(packageManager.additionalCaches ?? [])
]);
core.debug(`primary key is ${primaryKey}`);
core.saveState(STATE_CACHE_PRIMARY_KEY, primaryKey);
core.setOutput(STATE_CACHE_PRIMARY_KEY, primaryKey);
for (const preparedCache of preparedAdditionalCaches) {
core.debug(
`${preparedCache.cache.name} primary key is ${preparedCache.primaryKey}`
);
core.saveState(
additionalCachePrimaryKeyState(preparedCache.cache.name),
preparedCache.primaryKey
);
}
await Promise.all([
restorePrimaryCache(packageManager, primaryKey),
...preparedAdditionalCaches.map(preparedCache =>
restoreAdditionalCache(preparedCache)
)
]);
}
async function restorePrimaryCache(
packageManager: PackageManager,
primaryKey: string
) {
// No "restoreKeys" is set, to start with a clear cache after dependency update (see https://github.com/actions/setup-java/issues/269)
const matchedKey = await cache.restoreCache(packageManager.path, primaryKey);
if (matchedKey) {
@@ -204,32 +235,39 @@ export async function restore(id: string, cacheDependencyPath: string) {
core.setOutput('cache-hit', false);
core.info(`${packageManager.id} cache is not found`);
}
for (const additionalCache of packageManager.additionalCaches ?? []) {
await restoreAdditionalCache(additionalCache);
}
}
/**
* Restore an additional cache (e.g. a build-tool wrapper distribution) that is
* keyed independently of the main dependency cache so that it survives changes
* to volatile dependency files. Skips silently when the project does not use
* the corresponding feature.
* Compute keys for additional caches (e.g. build-tool wrapper distributions).
* Additional caches without a matching configuration file are omitted.
*/
async function restoreAdditionalCache(additionalCache: AdditionalCache) {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core.debug(
`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`
);
return;
}
core.debug(`${additionalCache.name} primary key is ${primaryKey}`);
core.saveState(
additionalCachePrimaryKeyState(additionalCache.name),
primaryKey
async function prepareAdditionalCaches(
additionalCaches: AdditionalCache[]
): Promise<PreparedAdditionalCache[]> {
const preparedCaches = await Promise.all(
additionalCaches.map(async additionalCache => {
const primaryKey = await computeAdditionalCacheKey(additionalCache);
if (!primaryKey) {
core.debug(
`No file matched [${additionalCache.pattern}] for the ${additionalCache.name} cache, skipping.`
);
return undefined;
}
return {cache: additionalCache, primaryKey};
})
);
return preparedCaches.filter(
(preparedCache): preparedCache is PreparedAdditionalCache =>
preparedCache !== undefined
);
}
/**
* Restore an additional cache keyed independently of the main dependency cache.
*/
async function restoreAdditionalCache(preparedCache: PreparedAdditionalCache) {
const {cache: additionalCache, primaryKey} = preparedCache;
const matchedKey = await cache.restoreCache(additionalCache.path, primaryKey);
if (matchedKey) {
core.saveState(
@@ -237,6 +275,8 @@ async function restoreAdditionalCache(additionalCache: AdditionalCache) {
matchedKey
);
core.info(`${additionalCache.name} cache restored from key: ${matchedKey}`);
} else {
core.info(`${additionalCache.name} cache is not found`);
}
}
@@ -252,7 +292,14 @@ export async function save(id: string) {
const primaryKey = core.getState(STATE_CACHE_PRIMARY_KEY);
for (const additionalCache of packageManager.additionalCaches ?? []) {
await saveAdditionalCache(packageManager, additionalCache);
try {
await saveAdditionalCache(packageManager, additionalCache);
} catch (error) {
const err = error as Error;
core.warning(
`Failed to save ${additionalCache.name} cache: ${err.message}. Continuing with primary cache save.`
);
}
}
if (!primaryKey) {
@@ -360,7 +407,7 @@ async function saveAdditionalCache(
} else {
if (isProbablyGradleDaemonProblem(packageManager, err)) {
core.warning(
'Failed to save Gradle cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with `--no-daemon` option. Refer to https://github.com/actions/cache/issues/454 for details.'
`Failed to save ${additionalCache.name} cache on Windows. If tar.exe reported "Permission denied", try to run Gradle with \`--no-daemon\` option. Refer to https://github.com/actions/cache/issues/454 for details.`
);
}
throw error;