|
| 1 | +id: ca-156 |
| 2 | +title: 'actions/cache save silently fails with misleading ''Unable to reserve cache with key'' message when token has read-only cache access' |
| 3 | +category: caching-artifacts |
| 4 | +severity: silent-failure |
| 5 | +tags: |
| 6 | + - actions-cache |
| 7 | + - read-only |
| 8 | + - pull_request_target |
| 9 | + - fork |
| 10 | + - cache-write-denied |
| 11 | + - misleading-error |
| 12 | + - permissions |
| 13 | +patterns: |
| 14 | + - regex: 'Unable to reserve cache with key.*another job may be creating this cache' |
| 15 | + flags: 'i' |
| 16 | + - regex: 'cache write denied|cache.*read.only.*token|read.only.*cache.*token' |
| 17 | + flags: 'i' |
| 18 | + - regex: 'Warning: Unable to reserve cache with key' |
| 19 | + flags: 'i' |
| 20 | +error_messages: |
| 21 | + - 'Warning: Unable to reserve cache with key cache-key-123, another job may be creating this cache.' |
| 22 | + - 'Warning: cache write denied' |
| 23 | +root_cause: | |
| 24 | + `actions/cache` silently swallows the error when the GITHUB_TOKEN does not have write access to |
| 25 | + the Actions cache service. Instead of surfacing a meaningful error, the action logs a misleading |
| 26 | + warning: "Unable to reserve cache with key X, another job may be creating this cache." — implying |
| 27 | + a concurrency race when the real cause is a permission denial. |
| 28 | +
|
| 29 | + This silent failure occurs in two scenarios: |
| 30 | +
|
| 31 | + 1. **Fork pull_request workflows** — the GITHUB_TOKEN for PRs from forks is scoped read-only by |
| 32 | + default. `actions/cache` save steps complete without error but the cache is never written. |
| 33 | + Subsequent runs pay full restore cost since no cache was saved. |
| 34 | +
|
| 35 | + 2. **pull_request_target workflows — UPCOMING** — GitHub is extending read-only cache access to |
| 36 | + `pull_request_target` workflows (actions/cache#1768, June 2026). These workflows receive a |
| 37 | + write-scoped GITHUB_TOKEN for repository operations, but the cache layer will enforce a |
| 38 | + separate read-only cache token for security. Existing `pull_request_target` workflows that |
| 39 | + rely on cache saves will silently stop caching without any obvious error. |
| 40 | +
|
| 41 | + Prior to `actions/cache@v5.1.0`, the "Unable to reserve cache with key" message was the only |
| 42 | + signal — developers typically assume it means a race condition and add retries or `fail-on-cache-miss`, |
| 43 | + which does not help. Version 5.1.0 surfaces the correct "cache write denied" message to make the |
| 44 | + permission cause visible. |
| 45 | +fix: | |
| 46 | + 1. **Upgrade to actions/cache@v5.1.0+** — this version emits a "cache write denied" warning |
| 47 | + instead of the misleading "another job may be creating this cache" message, making the root |
| 48 | + cause visible in the workflow log. |
| 49 | +
|
| 50 | + 2. **For pull_request workflows from forks** — cache restores (reads) still work; only saves |
| 51 | + (writes) are denied. Use a two-workflow split: a `pull_request` workflow for tests that |
| 52 | + restores cache and uploads artifacts, and a `workflow_run` or `push` workflow that writes |
| 53 | + cache from the base branch. |
| 54 | +
|
| 55 | + 3. **For pull_request_target** — if cache saves are required, use a PAT or GitHub App token |
| 56 | + scoped to cache write, or restructure so caches are populated from a `push` or `schedule` |
| 57 | + workflow on the base branch and only restored in `pull_request_target`. |
| 58 | +
|
| 59 | + 4. **Do NOT diagnose as concurrency** — "Unable to reserve cache" is almost never a race between |
| 60 | + jobs in the same run. If you see it consistently on every run for the same key, suspect |
| 61 | + read-only token before investigating concurrency. |
| 62 | +fix_code: |
| 63 | + - language: yaml |
| 64 | + label: 'Upgrade actions/cache to v5.1.0+ for accurate cache-write-denied error message' |
| 65 | + code: | |
| 66 | + jobs: |
| 67 | + build: |
| 68 | + runs-on: ubuntu-latest |
| 69 | + steps: |
| 70 | + - uses: actions/checkout@v4 |
| 71 | +
|
| 72 | + - name: Cache dependencies |
| 73 | + uses: actions/cache@v5 # v5.1.0+ emits "cache write denied" instead of misleading message |
| 74 | + with: |
| 75 | + path: ~/.npm |
| 76 | + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} |
| 77 | + restore-keys: | |
| 78 | + ${{ runner.os }}-node- |
| 79 | + - language: yaml |
| 80 | + label: 'Two-workflow split — restore cache in pull_request, populate from push' |
| 81 | + code: | |
| 82 | + # Workflow 1: pr-ci.yml — runs on pull_request (cache restores work, saves are denied) |
| 83 | + on: pull_request |
| 84 | +
|
| 85 | + jobs: |
| 86 | + test: |
| 87 | + runs-on: ubuntu-latest |
| 88 | + steps: |
| 89 | + - uses: actions/checkout@v4 |
| 90 | +
|
| 91 | + - name: Restore cache (read-only in fork PRs — saves will be denied silently) |
| 92 | + uses: actions/cache/restore@v5 |
| 93 | + with: |
| 94 | + path: ~/.npm |
| 95 | + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} |
| 96 | + restore-keys: ${{ runner.os }}-node- |
| 97 | +
|
| 98 | + - name: Run tests |
| 99 | + run: npm test |
| 100 | +
|
| 101 | + --- |
| 102 | + # Workflow 2: cache-prime.yml — runs on push to default branch (has cache write access) |
| 103 | + on: |
| 104 | + push: |
| 105 | + branches: [main] |
| 106 | +
|
| 107 | + jobs: |
| 108 | + prime-cache: |
| 109 | + runs-on: ubuntu-latest |
| 110 | + steps: |
| 111 | + - uses: actions/checkout@v4 |
| 112 | + - name: Populate cache from base branch |
| 113 | + uses: actions/cache@v5 |
| 114 | + with: |
| 115 | + path: ~/.npm |
| 116 | + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} |
| 117 | + - run: npm ci |
| 118 | +prevention: |
| 119 | + - 'Treat a recurring "Unable to reserve cache with key" warning on every run for the same key as a permission issue, not a concurrency race.' |
| 120 | + - 'Use actions/cache@v5.1.0+ which surfaces "cache write denied" for clear diagnosis.' |
| 121 | + - 'Know that pull_request workflows from forks have read-only cache access — cache restores work, saves do not.' |
| 122 | + - 'Expect pull_request_target workflows to also receive read-only cache tokens in a future platform update (June 2026+); populate caches from push/schedule workflows instead.' |
| 123 | + - 'Use actions/cache/restore@v5 explicitly in read-only contexts to make the intent clear and avoid confusing save failures.' |
| 124 | +docs: |
| 125 | + - url: 'https://github.com/actions/cache/pull/1768' |
| 126 | + label: 'actions/cache PR #1768: Bump @actions/cache to v5.1.0 — handle read-only cache access (June 18, 2026)' |
| 127 | + - url: 'https://github.com/actions/toolkit/pull/2435' |
| 128 | + label: 'actions/toolkit PR #2435: Handle cache write error due to read-only token' |
| 129 | + - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request' |
| 130 | + label: 'GitHub Docs: pull_request event — fork token restrictions' |
| 131 | + - url: 'https://docs.github.com/en/actions/writing-workflows/choosing-when-your-workflow-runs/events-that-trigger-workflows#pull_request_target' |
| 132 | + label: 'GitHub Docs: pull_request_target event' |
0 commit comments