Skip to content

Commit e91cc3b

Browse files
authored
Bump @actions/cache to 5.1.0, log cache write denied (#758)
* Bump @actions/cache to 5.1.0, log cache write denied * Add cache save tests * Re-trigger CI
1 parent 4a2405e commit e91cc3b

7 files changed

Lines changed: 258 additions & 17 deletions

File tree

.licenses/npm/@actions/cache.dep.yml

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

__tests__/cache-save.test.ts

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import * as cache from '@actions/cache';
2+
import * as core from '@actions/core';
3+
import fs from 'fs';
4+
5+
import {run} from '../src/cache-save';
6+
import * as cacheUtils from '../src/cache-utils';
7+
import {State} from '../src/constants';
8+
9+
describe('cache-save', () => {
10+
const primaryKey = 'primary-key';
11+
12+
let primaryKeyValue: string;
13+
let matchedKeyValue: string;
14+
15+
let getBooleanInputSpy: jest.SpyInstance;
16+
let getStateSpy: jest.SpyInstance;
17+
let infoSpy: jest.SpyInstance;
18+
let warningSpy: jest.SpyInstance;
19+
let debugSpy: jest.SpyInstance;
20+
let setFailedSpy: jest.SpyInstance;
21+
let saveCacheSpy: jest.SpyInstance;
22+
let getCacheDirectoryPathSpy: jest.SpyInstance;
23+
let existsSpy: jest.SpyInstance;
24+
25+
beforeEach(() => {
26+
primaryKeyValue = primaryKey;
27+
matchedKeyValue = 'matched-key';
28+
29+
getBooleanInputSpy = jest.spyOn(core, 'getBooleanInput');
30+
getBooleanInputSpy.mockReturnValue(true);
31+
32+
getStateSpy = jest.spyOn(core, 'getState');
33+
getStateSpy.mockImplementation((key: string) => {
34+
if (key === State.CachePrimaryKey) {
35+
return primaryKeyValue;
36+
}
37+
if (key === State.CacheMatchedKey) {
38+
return matchedKeyValue;
39+
}
40+
return '';
41+
});
42+
43+
infoSpy = jest.spyOn(core, 'info');
44+
infoSpy.mockImplementation(() => undefined);
45+
46+
warningSpy = jest.spyOn(core, 'warning');
47+
warningSpy.mockImplementation(() => undefined);
48+
49+
debugSpy = jest.spyOn(core, 'debug');
50+
debugSpy.mockImplementation(() => undefined);
51+
52+
setFailedSpy = jest.spyOn(core, 'setFailed');
53+
setFailedSpy.mockImplementation(() => undefined);
54+
55+
saveCacheSpy = jest.spyOn(cache, 'saveCache');
56+
saveCacheSpy.mockImplementation(() => Promise.resolve(0));
57+
58+
getCacheDirectoryPathSpy = jest.spyOn(cacheUtils, 'getCacheDirectoryPath');
59+
getCacheDirectoryPathSpy.mockImplementation(() =>
60+
Promise.resolve(['cache_directory_path', 'cache_directory_path'])
61+
);
62+
63+
existsSpy = jest.spyOn(fs, 'existsSync');
64+
existsSpy.mockImplementation(() => true);
65+
});
66+
67+
afterEach(() => {
68+
jest.restoreAllMocks();
69+
});
70+
71+
it('does not save cache when the cache input is false', async () => {
72+
getBooleanInputSpy.mockReturnValue(false);
73+
74+
await run();
75+
76+
expect(saveCacheSpy).not.toHaveBeenCalled();
77+
expect(warningSpy).not.toHaveBeenCalled();
78+
expect(setFailedSpy).not.toHaveBeenCalled();
79+
});
80+
81+
it('does not save cache when there are no cache folders on the disk', async () => {
82+
existsSpy.mockImplementation(() => false);
83+
84+
await run();
85+
86+
expect(warningSpy).toHaveBeenCalledWith(
87+
'There are no cache folders on the disk'
88+
);
89+
expect(saveCacheSpy).not.toHaveBeenCalled();
90+
expect(setFailedSpy).not.toHaveBeenCalled();
91+
});
92+
93+
it('does not save cache when the primary key was not generated', async () => {
94+
primaryKeyValue = '';
95+
96+
await run();
97+
98+
expect(infoSpy).toHaveBeenCalledWith(
99+
'Primary key was not generated. Please check the log messages above for more errors or information'
100+
);
101+
expect(saveCacheSpy).not.toHaveBeenCalled();
102+
expect(setFailedSpy).not.toHaveBeenCalled();
103+
});
104+
105+
it('does not save cache when a cache hit occurred on the primary key', async () => {
106+
matchedKeyValue = primaryKey;
107+
108+
await run();
109+
110+
expect(infoSpy).toHaveBeenCalledWith(
111+
`Cache hit occurred on the primary key ${primaryKey}, not saving cache.`
112+
);
113+
expect(saveCacheSpy).not.toHaveBeenCalled();
114+
expect(setFailedSpy).not.toHaveBeenCalled();
115+
});
116+
117+
it('saves cache when the primary key differs from the matched key', async () => {
118+
await run();
119+
120+
expect(saveCacheSpy).toHaveBeenCalled();
121+
expect(infoSpy).toHaveBeenCalledWith(
122+
`Cache saved with the key: ${primaryKey}`
123+
);
124+
expect(warningSpy).not.toHaveBeenCalled();
125+
expect(setFailedSpy).not.toHaveBeenCalled();
126+
});
127+
128+
it('save with -1 cacheId , should not fail workflow', async () => {
129+
saveCacheSpy.mockImplementation(() => Promise.resolve(-1));
130+
131+
await run();
132+
133+
expect(saveCacheSpy).toHaveBeenCalled();
134+
expect(debugSpy).toHaveBeenCalledWith(
135+
`Cache was not saved for the key: ${primaryKey}`
136+
);
137+
expect(infoSpy).not.toHaveBeenCalledWith(
138+
`Cache saved with the key: ${primaryKey}`
139+
);
140+
expect(warningSpy).not.toHaveBeenCalled();
141+
expect(setFailedSpy).not.toHaveBeenCalled();
142+
});
143+
144+
it('saves with error from toolkit, should not fail workflow', async () => {
145+
saveCacheSpy.mockImplementation(() =>
146+
Promise.reject(new Error('Unable to reach the service'))
147+
);
148+
149+
await run();
150+
151+
expect(saveCacheSpy).toHaveBeenCalled();
152+
expect(warningSpy).toHaveBeenCalledWith('Unable to reach the service');
153+
expect(setFailedSpy).not.toHaveBeenCalled();
154+
});
155+
});

dist/cache-save/index.js

Lines changed: 47 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
4949
});
5050
};
5151
Object.defineProperty(exports, "__esModule", ({ value: true }));
52-
exports.FinalizeCacheError = exports.ReserveCacheError = exports.ValidationError = void 0;
52+
exports.FinalizeCacheError = exports.CacheWriteDeniedError = exports.CACHE_WRITE_DENIED_PREFIX = exports.ReserveCacheError = exports.ValidationError = void 0;
5353
exports.isFeatureAvailable = isFeatureAvailable;
5454
exports.restoreCache = restoreCache;
5555
exports.saveCache = saveCache;
@@ -77,6 +77,26 @@ class ReserveCacheError extends Error {
7777
}
7878
}
7979
exports.ReserveCacheError = ReserveCacheError;
80+
/**
81+
* Stable prefix used by the cache receiver to signal that the token has
82+
* no writable scopes (read-only cache policy). Consumers can match on
83+
* this prefix to distinguish policy denials from ordinary contention.
84+
*/
85+
exports.CACHE_WRITE_DENIED_PREFIX = 'cache write denied:';
86+
/**
87+
* Extends ReserveCacheError for source-compatibility: existing
88+
* `instanceof ReserveCacheError` checks and `typedError.name ===
89+
* ReserveCacheError.name` paths keep working, while consumers that want to
90+
* distinguish a policy denial can check for CacheWriteDeniedError.name.
91+
*/
92+
class CacheWriteDeniedError extends ReserveCacheError {
93+
constructor(message) {
94+
super(message);
95+
this.name = 'CacheWriteDeniedError';
96+
Object.setPrototypeOf(this, CacheWriteDeniedError.prototype);
97+
}
98+
}
99+
exports.CacheWriteDeniedError = CacheWriteDeniedError;
80100
class FinalizeCacheError extends Error {
81101
constructor(message) {
82102
super(message);
@@ -387,7 +407,11 @@ function saveCacheV1(paths_1, key_1, options_1) {
387407
throw new Error((_d = (_c = reserveCacheResponse === null || reserveCacheResponse === void 0 ? void 0 : reserveCacheResponse.error) === null || _c === void 0 ? void 0 : _c.message) !== null && _d !== void 0 ? _d : `Cache size of ~${Math.round(archiveFileSize / (1024 * 1024))} MB (${archiveFileSize} B) is over the data cap limit, not saving cache.`);
388408
}
389409
else {
390-
throw new ReserveCacheError(`Unable to reserve cache with key ${key}, another job may be creating this cache. More details: ${(_e = reserveCacheResponse === null || reserveCacheResponse === void 0 ? void 0 : reserveCacheResponse.error) === null || _e === void 0 ? void 0 : _e.message}`);
410+
const detailMessage = (_e = reserveCacheResponse === null || reserveCacheResponse === void 0 ? void 0 : reserveCacheResponse.error) === null || _e === void 0 ? void 0 : _e.message;
411+
if (detailMessage === null || detailMessage === void 0 ? void 0 : detailMessage.startsWith(exports.CACHE_WRITE_DENIED_PREFIX)) {
412+
throw new CacheWriteDeniedError(`Unable to reserve cache with key ${key}. More details: ${detailMessage}`);
413+
}
414+
throw new ReserveCacheError(`Unable to reserve cache with key ${key}, another job may be creating this cache. More details: ${detailMessage}`);
391415
}
392416
core.debug(`Saving Cache (ID: ${cacheId})`);
393417
yield cacheHttpClient.saveCache(cacheId, archivePath, '', options);
@@ -397,6 +421,9 @@ function saveCacheV1(paths_1, key_1, options_1) {
397421
if (typedError.name === ValidationError.name) {
398422
throw error;
399423
}
424+
else if (typedError.name === CacheWriteDeniedError.name) {
425+
core.warning(`Failed to save: ${typedError.message}`);
426+
}
400427
else if (typedError.name === ReserveCacheError.name) {
401428
core.info(`Failed to save: ${typedError.message}`);
402429
}
@@ -435,6 +462,7 @@ function saveCacheV1(paths_1, key_1, options_1) {
435462
*/
436463
function saveCacheV2(paths_1, key_1, options_1) {
437464
return __awaiter(this, arguments, void 0, function* (paths, key, options, enableCrossOsArchive = false) {
465+
var _a;
438466
// Override UploadOptions to force the use of Azure
439467
// ...options goes first because we want to override the default values
440468
// set in UploadOptions with these specific figures
@@ -470,7 +498,11 @@ function saveCacheV2(paths_1, key_1, options_1) {
470498
try {
471499
const response = yield twirpClient.CreateCacheEntry(request);
472500
if (!response.ok) {
473-
if (response.message) {
501+
// Skip the redundant inner warning when the receiver signalled a
502+
// policy denial: the outer catch arm below will log a single
503+
// customer-facing warning.
504+
if (response.message &&
505+
!response.message.startsWith(exports.CACHE_WRITE_DENIED_PREFIX)) {
474506
core.warning(`Cache reservation failed: ${response.message}`);
475507
}
476508
throw new Error(response.message || 'Response was not ok');
@@ -479,6 +511,10 @@ function saveCacheV2(paths_1, key_1, options_1) {
479511
}
480512
catch (error) {
481513
core.debug(`Failed to reserve cache: ${error}`);
514+
const errorMessage = (_a = error === null || error === void 0 ? void 0 : error.message) !== null && _a !== void 0 ? _a : '';
515+
if (errorMessage.startsWith(exports.CACHE_WRITE_DENIED_PREFIX)) {
516+
throw new CacheWriteDeniedError(`Unable to reserve cache with key ${key}. More details: ${errorMessage}`);
517+
}
482518
throw new ReserveCacheError(`Unable to reserve cache with key ${key}, another job may be creating this cache.`);
483519
}
484520
core.debug(`Attempting to upload cache located at: ${archivePath}`);
@@ -503,6 +539,9 @@ function saveCacheV2(paths_1, key_1, options_1) {
503539
if (typedError.name === ValidationError.name) {
504540
throw error;
505541
}
542+
else if (typedError.name === CacheWriteDeniedError.name) {
543+
core.warning(`Failed to save: ${typedError.message}`);
544+
}
506545
else if (typedError.name === ReserveCacheError.name) {
507546
core.info(`Failed to save: ${typedError.message}`);
508547
}
@@ -46404,6 +46443,10 @@ const cachePackages = () => __awaiter(void 0, void 0, void 0, function* () {
4640446443
}
4640546444
const cacheId = yield cache.saveCache(cachePaths, primaryKey);
4640646445
if (cacheId === -1) {
46446+
// saveCache returns -1 without throwing when the cache was not saved, e.g.
46447+
// a reserve collision or a read-only token (fork PR). @actions/cache has
46448+
// already logged the reason at the appropriate severity, so just trace it.
46449+
core.debug(`Cache was not saved for the key: ${primaryKey}`);
4640746450
return;
4640846451
}
4640946452
core.info(`Cache saved with the key: ${primaryKey}`);
@@ -87992,7 +88035,7 @@ function randomUUID() {
8799288035
/***/ ((module) => {
8799388036

8799488037
"use strict";
87995-
module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"5.0.5","preview":true,"description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","main":"lib/cache.js","types":"lib/cache.d.ts","directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^2.0.0","@actions/exec":"^2.0.0","@actions/glob":"^0.5.1","@protobuf-ts/runtime-rpc":"^2.11.1","@actions/http-client":"^3.0.2","@actions/io":"^2.0.0","@azure/abort-controller":"^1.1.0","@azure/core-rest-pipeline":"^1.22.0","@azure/storage-blob":"^12.29.1","semver":"^6.3.1"},"devDependencies":{"@types/node":"^24.1.0","@types/semver":"^6.0.0","@protobuf-ts/plugin":"^2.9.4","typescript":"^5.2.2"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}');
88038+
module.exports = /*#__PURE__*/JSON.parse('{"name":"@actions/cache","version":"5.1.0","preview":true,"description":"Actions cache lib","keywords":["github","actions","cache"],"homepage":"https://github.com/actions/toolkit/tree/main/packages/cache","license":"MIT","main":"lib/cache.js","types":"lib/cache.d.ts","directories":{"lib":"lib","test":"__tests__"},"files":["lib","!.DS_Store"],"publishConfig":{"access":"public"},"repository":{"type":"git","url":"git+https://github.com/actions/toolkit.git","directory":"packages/cache"},"scripts":{"audit-moderate":"npm install && npm audit --json --audit-level=moderate > audit.json","test":"echo \\"Error: run tests from root\\" && exit 1","tsc":"tsc"},"bugs":{"url":"https://github.com/actions/toolkit/issues"},"dependencies":{"@actions/core":"^2.0.0","@actions/exec":"^2.0.0","@actions/glob":"^0.5.1","@protobuf-ts/runtime-rpc":"^2.11.1","@actions/http-client":"^3.0.2","@actions/io":"^2.0.0","@azure/abort-controller":"^1.1.0","@azure/core-rest-pipeline":"^1.22.0","@azure/storage-blob":"^12.29.1","semver":"^6.3.1"},"devDependencies":{"@types/node":"^24.1.0","@types/semver":"^6.0.0","@protobuf-ts/plugin":"^2.9.4","typescript":"^5.2.2"},"overrides":{"uri-js":"npm:uri-js-replace@^1.0.1","node-fetch":"^3.3.2"}}');
8799688039

8799788040
/***/ })
8799888041

0 commit comments

Comments
 (0)