Skip to content

Commit 0f7857f

Browse files
authored
fix(cache): report a denied cache write as a failure (#1370)
@actions/cache catches reservation and upload failures internally and returns -1 rather than throwing, so the catch block never ran and every save reported success. A run whose write was denied logged 'Cache saved' 25ms after the platform refused it, then set the state flag that tells the post-action there is nothing left to do. Branch on the sentinel at all three call sites. The library cannot distinguish a policy denial from a benign key collision once it has swallowed the error, so a collision is now reported as a failure too; the alternative is continuing to report real failures as success. Restores the post-action retry, which the false success had been suppressing.
1 parent d0a99d3 commit 0f7857f

9 files changed

Lines changed: 112 additions & 9 deletions

File tree

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

dist/main.js

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/post.js

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

src/services/cache/cache.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -470,6 +470,36 @@ describe('saveCache', () => {
470470
expect(result).toBe(true)
471471
})
472472

473+
it('returns false and warns when save returns the failure sentinel', async () => {
474+
// #given storage with content and an adapter that reports an unsuccessful save
475+
await fs.mkdir(storagePath, {recursive: true})
476+
await fs.writeFile(path.join(storagePath, 'session.db'), 'test data')
477+
478+
const logger: Logger = {
479+
debug: vi.fn(),
480+
info: vi.fn(),
481+
warning: vi.fn(),
482+
error: vi.fn(),
483+
}
484+
const adapter = createMockCacheAdapter({saveResult: -1})
485+
486+
// #when saving cache
487+
const result = await saveCache({
488+
components: testComponents,
489+
runId: 98765,
490+
logger,
491+
storagePath,
492+
authPath,
493+
cacheAdapter: adapter,
494+
})
495+
496+
// #then the failure sentinel is reported as an unpersisted cache
497+
expect(result).toBe(false)
498+
expect(logger.warning).toHaveBeenCalledWith('Cache save did not persist', {
499+
saveKey: 'opencode-storage-github-owner-repo-main-Linux-98765',
500+
})
501+
})
502+
473503
it('writes to object store and cache when configured', async () => {
474504
await fs.mkdir(storagePath, {recursive: true})
475505
await fs.writeFile(path.join(storagePath, 'session.db'), 'test data')

src/services/cache/dedup.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,25 @@ describe('saveDeduplicationMarker', () => {
137137
expect(saveCache).toHaveBeenCalledWith([testEntityDir], `${DEDUP_CACHE_PREFIX}-owner-repo-pr-42-3003`)
138138
})
139139

140+
it('returns false and warns when save returns the failure sentinel', async () => {
141+
// #given a marker and an adapter that reports an unsuccessful save
142+
const marker = createMarker(3503)
143+
const logger = createMockLogger()
144+
const cacheAdapter: CacheAdapter = {
145+
restoreCache: vi.fn(async () => undefined),
146+
saveCache: vi.fn(async () => -1),
147+
}
148+
149+
// #when saving the deduplication marker
150+
const result = await saveDeduplicationMarker(testRepo, testEntity, marker, logger, cacheAdapter)
151+
152+
// #then the failure sentinel is reported as an unpersisted marker
153+
expect(result).toBe(false)
154+
expect(logger.warning).toHaveBeenCalledWith('Dedup marker cache save did not persist', {
155+
saveKey: `${DEDUP_CACHE_PREFIX}-owner-repo-pr-42-3503`,
156+
})
157+
})
158+
140159
it('writes sentinel file with marker json content', async () => {
141160
// #given marker and save adapter
142161
const marker = createMarker(4004)

src/services/cache/dedup.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,14 @@ export async function saveDeduplicationMarker(
8787
try {
8888
await fs.mkdir(entityDir, {recursive: true})
8989
await fs.writeFile(sentinelPath, JSON.stringify(marker), 'utf8')
90-
await cacheAdapter.saveCache([entityDir], saveKey)
90+
const cacheId = await cacheAdapter.saveCache([entityDir], saveKey)
91+
// @actions/cache returns -1 for both write failures and reservation collisions. The
92+
// adapter exposes no reason, so report the marker as unpersisted rather than claiming success.
93+
if (cacheId === -1) {
94+
logger.warning('Dedup marker cache save did not persist', {saveKey})
95+
return false
96+
}
97+
9198
return true
9299
} catch (error) {
93100
const message = toErrorMessage(error).toLowerCase()

src/services/cache/save.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,14 @@ export async function saveCache(options: SaveCacheOptions): Promise<boolean> {
113113
}
114114
}
115115

116-
await cacheAdapter.saveCache(cachePaths, saveKey)
116+
const cacheId = await cacheAdapter.saveCache(cachePaths, saveKey)
117+
// @actions/cache returns -1 for both write failures and reservation collisions. The
118+
// adapter exposes no reason, so report the save as unpersisted rather than claiming success.
119+
if (cacheId === -1) {
120+
logger.warning('Cache save did not persist', {saveKey})
121+
return false
122+
}
123+
117124
logger.info('Cache saved', {saveKey})
118125
return true
119126
} catch (error) {

src/services/setup/tools-cache.test.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {Logger} from '../../shared/logger.js'
22
import * as fs from 'node:fs/promises'
33
import * as os from 'node:os'
44
import * as path from 'node:path'
5-
import {afterEach, beforeEach, describe, expect, it} from 'vitest'
5+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
66
import {
77
buildCachePaths,
88
buildToolsCacheKey,
@@ -618,6 +618,39 @@ describe('saveToolsCache', () => {
618618
expect(result).toBe(true)
619619
})
620620

621+
it('returns false and warns when save returns the failure sentinel', async () => {
622+
// #given an adapter that reports an unsuccessful save
623+
const logger: Logger = {
624+
debug: vi.fn(),
625+
info: vi.fn(),
626+
warning: vi.fn(),
627+
error: vi.fn(),
628+
}
629+
const adapter = createMockToolsCacheAdapter({saveResult: -1})
630+
631+
// #when saving the tools cache
632+
const result = await saveToolsCache({
633+
logger,
634+
os: 'Linux',
635+
bunVersion: '1.3.14',
636+
opencodeVersion: '1.0.0',
637+
omoVersion: '3.5.5',
638+
systematicVersion: '2.1.0',
639+
cacheMode: 'enabled',
640+
toolCachePath,
641+
bunCachePath,
642+
omoConfigPath,
643+
opencodeCachePath,
644+
cacheAdapter: adapter,
645+
})
646+
647+
// #then the failure sentinel is reported as an unpersisted cache
648+
expect(result).toBe(false)
649+
expect(logger.warning).toHaveBeenCalledWith('Tools cache save did not persist', {
650+
saveKey: 'opencode-tools-Linux-enabled-oc-1.0.0-omo-3.5.5-sys-2.1.0-bun-1.3.14',
651+
})
652+
})
653+
621654
it('saves cache successfully in disabled mode', async () => {
622655
// #given a cache adapter
623656
const adapter = createMockToolsCacheAdapter({saveResult: 1024})

src/services/setup/tools-cache.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -170,7 +170,14 @@ export async function saveToolsCache(options: SaveToolsCacheOptions): Promise<bo
170170
logger.info('Saving tools cache', {saveKey, paths: cachePaths})
171171

172172
try {
173-
await cacheAdapter.saveCache([...cachePaths], saveKey)
173+
const cacheId = await cacheAdapter.saveCache([...cachePaths], saveKey)
174+
// @actions/cache returns -1 for both write failures and reservation collisions. The
175+
// adapter exposes no reason, so report the save as unpersisted rather than claiming success.
176+
if (cacheId === -1) {
177+
logger.warning('Tools cache save did not persist', {saveKey})
178+
return false
179+
}
180+
174181
logger.info('Tools cache saved', {saveKey})
175182
return true
176183
} catch (error) {

0 commit comments

Comments
 (0)