10.2 Caching Dependencies with `actions/cache`
Key Takeaways
- Dependency caching via `actions/cache@v4` persists package dependencies, build trees, and compilers across workflow runs to drastically shorten execution duration and compute minute consumption.
- Cache keys must be unique and deterministic, dynamically constructed using operating system and package lockfile hashes (e.g., `${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}`).
- Sequential `restore-keys` fallback matching allows partial prefix hits when an exact key match fails, downloading the most recently saved related cache and reducing fresh download overhead.
- Built-in caching in official setup actions (`actions/setup-node`, `actions/setup-python`, `actions/setup-go`, `actions/setup-java`) automatically resolves paths and lockfiles with a single `cache: <manager>` parameter, eliminating verbose boilerplate.
- GitHub enforces a 10 GB cache storage limit per repository and an automated LRU (Least Recently Used) eviction policy, purging unaccessed caches after 7 days or when total repository usage exceeds 10 GB. Branch cache isolation ensures feature branches can read the default branch's cache but cannot overwrite or access peer branch caches.
Caching Dependencies with actions/cache
Continuous Integration workflows frequently perform redundant, time-consuming tasks across consecutive runs: downloading hundreds of megabytes of third-party package dependencies (such as node_modules, pip wheels, Maven jars, or Go modules), compiling static assets, and pre-warming build caches. Because GitHub-hosted runners run on clean, ephemeral virtual machines for every job, these dependencies are destroyed when the runner terminates unless explicitly saved.
To prevent unnecessary network bandwidth usage, reduce workflow execution duration, and optimize billable runner minutes, GitHub Actions provides a robust caching subsystem powered by actions/cache@v4 and native caching in official setup actions.
1. Mechanics of actions/cache@v4
The actions/cache action operates across two distinct phases within a job's execution lifecycle: the Restore phase (executed when the step runs) and the Save phase (executed automatically as a post-job action upon successful job completion).
+-----------------------------------------------------------------------------+
| ACTIONS/CACHE LIFECYCLE FLOW |
| |
| [JOB EXECUTION: STEP RUNS] |
| 1. Runner queries GitHub Cache Storage for exact match on 'key'. |
| 2. If MATCH found: Cache downloaded & extracted to 'path' -> cache-hit=true|
| 3. If NO MATCH: Runner iterates sequentially through 'restore-keys'. |
| - Partial prefix match found -> downloads latest cache -> cache-hit=false|
| - No prefix match -> cache miss -> job continues fresh. |
| |
| [POST-JOB EXECUTION: JOB COMPLETES SUCCESSFULLY] |
| 4. If exact 'key' match occurred during restore -> SKIP SAVE (No-op). |
| 5. If exact 'key' was NOT matched -> Archive 'path' directory & upload |
| new cache archive bound to 'key'. |
+-----------------------------------------------------------------------------+
Core Configuration Inputs
An actions/cache configuration requires three fundamental parameters:
- name: Cache Node.js Dependencies
id: npm-cache
uses: actions/cache@v4
with:
# 1. Path: Directory or file to cache (can be multi-line for multiple paths)
path: ~/.npm
# 2. Key: Unique, deterministic identifier for this cache snapshot
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
# 3. Restore-Keys: Ordered prefix fallbacks when exact key is not found
restore-keys: |
${{ runner.os }}-npm-
Parameter Breakdown
path: The file path or directory on the runner to archive and restore. Supports absolute paths, relative paths, and glob patterns. Multiple paths can be listed on separate lines.key: A string uniquely identifying the cache. Best practice combines the runner OS (${{ runner.os }}), the ecosystem name, and a cryptographic hash of the lockfile (${{ hashFiles('**/package-lock.json') }}). If the lockfile changes, the hash changes, generating a new cache key.restore-keys: An optional list of prefix strings evaluated sequentially from top to bottom if no exact match is found forkey. The runner searches for any existing cache matching the prefix and restores the most recently created one.
The cache-hit Output
The cache step sets an output variable named cache-hit. Workflows can use this output in conditional step expressions (if:) to skip redundant package installation commands when an exact match occurs:
- name: Install Dependencies
if: steps.npm-cache.outputs.cache-hit != 'true'
run: npm ci
[!WARNING] Partial Hits and
cache-hit: If an exact key match is found,cache-hitevaluates to'true'. However, if a partial prefix match occurs viarestore-keys,cache-hitevaluates to'false'(or is unset). This guarantees that package installation commands still run to fetch only the newly added delta dependencies.
2. Dynamic Cache Key Hashing: hashFiles()
The GitHub Actions expression function hashFiles(pattern) calculates a single SHA-256 hash across all files matching one or more comma-separated glob patterns. This enables automated cache invalidation:
Common hashFiles() Patterns Across Ecosystems
| Ecosystem / Tool | Recommended Cache Path | Cache Key Expression |
|---|---|---|
| Node.js (npm) | ~/.npm | ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }} |
| Node.js (Yarn) | ~/.cache/yarn or .yarn/cache | ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} |
| Python (pip) | ~/.cache/pip | ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt', '**/setup.py') }} |
| Python (Poetry) | ~/.cache/pypoetry | ${{ runner.os }}-poetry-${{ hashFiles('**/poetry.lock') }} |
| Java (Maven) | ~/.m2/repository | ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} |
| Java (Gradle) | ~/.gradle/caches | ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} |
| Go Modules | ~/go/pkg/mod | ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} |
| Rust (Cargo) | ~/.cargo/registry, ~/.cargo/git, target | ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} |
[!TIP] Cache Package Manager Caches, Not
node_modules: In Node.js workflows, it is strongly recommended to cache the global package cache directory (~/.npmor~/.cache/yarn) rather than the localnode_modulesfolder. Caching~/.npmavoids cross-platform native binary compilation issues and allowsnpm cito quickly hydrate dependencies from local disk cache safely.
3. Built-In Caching in Official Setup Actions
To simplify workflow maintenance and eliminate repetitive actions/cache boilerplate, GitHub's official setup actions provide built-in caching support via a single cache: input.
+-----------------------------------------------------------------------------+
| NATIVE SETUP ACTION CACHING ARCHITECTURE |
| |
| uses: actions/setup-node@v4 |
| with: |
| node-version: '20' |
| cache: 'npm' <---- AUTOMATICALLY HANDLES: |
| 1. Resolves path: ~/.npm |
| 2. Hashes: **/package-lock.json |
| 3. Generates key: ${{ runner.os }}-node-${hash} |
| 4. Sets fallback restore-keys: ${{ runner.os }}-node|
+-----------------------------------------------------------------------------+
Native Setup Actions Syntax Comparison
# 1. Node.js (supports 'npm', 'yarn', 'pnpm')
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: 'client/package-lock.json' # Optional for monorepos
# 2. Python (supports 'pip', 'pipenv', 'poetry')
- uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'poetry'
# 3. Go (supports go.sum caching)
- uses: actions/setup-go@v5
with:
go-version: '1.22'
cache: true # Automatically hashes go.sum
# 4. Java (supports 'maven', 'gradle', 'sbt')
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
When using official setup actions with cache:, you do not need to declare a separate actions/cache step unless you require custom paths or specialized key naming strategies.
4. Cache Storage Limits, Eviction Policies & Branch Isolation
Understanding GitHub Actions cache quotas, retention lifecycles, and security boundaries is critical for system architecture and exam success.
Platform Storage Limits & LRU Eviction
- 10 GB Quota per Repository: GitHub allocates a maximum of 10 GB of total cache storage per repository. There is no limit on the number of individual cache archives, provided their combined size remains under 10 GB.
- LRU (Least Recently Used) Eviction: When a new cache upload would exceed the 10 GB repository limit, GitHub Actions automatically deletes existing caches in order of least recently accessed until total storage drops below 10 GB.
- 7-Day Inactivity Expiration: Any cache entry that has not been accessed (read or restored) within 7 days is automatically deleted from GitHub cache storage.
Branch Cache Scope & Isolation Hierarchy
To prevent malicious cache poisoning across branches and pull requests, GitHub Actions enforces strict branch isolation boundaries:
+-----------------------------------------------------------------------------+
| BRANCH CACHE ISOLATION TOPOLOGY |
| |
| +-----------------------+ |
| | DEFAULT BRANCH | |
| | ('main') | |
| | Read/Write Own Cache | |
| +-----------------------+ |
| | |
| Child branches CAN READ from main |
| v |
| +-----------------------------------------------+ |
| | | |
| v v |
| +------------------+ +-------------------+ |
| | FEATURE BRANCH | | FEATURE BRANCH | |
| | ('feat-A') | CANNOT READ PEER CACHE | ('feat-B') | |
| | Read: main + A | < - - - - - - - - - - - > | Read: main + B | |
| | Write: Only A | | Write: Only B | |
| +------------------+ +-------------------+ |
| | |
| v |
| +------------------+ |
| | PULL REQUEST | Read: base branch + default branch |
| | (to 'feat-A') | Write: PR cache (isolated; deleted on PR merge) |
| +------------------+ |
+-----------------------------------------------------------------------------+
Branch Isolation Rules Summary Matrix
| Requesting Execution Scope | Can Read From? | Can Write / Save To? | Can Be Read By? |
|---|---|---|---|
Default Branch (main) | main cache only | main cache | All child branches, PRs, and workflows |
Feature Branch (feat-x) | feat-x cache, then fallback to main cache | feat-x cache only | feat-x workflows and PRs targeting feat-x |
| Pull Request (Internal) | PR branch cache, base branch cache, main cache | PR-specific temporary cache | Isolated to that PR execution scope |
| Pull Request (Fork) | Fork repository cache only (Cannot read upstream base cache) | Fork cache only | Isolated to fork repository |
[!IMPORTANT] Cache Management: Administrators can inspect, list, and manually delete repository caches using the GitHub Web UI (
Actions > Caches), the REST API (DELETE /repos/{owner}/{repo}/actions/caches/{cache_id}), or the GitHub CLI (gh cache listandgh cache delete <key>).
A workflow defines the following caching configuration for a Node.js project:
A developer updates a single dependency in - name: Cache Node Modules
id: npm-cache
uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
package-lock.json on a feature branch. What occurs during the cache restoration and post-job save phases?
Which statement accurately describes GitHub Actions cache storage limits, eviction policies, and branch isolation rules?
A DevOps engineer wants to enable dependency caching for a Python project managed with Poetry using official GitHub setup actions without authoring a verbose custom actions/cache step. Which configuration is correct?