Handle early dependency cache failures during Java setup (#1226)

* Handle cache restore promise rejections

Validate dependency cache providers before Java installation and settle cache restore failures immediately while preserving setup error precedence.

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

* Make cache-rejection regression test deterministic

Reject the cache restore only after it has started and while the Java
installation is still pending, instead of relying on event-loop timing.
Verified the test fails against the pre-fix implementation.

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

* Validate cache input after required input checks

Move the package-manager validation out of the top of run() so that
missing java-version/java-version-file and toolchain id errors keep
their original precedence. Validation now happens immediately before
the cache restore is started, which still fails fast before any JDK
download.

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: Bruno Borges <brborges@microsoft.com>
This commit is contained in:
Julien Dubois
2026-08-07 05:04:13 +02:00
committed by GitHub
parent 0b0c385478
commit d17a685945
7 changed files with 149 additions and 22 deletions
+15 -1
View File
@@ -68,7 +68,7 @@ jest.unstable_mockModule('@actions/glob', () => ({
const core = await import('@actions/core'); const core = await import('@actions/core');
const cache = await import('@actions/cache'); const cache = await import('@actions/cache');
const glob = await import('@actions/glob'); const glob = await import('@actions/glob');
const {restore, save} = await import('../src/cache.js'); const {restore, save, validatePackageManager} = await import('../src/cache.js');
describe('dependency cache', () => { describe('dependency cache', () => {
const ORIGINAL_RUNNER_OS = process.env['RUNNER_OS']; const ORIGINAL_RUNNER_OS = process.env['RUNNER_OS'];
@@ -131,6 +131,20 @@ describe('dependency cache', () => {
jest.restoreAllMocks(); jest.restoreAllMocks();
}); });
describe('validatePackageManager', () => {
it('accepts supported package managers', () => {
expect(() => validatePackageManager('maven')).not.toThrow();
expect(() => validatePackageManager('gradle')).not.toThrow();
expect(() => validatePackageManager('sbt')).not.toThrow();
});
it('throws the targeted error for unsupported package managers', () => {
expect(() => validatePackageManager('ant')).toThrow(
'unknown package manager specified: ant'
);
});
});
describe('restore', () => { describe('restore', () => {
let spyCacheRestore: any; let spyCacheRestore: any;
let spyGlobHashFiles: any; let spyGlobHashFiles: any;
+81 -1
View File
@@ -47,7 +47,8 @@ jest.unstable_mockModule('../src/toolchain-ids.js', () => ({
})); }));
jest.unstable_mockModule('../src/cache.js', () => ({ jest.unstable_mockModule('../src/cache.js', () => ({
restore: jest.fn() restore: jest.fn(),
validatePackageManager: jest.fn()
})); }));
jest.unstable_mockModule('../src/cache-feature.js', () => ({ jest.unstable_mockModule('../src/cache-feature.js', () => ({
@@ -129,6 +130,9 @@ describe('setup action orchestration', () => {
(toolchains.configureToolchains as jest.Mock).mockResolvedValue(undefined); (toolchains.configureToolchains as jest.Mock).mockResolvedValue(undefined);
(auth.configureAuthentication as jest.Mock).mockResolvedValue(undefined); (auth.configureAuthentication as jest.Mock).mockResolvedValue(undefined);
(cache.restore as jest.Mock).mockResolvedValue(undefined); (cache.restore as jest.Mock).mockResolvedValue(undefined);
(cache.validatePackageManager as jest.Mock).mockImplementation(
() => undefined
);
}); });
it('does not execute the action when imported', () => { it('does not execute the action when imported', () => {
@@ -486,6 +490,41 @@ describe('setup action orchestration', () => {
); );
}); });
it('fails invalid cache input before resolving a Java distribution', async () => {
inputs.set('distribution', 'temurin');
inputs.set('cache', 'ant');
multilineInputs.set('java-version', ['21']);
const setupJava = jest.fn();
(factory.getJavaDistribution as jest.Mock).mockReturnValue({setupJava});
(cache.validatePackageManager as jest.Mock).mockImplementation(() => {
throw new Error('unknown package manager specified: ant');
});
await run();
expect(core.setFailed).toHaveBeenCalledWith(
'unknown package manager specified: ant'
);
expect(factory.getJavaDistribution).not.toHaveBeenCalled();
expect(setupJava).not.toHaveBeenCalled();
expect(cache.restore).not.toHaveBeenCalled();
});
it('reports missing java-version before validating the cache input', async () => {
inputs.set('distribution', 'temurin');
inputs.set('cache', 'ant');
(cache.validatePackageManager as jest.Mock).mockImplementation(() => {
throw new Error('unknown package manager specified: ant');
});
await run();
expect(core.setFailed).toHaveBeenCalledWith(
'java-version or java-version-file input expected'
);
expect(cache.validatePackageManager).not.toHaveBeenCalled();
});
it.each([ it.each([
['', '', false], ['', '', false],
['', 'true', true], ['', 'true', true],
@@ -592,6 +631,47 @@ describe('setup action orchestration', () => {
expect(core.setFailed).toHaveBeenCalledWith('download failed'); expect(core.setFailed).toHaveBeenCalledWith('download failed');
}); });
it('observes cache failures while Java setup is still pending', async () => {
inputs.set('distribution', 'temurin');
inputs.set('cache', 'maven');
multilineInputs.set('java-version', ['21']);
const javaSetup = deferred<{version: string; path: string}>();
const setupJava = jest.fn(() => javaSetup.promise);
(factory.getJavaDistribution as jest.Mock).mockReturnValue({setupJava});
const cacheRestore = deferred<void>();
const cacheRestoreCalled = deferred<void>();
(cache.restore as jest.Mock).mockImplementation(() => {
cacheRestoreCalled.resolve();
return cacheRestore.promise;
});
const unhandledRejections: unknown[] = [];
const onUnhandledRejection = (reason: unknown) => {
unhandledRejections.push(reason);
};
process.on('unhandledRejection', onUnhandledRejection);
const runPromise = run();
try {
// Only reject once the restore has actually started and while the Java
// installation is still pending, so the unfixed code would leave the
// rejection unhandled.
await cacheRestoreCalled.promise;
cacheRestore.reject(new Error('cache restore failed'));
await tick();
expect(setupJava).toHaveBeenCalled();
expect(unhandledRejections).toEqual([]);
expect(core.setFailed).not.toHaveBeenCalled();
} finally {
javaSetup.resolve({version: '21.0.4+7', path: '/opt/java/21'});
await runPromise;
process.off('unhandledRejection', onUnhandledRejection);
}
expect(unhandledRejections).toEqual([]);
expect(core.setFailed).toHaveBeenCalledWith('cache restore failed');
});
}); });
function deferred<T>() { function deferred<T>() {
+4 -1
View File
@@ -8,7 +8,7 @@ export const modules = {
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ save: () => (/* binding */ save) /* harmony export */ save: () => (/* binding */ save)
/* harmony export */ }); /* harmony export */ });
/* unused harmony export restore */ /* unused harmony exports validatePackageManager, restore */
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6928); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6928);
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(path__WEBPACK_IMPORTED_MODULE_0__);
/* harmony import */ var os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(857); /* harmony import */ var os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(857);
@@ -108,6 +108,9 @@ function findPackageManager(id) {
} }
return packageManager; return packageManager;
} }
function validatePackageManager(id) {
findPackageManager(id);
}
function resolveCachePaths(packageManager, cachePaths) { function resolveCachePaths(packageManager, cachePaths) {
return cachePaths.length > 0 ? cachePaths : packageManager.path; return cachePaths.length > 0 ? cachePaths : packageManager.path;
} }
+5 -1
View File
@@ -6,7 +6,8 @@ export const modules = {
/***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => { /***/ ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/* harmony export */ __webpack_require__.d(__webpack_exports__, { /* harmony export */ __webpack_require__.d(__webpack_exports__, {
/* harmony export */ restore: () => (/* binding */ restore) /* harmony export */ restore: () => (/* binding */ restore),
/* harmony export */ validatePackageManager: () => (/* binding */ validatePackageManager)
/* harmony export */ }); /* harmony export */ });
/* unused harmony export save */ /* unused harmony export save */
/* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6928); /* harmony import */ var path__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6928);
@@ -108,6 +109,9 @@ function findPackageManager(id) {
} }
return packageManager; return packageManager;
} }
function validatePackageManager(id) {
findPackageManager(id);
}
function resolveCachePaths(packageManager, cachePaths) { function resolveCachePaths(packageManager, cachePaths) {
return cachePaths.length > 0 ? cachePaths : packageManager.path; return cachePaths.length > 0 ? cachePaths : packageManager.path;
} }
+17 -9
View File
@@ -36380,8 +36380,9 @@ async function run() {
jdkFile, jdkFile,
toolchainIds toolchainIds
}; };
await validateCacheInput(cache);
cacheRestore = cache cacheRestore = cache
? startCacheRestore(cache, cacheDependencyPath, cachePath) ? settle(startCacheRestore(cache, cacheDependencyPath, cachePath))
: undefined; : undefined;
toolchainConfigurations.push(await installVersion(versionInfo.version, installerInputsOptions)); toolchainConfigurations.push(await installVersion(versionInfo.version, installerInputsOptions));
} }
@@ -36403,8 +36404,9 @@ async function run() {
jdkFile, jdkFile,
toolchainIds toolchainIds
}; };
await validateCacheInput(cache);
cacheRestore = cache cacheRestore = cache
? startCacheRestore(cache, cacheDependencyPath, cachePath) ? settle(startCacheRestore(cache, cacheDependencyPath, cachePath))
: undefined; : undefined;
for (const [index, version] of versions.entries()) { for (const [index, version] of versions.entries()) {
toolchainConfigurations.push(await installVersion(version, installerInputsOptions, index)); toolchainConfigurations.push(await installVersion(version, installerInputsOptions, index));
@@ -36419,19 +36421,25 @@ async function run() {
actionError = error; actionError = error;
} }
if (cacheRestore) { if (cacheRestore) {
try { const cacheResult = await cacheRestore;
await cacheRestore; if (cacheResult.status === 'rejected' && !actionError) {
} actionError = cacheResult.reason;
catch (error) {
if (!actionError) {
actionError = error;
}
} }
} }
if (actionError) { if (actionError) {
setup_java_core/* setFailed */.C1(actionError.message); setup_java_core/* setFailed */.C1(actionError.message);
} }
} }
async function validateCacheInput(cache) {
if (!cache) {
return;
}
const { validatePackageManager } = await Promise.all(/* import() */[__nccwpck_require__.e(824), __nccwpck_require__.e(971), __nccwpck_require__.e(377)]).then(__nccwpck_require__.bind(__nccwpck_require__, 7377));
validatePackageManager(cache);
}
function settle(promise) {
return promise.then(value => ({ status: 'fulfilled', value }), reason => ({ status: 'rejected', reason }));
}
if (process.argv[1] === (0,external_url_.fileURLToPath)(import.meta.url)) { if (process.argv[1] === (0,external_url_.fileURLToPath)(import.meta.url)) {
run(); run();
} }
+4
View File
@@ -138,6 +138,10 @@ function findPackageManager(id: string): PackageManager {
return packageManager; return packageManager;
} }
export function validatePackageManager(id: string): void {
findPackageManager(id);
}
function resolveCachePaths( function resolveCachePaths(
packageManager: PackageManager, packageManager: PackageManager,
cachePaths: string[] cachePaths: string[]
+23 -9
View File
@@ -37,7 +37,7 @@ export async function run() {
core.getInput(constants.INPUT_VERIFY_SIGNATURE_PUBLIC_KEY) || undefined; core.getInput(constants.INPUT_VERIFY_SIGNATURE_PUBLIC_KEY) || undefined;
const toolchainIds = core.getMultilineInput(constants.INPUT_MVN_TOOLCHAIN_ID); const toolchainIds = core.getMultilineInput(constants.INPUT_MVN_TOOLCHAIN_ID);
let actionError: Error | undefined; let actionError: Error | undefined;
let cacheRestore: Promise<void> | undefined; let cacheRestore: Promise<PromiseSettledResult<void>> | undefined;
const toolchainConfigurations: ToolchainConfiguration[] = []; const toolchainConfigurations: ToolchainConfiguration[] = [];
try { try {
@@ -94,8 +94,9 @@ export async function run() {
toolchainIds toolchainIds
}; };
await validateCacheInput(cache);
cacheRestore = cache cacheRestore = cache
? startCacheRestore(cache, cacheDependencyPath, cachePath) ? settle(startCacheRestore(cache, cacheDependencyPath, cachePath))
: undefined; : undefined;
toolchainConfigurations.push( toolchainConfigurations.push(
await installVersion(versionInfo.version, installerInputsOptions) await installVersion(versionInfo.version, installerInputsOptions)
@@ -120,8 +121,9 @@ export async function run() {
toolchainIds toolchainIds
}; };
await validateCacheInput(cache);
cacheRestore = cache cacheRestore = cache
? startCacheRestore(cache, cacheDependencyPath, cachePath) ? settle(startCacheRestore(cache, cacheDependencyPath, cachePath))
: undefined; : undefined;
for (const [index, version] of versions.entries()) { for (const [index, version] of versions.entries()) {
toolchainConfigurations.push( toolchainConfigurations.push(
@@ -144,12 +146,9 @@ export async function run() {
} }
if (cacheRestore) { if (cacheRestore) {
try { const cacheResult = await cacheRestore;
await cacheRestore; if (cacheResult.status === 'rejected' && !actionError) {
} catch (error) { actionError = cacheResult.reason as Error;
if (!actionError) {
actionError = error as Error;
}
} }
} }
@@ -158,6 +157,21 @@ export async function run() {
} }
} }
async function validateCacheInput(cache: string): Promise<void> {
if (!cache) {
return;
}
const {validatePackageManager} = await import('./cache.js');
validatePackageManager(cache);
}
function settle<T>(promise: Promise<T>): Promise<PromiseSettledResult<T>> {
return promise.then<PromiseFulfilledResult<T>, PromiseRejectedResult>(
value => ({status: 'fulfilled', value}),
reason => ({status: 'rejected', reason})
);
}
if (process.argv[1] === fileURLToPath(import.meta.url)) { if (process.argv[1] === fileURLToPath(import.meta.url)) {
run(); run();
} else { } else {