🎉 DevOps Interview Prep Bundle is live — 1000+ Q&A across 20 topicsGet it →
All Fixes
Today I Fixed

GitHub Actions cache save was skipped because cache-mode was read-only

GitHub ActionsSep 24, 202615 minutes to fixgithub-actionscicdcachetroubleshooting

Problem

A GitHub Actions workflow restored an existing cache but did not save a new one. The cache step did not fail the job; it logged that the save was skipped or unavailable under the effective cache access mode.

The key and path looked correct, and the same workflow had previously populated caches.

Diagnosis

GitHub Actions now exposes an effective cache access mode through ACTIONS_CACHE_MODE. I added a temporary diagnostic step:

yaml
- name: Show effective cache access
  run: echo "Cache mode is $ACTIONS_CACHE_MODE"

The affected run reported:

text
Cache mode is read

The workflow used a low-trust trigger. GitHub's secure default gives those runs read-only cache access, so they may restore an existing cache but may not save or replace one.

Root Cause

The cache operation was behaving as designed. GitHub Actions supports four cache-mode values:

ModeRestoreSave
readYesNo
writeYesYes
write-onlyNoYes
noneNoNo

Trusted triggers such as a normal push default to write. Low-trust triggers such as pull_request_target, issue_comment, and workflow_run default to read when cache-mode is omitted.

When a mode disallows a cache operation, actions/cache logs the result and continues. A blocked restore becomes a cache miss; a blocked save is skipped. That is why the job stayed green.

Safe Fix

I kept the low-trust workflow read-only and moved cache population to a trusted workflow triggered by a push to the default branch:

yaml
name: Build trusted cache
 
on:
  push:
    branches: [main]
 
cache-mode: write
 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
 
      - uses: actions/cache@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}
 
      - run: npm ci

The low-trust workflow explicitly documents its intended access:

yaml
name: Validate external event
 
on:
  pull_request_target:
 
cache-mode: read
 
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/cache/restore@v4
        with:
          path: ~/.npm
          key: npm-${{ runner.os }}-${{ hashFiles('**/package-lock.json') }}

This removes the misleading save attempt and preserves the protection against cache poisoning.

Why I Did Not Set write on the Untrusted Trigger

An explicit cache-mode: write or write-only overrides the secure default. That may make the warning disappear, but it can allow untrusted code to place files in a cache later restored by a privileged workflow.

GitHub warns that cache contents are not signed or verified. A poisoned cache can modify files that a later job executes. A low-trust workflow should only receive write access after a security review proves that it does not process attacker-controlled code or data before saving.

For ordinary fork validation, keep cache access read-only and let a trusted push workflow maintain the cache.

Reusable Workflow Check

cache-mode propagates from a caller to a reusable workflow. A called workflow cannot request access beyond an explicit limit from its caller.

For example, this caller caps access at read:

yaml
jobs:
  test:
    cache-mode: read
    uses: acme/shared-workflows/.github/workflows/test.yml@v3

If the called workflow declares write, the run fails validation before jobs start. The fix is to reduce the called workflow's requested access or move cache writing into a separately trusted workflow. Do not widen the caller simply to satisfy validation.

Verification

After the change, I verified both paths:

  1. A push run reported ACTIONS_CACHE_MODE=write and saved a new cache.
  2. The low-trust run reported ACTIONS_CACHE_MODE=read and restored the trusted cache.
  3. The low-trust workflow no longer attempted a save.
  4. A changed lock file produced a new key during the next trusted run.

If the mode is correct but the workflow still misses the cache, check the key, path, branch scope, and retention behavior in the broader GitHub Actions cache troubleshooting guide.

Lesson

A green cache step does not mean both restore and save were allowed. Check ACTIONS_CACHE_MODE first. For low-trust triggers, a skipped save is normally a security control—not a cache-key bug.

Sources

Did this fix work?

Tell us what needs improving. No account required.