# Dependency Caching — GitHub Actions

Source: https://www.skillbyai.com/en/github-actions/s-cache

> Keys, restore keys and lockfiles.

## Exact keys and fallbacks

Caching restores dependency directories between runs. A cache **key** usually combines the OS and a hash of the lockfile (`hashFiles('**/package-lock.json')`), so the key changes exactly when dependencies change. On a miss, **restore-keys** prefixes allow a partial hit from an older cache, after which the package manager installs only the difference. Many setup actions (setup-node, setup-python, setup-java) have a built-in `cache:` option. Caches are scoped by branch, with access to the default branch's caches, and old entries are evicted when the repository's cache limit is reached.

## Less waiting, fewer minutes

Caching, concurrency control and timeouts cut cost and feedback time.

![Three ideas: caching, concurrency, timeouts and job sizing.](assets/figures/github-actions/section-5-map.svg) — Figure 5.1 — Caching, concurrency and timeouts.

## Exact hits, partial hits and misses, run

I ran this with Python 3. It is a simplified model of GitHub Actions behaviour for learning, not GitHub's implementation. The same lockfile restores the cache exactly; a changed lockfile misses the exact key but restores the older Linux cache through the restore-key prefix; a macOS job has no matching cache and installs everything.

```python
import hashlib
def hash_files(*contents):
    h = hashlib.sha256()
    for c in contents: h.update(c.encode())
    return h.hexdigest()[:12]
lock_v1 = '{"lodash": "4.17.21"}'
lock_v2 = '{"lodash": "4.17.21", "zod": "3.23.8"}'
saved = {f"npm-Linux-{hash_files(lock_v1)}": "node_modules@v1"}     # caches already stored
def restore(key, restore_keys):
    if key in saved: return f"exact hit: {key}"
    for prefix in restore_keys:                      # newest matching prefix wins
        hits = [k for k in saved if k.startswith(prefix)]
        if hits: return f"partial hit via '{prefix}': {hits[-1]} (then npm installs the difference)"
    return "miss: full install"
print(restore(f"npm-Linux-{hash_files(lock_v1)}", ["npm-Linux-"]))
print(restore(f"npm-Linux-{hash_files(lock_v2)}", ["npm-Linux-"]))
print(restore(f"npm-macOS-{hash_files(lock_v2)}", ["npm-macOS-"]))
```

Output:

```
exact hit: npm-Linux-bb5d4b6bb85e
partial hit via 'npm-Linux-': npm-Linux-bb5d4b6bb85e (then npm installs the difference)
miss: full install
```

## Cache dependencies, not build outputs of untrusted code

Cache poisoning is possible if untrusted workflows can write caches used by trusted ones; keep sensitive release builds cache-free or isolated.

**Quiz:** What should a dependency cache key usually include?

- [ ] The current time
- [x] The OS and a hash of the lockfile
- [ ] A random number
- [ ] The workflow file name only

*Answer:* The OS and a hash of the lockfile. Change the key exactly when dependencies change.
