Skip to content

Commit caa2961

Browse files
authored
Merge pull request #1775 from jasongin/readonly-cache-v5
Bump @actions/cache to v5.1.0 - handle read-only cache access
2 parents 27d5ce7 + 00c2da9 commit caa2961

11 files changed

Lines changed: 224 additions & 37 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.

‎README.md‎

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ If you do not upgrade, all workflow runs using any of the deprecated [actions/ca
3333

3434
Upgrading to the recommended versions will not break your workflows.
3535

36-
> **Additionally, if you are managing your own GitHub runners, you must update your runner version to `2.231.0` or newer to ensure compatibility with the new cache service.**
36+
> **Additionally, if you are managing your own GitHub runners, you must update your runner version to `2.231.0` or newer to ensure compatibility with the new cache service.**
3737
> Failure to update both the action version and your runner version may result in workflow failures after the migration date.
3838
3939
Read more about the change & access the migration guide: [reference to the announcement](https://github.com/actions/cache/discussions/1510).
@@ -109,6 +109,14 @@ The cache is scoped to the key, [version](#cache-version), and branch. The defau
109109

110110
See [Matching a cache key](https://help.github.com/en/actions/configuring-and-managing-workflows/caching-dependencies-to-speed-up-workflows#matching-a-cache-key) for more info.
111111

112+
### Read-only access
113+
114+
Some workflow runs only have read-only access to the cache. A common case is a workflow triggered by a pull request from a fork: such runs can **restore** existing caches but may not be permitted to **save** new ones.
115+
116+
When the cache token is read-only, the save step does not fail the job. Instead, `@actions/cache` reports the denial once as a warning (for example, `Failed to save: ... cache write denied: ...`) and the step completes successfully without writing a cache entry. Restores in the same run continue to work as usual.
117+
118+
> **Note** This applies to the action's normal save path as well as the standalone [Save action](./save/README.md). If you intentionally want a restore-only setup, see [Make cache read only / Reuse cache from centralized job](./caching-strategies.md#make-cache-read-only--reuse-cache-from-centralized-job).
119+
112120
### Example cache workflow
113121

114122
#### Restoring and saving cache using a single action
@@ -351,7 +359,7 @@ Please note that Windows environment variables (like `%LocalAppData%`) will NOT
351359

352360
## Note
353361

354-
Thank you for your interest in this GitHub repo, however, right now we are not taking contributions.
362+
Thank you for your interest in this GitHub repo, however, right now we are not taking contributions.
355363

356364
We continue to focus our resources on strategic areas that help our customers be successful while making developers' lives easier. While GitHub Actions remains a key part of this vision, we are allocating resources towards other areas of Actions and are not taking contributions to this repository at this time. The GitHub public roadmap is the best place to follow along for any updates on features we’re working on and what stage they’re in.
357365

@@ -369,4 +377,4 @@ You are welcome to still raise bugs in this repo.
369377

370378
## License
371379

372-
The scripts and documentation in this project are released under the [MIT License](LICENSE)
380+
The scripts and documentation in this project are released under the [MIT License](LICENSE)

‎RELEASES.md‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
## How to prepare a release
44

5-
> [!NOTE]
5+
> [!NOTE]
66
> Relevant for maintainers with write access only.
77
88
1. Switch to a new branch from `main`.
@@ -21,10 +21,15 @@
2121
1. Publish the release.
2222
1. Navigate to https://github.com/actions/cache/actions/workflows/release-new-action-version.yml
2323
1. There should be a workflow run queued with the same version number.
24-
1. Approve the run to publish the new version and update the major tags for this action.
24+
1. Approve the run to publish the new version and update the major tags for this action.
2525

2626
## Changelog
2727

28+
### 5.1.0
29+
30+
- Bump `@actions/cache` to v5.1.0 to pick up [actions/toolkit#2435 Handle cache write error due to read-only token](https://github.com/actions/toolkit/pull/2435)
31+
- Switch redundant "Cache save failed" warning to debug log in save-only
32+
2833
### 5.0.4
2934

3035
- Bump `minimatch` to v3.1.5 (fixes ReDoS via globstar patterns)

‎__tests__/saveOnly.test.ts‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -105,8 +105,10 @@ test("save with valid inputs uploads a cache", async () => {
105105
expect(failedMock).toHaveBeenCalledTimes(0);
106106
});
107107

108-
test("save failing logs the warning message", async () => {
108+
test("save failing logs the debug message", async () => {
109+
const debugMock = jest.spyOn(core, "debug");
109110
const warningMock = jest.spyOn(core, "warning");
111+
const failedMock = jest.spyOn(core, "setFailed");
110112

111113
const primaryKey = "Linux-node-bb828da54c148048dd17899ba9fda624811cfb43";
112114

@@ -115,6 +117,9 @@ test("save failing logs the warning message", async () => {
115117
testUtils.setInput(Inputs.Path, inputPath);
116118
testUtils.setInput(Inputs.UploadChunkSize, "4000000");
117119

120+
// A read-only / write-denied save surfaces to the action as saveCache resolving
121+
// to -1; the toolkit has already logged the underlying reason. The action
122+
// must not fail the job or emit its own warning.
118123
const cacheId = -1;
119124
const saveCacheMock = jest
120125
.spyOn(cache, "saveCache")
@@ -134,6 +139,7 @@ test("save failing logs the warning message", async () => {
134139
false
135140
);
136141

137-
expect(warningMock).toHaveBeenCalledTimes(1);
138-
expect(warningMock).toHaveBeenCalledWith("Cache save failed.");
142+
expect(debugMock).toHaveBeenCalledWith("Cache was not saved.");
143+
expect(warningMock).not.toHaveBeenCalled();
144+
expect(failedMock).not.toHaveBeenCalled();
139145
});

‎dist/restore-only/index.js‎

Lines changed: 43 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
}
@@ -87527,7 +87566,7 @@ function randomUUID() {
8752787566
/***/ ((module) => {
8752887567

8752987568
"use strict";
87530-
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"}}');
87569+
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"}}');
8753187570

8753287571
/***/ })
8753387572

0 commit comments

Comments
 (0)