mirror of
https://github.com/actions/toolkit.git
synced 2026-08-15 00:00:25 +02:00
feat: backport cache read-denied + ACTIONS_CACHE_MODE handling (5.2.0)
Backport of the read-denied and ACTIONS_CACHE_MODE cache-mode gating from the ESM v6.2.0 line (#2447) to the CommonJS v5 line, released as 5.2.0. Mirrors the earlier write-denied backport (#2435, 5.1.0). - Detect the `cache read denied:` prefix on download failures (v2 twirp path and v1 `_apis/artifactcache` path) and surface it as a core.warning without failing the run. - Honor ACTIONS_CACHE_MODE: skip restore when the effective cache-mode does not permit reads (none, write-only) and skip save when it does not permit writes (none, read), logging a single non-fatal core.info line. Unset or unrecognized modes are unchanged. - Add read-denied and cache-mode tests; bump to 5.2.0 with RELEASES entry. Co-authored-by: Copilot App <[email protected]> Copilot-Session: e96deec1-716e-4e14-acdf-a230139420a2
This commit is contained in:
co-authored by
Copilot App
parent
c6ca5e729f
commit
3b3db3879e
+40
@@ -33,6 +33,19 @@ beforeAll(() => {
|
||||
// Ensure that we're using v2 for these tests
|
||||
jest.spyOn(config, 'getCacheServiceVersion').mockReturnValue('v2')
|
||||
|
||||
// config is auto-mocked; use the real cache-mode helpers so gating reflects
|
||||
// ACTIONS_CACHE_MODE and unset stays permissive.
|
||||
const actualConfig = jest.requireActual('../src/internal/config')
|
||||
jest
|
||||
.spyOn(config, 'getCacheMode')
|
||||
.mockImplementation(actualConfig.getCacheMode)
|
||||
jest
|
||||
.spyOn(config, 'isCacheReadable')
|
||||
.mockImplementation(actualConfig.isCacheReadable)
|
||||
jest
|
||||
.spyOn(config, 'isCacheWritable')
|
||||
.mockImplementation(actualConfig.isCacheWritable)
|
||||
|
||||
logDebugMock = jest.spyOn(core, 'debug')
|
||||
logInfoMock = jest.spyOn(core, 'info')
|
||||
})
|
||||
@@ -112,6 +125,33 @@ test('restore with server error should fail', async () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('restore denied by read-only token logs warning and reports cache miss', async () => {
|
||||
// The receiver returns twirp PermissionDenied (403) when the run's token has
|
||||
// no readable cache scopes; the client wraps it so the `cache read denied:`
|
||||
// prefix arrives embedded. Expect a single warning (not error) and a miss.
|
||||
const paths = ['node_modules']
|
||||
const key = 'node-test'
|
||||
const logErrorMock = jest.spyOn(core, 'error')
|
||||
const logWarningMock = jest.spyOn(core, 'warning')
|
||||
const wrappedDeniedMessage =
|
||||
'Failed to GetCacheEntryDownloadURL: Received non-retryable error: ' +
|
||||
'Failed request: (403) Forbidden: cache read denied: token has no readable scopes'
|
||||
|
||||
jest
|
||||
.spyOn(CacheServiceClientJSON.prototype, 'GetCacheEntryDownloadURL')
|
||||
.mockImplementation(() => {
|
||||
throw new Error(wrappedDeniedMessage)
|
||||
})
|
||||
|
||||
const cacheKey = await restoreCache(paths, key)
|
||||
expect(cacheKey).toBe(undefined)
|
||||
expect(logErrorMock).not.toHaveBeenCalled()
|
||||
expect(logWarningMock).toHaveBeenCalledWith(
|
||||
`Failed to restore: ${wrappedDeniedMessage}`
|
||||
)
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('restore with restore keys and no cache found', async () => {
|
||||
const paths = ['node_modules']
|
||||
const key = 'node-test'
|
||||
|
||||
Reference in New Issue
Block a user