Skip to content

feat(cache): add cache-mode client behavior (read-denied warning + ACTIONS_CACHE_MODE skip)#2447

Merged
philip-gai merged 15 commits into
mainfrom
philip-gai/cache-read-denied-warning
Jul 13, 2026
Merged

feat(cache): add cache-mode client behavior (read-denied warning + ACTIONS_CACHE_MODE skip)#2447
philip-gai merged 15 commits into
mainfrom
philip-gai/cache-read-denied-warning

Conversation

@philip-gai

@philip-gai philip-gai commented Jul 1, 2026

Copy link
Copy Markdown
Member

Description

This PR adds client-side cache-mode behavior to @actions/cache. It combines two related pieces of work:

  • Surface the service-side read-denied policy as a non-fatal warning.
  • Honor a new ACTIONS_CACHE_MODE environment variable to skip cache operations the effective mode does not permit.

Cache-mode values are none | read | write | write-only (hyphenated). It is a partial lattice, not linear: readable modes = {read, write}, writable modes = {write, write-only}, none = neither. The ACTIONS_CACHE_MODE env var is provided by the Actions runner, and is gated on the service side. When it is unset or unrecognized, behavior is identical to today (regression-safe).

Read-denied warning

When the cache service denies a download because the token has no readable scopes, the run should continue with a clear warning instead of a confusing failure.

  • Added CacheReadDeniedError and the shared cache read denied: prefix constant.
  • On the v2 restore path, the denial arrives as a wrapped 403; the restore dispatch detects the prefix and surfaces it as a core.warning, then returns a cache miss.
  • Matches the existing write-denied handling (cache write denied:) for consistency.

ACTIONS_CACHE_MODE skip

Skip operations the effective cache-mode does not permit, before any tar or network work.

  • Skip restore when the mode is not readable (none, write-only).
  • Skip save when the mode is not writable (none, read).
  • Restore skip returns a cache miss (undefined); save skip returns the existing not-saved sentinel (-1). Neither throws.
  • Exactly one core.info line per skip; extra detail (paths, key) is behind ACTIONS_STEP_DEBUG.
  • Unset or unrecognized modes are permissive, so behavior is unchanged.

Rationale for skipping restore on write-only: write-only tokens have no read scope, so a restore would be denied service-side and trip the read-denied warning. Skipping client-side avoids the wasted round-trip and gives a clearer message.

Companion changes

  • actions/runner exports ACTIONS_CACHE_MODE into the step environment.

Notes

  • packages/cache/RELEASES.md updated under the 6.2.0 entry.
  • Test coverage: full cache suite passing, including the read-denied dispatch and a cache-mode truth table (none/read/write/write-only plus unset and unknown regression cases).

https://github.com/github/actions-persistence/issues/1168

Mirror the existing cache write-denied handling on the restore path. When
the receiver refuses a download URL because the run's token has no readable
cache scopes, it returns a twirp PermissionDenied (HTTP 403). The twirp
client wraps that 403 in a generic Error, so the stable 'cache read denied:'
prefix is embedded in the message rather than at the start.

- Add CACHE_READ_DENIED_PREFIX and CacheReadDeniedError
- Dispatch on the prefix in the restoreCacheV2 catch block (V2 only), log a
  policy-specific warning, and report a cache miss so the run continues
- Add a test mirroring the write-denied coverage
Re-throw CacheReadDeniedError from an inner try/catch around
GetCacheEntryDownloadURL and dispatch on typedError.name in the outer catch,
matching how saveCacheV2 handles CacheWriteDeniedError.
Extend the read-denied handling to Cache Service v1 so GHES (which forces v1
via _apis/artifactcache) is covered when read-scope enforcement ships there.

- Surface the receiver's error body message from getCacheEntry instead of a
  generic status-code error, so the cache read denied: prefix reaches callers
- Re-throw CacheReadDeniedError from restoreCacheV1 and dispatch on it in the
  outer catch, mirroring restoreCacheV2 and the write-denied v1 handling
- Add a v1 read-denied test

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves cache restore resilience by detecting policy-driven cache read denials (insufficient read permissions) and surfacing them as a clear core.warning while treating the restore as a cache miss so workflows continue.

Changes:

  • Added read-denial detection via CACHE_READ_DENIED_PREFIX and a dedicated CacheReadDeniedError for targeted handling.
  • Updated both cache restore implementations (v1 REST + v2 Twirp/gRPC) to reclassify read-denied failures into a single warning and continue as a miss.
  • Adjusted v1 getCacheEntry to selectively surface the receiver’s denial message and added tests + release/version bumps.
Show a summary per file
File Description
packages/cache/src/internal/cacheHttpClient.ts Surfaces receiver error message only for cache read denied: so restore paths can reclassify it.
packages/cache/src/cache.ts Adds read-denied prefix + error type and handles read-denied restores as warning + cache miss for both v1/v2.
packages/cache/tests/restoreCache.test.ts Adds v1 tests asserting warning behavior and cache-miss semantics for read denial.
packages/cache/tests/restoreCacheV2.test.ts Adds v2 test asserting warning behavior and cache-miss semantics for wrapped read-denied errors.
packages/cache/tests/cacheHttpClient.test.ts Adds tests ensuring getCacheEntry only surfaces body for read-denied cases.
packages/cache/RELEASES.md Documents the 6.2.0 behavior change.
packages/cache/package.json Bumps @actions/cache version to 6.2.0.
packages/cache/package-lock.json Updates lockfile version metadata to 6.2.0.

Review details

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Files not reviewed (1)
  • packages/cache/package-lock.json: Generated file
  • Files reviewed: 7/8 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread packages/cache/src/cache.ts Outdated
@philip-gai philip-gai changed the title Handle cache read error due to insufficient read permissions feat(cache): add cache-mode client behavior (read-denied warning + ACTIONS_CACHE_MODE skip) Jul 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review details

Files not reviewed (1)
  • packages/cache/package-lock.json: Generated file
  • Files reviewed: 11/12 changed files
  • Comments generated: 1
  • Review effort level: Low

Comment thread packages/cache/src/cache.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@philip-gai
philip-gai marked this pull request as ready for review July 8, 2026 14:58
@philip-gai
philip-gai requested a review from a team as a code owner July 8, 2026 14:58
jasongin
jasongin previously approved these changes Jul 9, 2026
Comment thread packages/cache/src/internal/config.ts Outdated
jasongin
jasongin previously approved these changes Jul 10, 2026
Comment thread packages/cache/__tests__/config.test.ts
Comment thread packages/cache/__tests__/config.test.ts
Comment thread packages/cache/__tests__/restoreCache.test.ts Outdated
Comment thread packages/cache/__tests__/restoreCache.test.ts Outdated
Comment thread packages/cache/__tests__/saveCache.test.ts Outdated
Comment thread packages/cache/src/internal/cacheHttpClient.ts
Comment thread packages/cache/src/cache.ts Outdated
Comment thread packages/cache/src/cache.ts Outdated
Comment thread packages/cache/src/cache.ts
…denied handling

Address PR review feedback:
- Merge the duplicate restore/save skip test.each blocks into single blocks parametrized over ACTIONS_CACHE_SERVICE_V2.
- Drop the redundant CacheReadDeniedError catch arms; the typed error is not an HttpClientError so it already falls through to a non-fatal warning.
- Clarify why read-denied classification happens both in getCacheEntry and cache.ts (dependency-free internal module cannot import the typed error).
Mirror the read-denied simplification on the save path. CacheWriteDeniedError
is not an HttpClientError and its name does not match the ReserveCacheError
arm, so it falls through to the same non-fatal warning. Logging behavior is
unchanged (warns, never fails the run) and the exported type is still thrown
internally for consumers and tests. Also refresh stale doc wording.
The two restoreCache tests exercised the identical warning + cache-miss path
now that read-denied is no longer reclassified in the catch, so merge them into
one. The read-denied prefix detection that actually branches on the message is
covered by getCacheEntry tests in cacheHttpClient.test.ts.

@boxofyellow boxofyellow left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me.

@philip-gai
philip-gai merged commit ffdc20e into main Jul 13, 2026
25 of 27 checks passed
@philip-gai
philip-gai deleted the philip-gai/cache-read-denied-warning branch July 13, 2026 15:03
philip-gai added a commit that referenced this pull request Jul 15, 2026
#2451)

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.


Copilot-Session: e96deec1-716e-4e14-acdf-a230139420a2

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants