mirror of
https://github.com/actions/toolkit.git
synced 2026-08-13 00:00:44 +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
+82
@@ -35,6 +35,18 @@ beforeAll(() => {
|
||||
jest.spyOn(cacheUtils, 'createTempDirectory').mockImplementation(async () => {
|
||||
return Promise.resolve('/foo/bar')
|
||||
})
|
||||
// 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)
|
||||
})
|
||||
|
||||
test('save with missing input should fail', async () => {
|
||||
@@ -45,6 +57,75 @@ test('save with missing input should fail', async () => {
|
||||
)
|
||||
})
|
||||
|
||||
describe('save cache-mode gating', () => {
|
||||
const originalMode = process.env.ACTIONS_CACHE_MODE
|
||||
const originalV2 = process.env.ACTIONS_CACHE_SERVICE_V2
|
||||
|
||||
const restoreEnv = (key: string, value: string | undefined): void => {
|
||||
if (value === undefined) {
|
||||
delete process.env[key]
|
||||
} else {
|
||||
process.env[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
restoreEnv('ACTIONS_CACHE_MODE', originalMode)
|
||||
restoreEnv('ACTIONS_CACHE_SERVICE_V2', originalV2)
|
||||
})
|
||||
|
||||
// The skip short-circuits before v1/v2 dispatch, so it applies regardless of
|
||||
// the ACTIONS_CACHE_SERVICE_V2 feature flag.
|
||||
test.each([
|
||||
['read', undefined],
|
||||
['read', 'true'],
|
||||
['none', undefined],
|
||||
['none', 'true']
|
||||
])(
|
||||
"mode '%s' skips save with ACTIONS_CACHE_SERVICE_V2=%s",
|
||||
async (mode, v2) => {
|
||||
process.env.ACTIONS_CACHE_MODE = mode
|
||||
restoreEnv('ACTIONS_CACHE_SERVICE_V2', v2)
|
||||
const logInfoMock = jest.spyOn(core, 'info')
|
||||
const resolvePathsMock = jest.spyOn(cacheUtils, 'resolvePaths')
|
||||
|
||||
const cacheId = await saveCache(['node_modules'], 'node-test')
|
||||
|
||||
expect(cacheId).toBe(-1)
|
||||
expect(resolvePathsMock).not.toHaveBeenCalled()
|
||||
expect(logInfoMock).toHaveBeenCalledTimes(1)
|
||||
expect(logInfoMock).toHaveBeenCalledWith(
|
||||
`Cache save skipped: the effective cache-mode '${mode}' does not permit writes.`
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test.each(['write', 'write-only', '', 'garbage'])(
|
||||
"mode '%s' does not skip save",
|
||||
async mode => {
|
||||
if (mode === '') {
|
||||
delete process.env.ACTIONS_CACHE_MODE
|
||||
} else {
|
||||
process.env.ACTIONS_CACHE_MODE = mode
|
||||
}
|
||||
const logInfoMock = jest.spyOn(core, 'info')
|
||||
const resolvePathsMock = jest.spyOn(cacheUtils, 'resolvePaths')
|
||||
|
||||
try {
|
||||
await saveCache(['node_modules'], 'node-test')
|
||||
} catch {
|
||||
// Downstream client is not fully mocked here; we only assert the guard
|
||||
// let execution proceed past it.
|
||||
}
|
||||
|
||||
expect(resolvePathsMock).toHaveBeenCalled()
|
||||
expect(logInfoMock).not.toHaveBeenCalledWith(
|
||||
expect.stringContaining('Cache save skipped')
|
||||
)
|
||||
}
|
||||
)
|
||||
})
|
||||
|
||||
test('save with large cache outputs should fail', async () => {
|
||||
const filePath = 'node_modules'
|
||||
const primaryKey = 'Linux-node-bb828da54c148048dd17899ba9fda624811cfb43'
|
||||
@@ -265,6 +346,7 @@ test('save with reserve cache denied by read-only token logs warning (not info)'
|
||||
`Failed to save: Unable to reserve cache with key ${primaryKey}. More details: ${deniedMessage}`
|
||||
)
|
||||
|
||||
expect(logWarningMock).toHaveBeenCalledTimes(1)
|
||||
expect(reserveCacheMock).toHaveBeenCalledTimes(1)
|
||||
expect(createTarMock).toHaveBeenCalledTimes(1)
|
||||
expect(saveCacheMock).toHaveBeenCalledTimes(0)
|
||||
|
||||
Reference in New Issue
Block a user