Git, CI/CD & GitOpsXLVI · CachingFoundations
What a cache is — a content-addressed, key-matched, best-effort blob store
What you'll learn
- Define a cache as a key-addressed, content-derived, best-effort blob store
- Identify the role of `hashFiles()` in deriving a cache key from a lockfile
- Distinguish exact-match keys from prefix-match fallback keys
- Recognise the scope of a cache (repository, branch, workflow) and why the scope matters
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A cache is a key-addressed blob store the runner reads and writes to skip redundant work. The same bytes the runner would otherwise rebuild - language dependencies, compiled objects, downloaded tooling - are uploaded once, addressed by a hash of the inputs that produced them, and restored on the next run when the same inputs reappear. The cache is not durable storage; it is a performance optimisation that survives only as long as the eviction policy allows.
The cache has three properties
A cache, as exposed by actions/cache@v4, has three properties
that distinguish it from every other data mechanism in a CI job:
- Content-addressed. The key is a hash of the inputs the cache
depends on. A cache keyed by
hashFiles('requirements.txt')changes wheneverrequirements.txtchanges. Two identical inputs produce the same key, no matter who runs the workflow. - Best-effort. A cache miss is not an error. If the lookup fails - the key has no entry, the cache was evicted, the store is unavailable - the workflow continues and does the work. Coupling correctness to a cache is a misconfiguration.
- Scoped. The cache is addressed by
repo + branch + workflow + key. A cache entry created onmainis not visible from a feature branch with the same key. Two workflows in the same repository cannot collide on the same key.
flowchart LR
A["requirements.txt"] --> B["hashFiles()"]
B --> C["key: pip-<sha256>"]
C --> D{"Cache lookup"}
D -->|hit| E["Restore path"]
D -->|miss| F["Run install"]
F --> G["Save post-job"]
G --> H["Cache entry created"]
The diagram shows the round trip: the key is derived from the
inputs, the runner looks the key up, a hit restores the path, a
miss runs the work and uploads the result at job end. The upload
at job end is implicit - actions/cache@v4 registers a
post-job step that uploads when the job exits successfully.
Why the key is a hash
The cache key is a hash rather than a fixed string because the
cache must invalidate itself when the inputs change. A cache
keyed by the literal string pip would never invalidate: every
run would restore the same payload, even if requirements.txt
had changed underneath it. A cache keyed by the SHA of
requirements.txt changes the moment any byte of
requirements.txt changes, the lookup misses, and the runner
rebuilds.
- name: Cache pip
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: pip-${ hashFiles('requirements.txt') }
The expression hashFiles('requirements.txt') evaluates to a
SHA-256 of the file’s bytes. The pip- prefix is a namespace
that lets multiple caches coexist in the same repository
(pip-..., npm-..., go-...). The prefix has no semantic
meaning to the cache; it is purely a namespacing convention to
prevent collisions between different language ecosystems in the
same workflow.
Scope: where the cache is visible
A cache is scoped to repository + branch + workflow + key. The
scope has three operational consequences:
- Branch isolation. A cache created on
mainis not visible from a feature branch. A pull request fromfeature-xcannot read or overwritemain’s cache for the same key. This isolation prevents cross-branch contamination but also means a feature branch has to rebuild from scratch on its first run. - Workflow isolation. A cache key created by the
buildworkflow is not visible from thelintworkflow, even on the same branch, unless the key collides. Different workflows can safely use the same prefix without interfering. - Cross-PR isolation. GitHub enforces additional scope at the pull-request level: a PR from a fork cannot read caches created by the upstream repository. This isolation is a security control - it prevents a malicious fork from restoring a cache payload it did not create.
What goes into a cache
Caches are appropriate for work that is expensive to repeat and safe to skip:
- Language dependency caches:
pip,npm,yarn,maven,gradle,go modules,composer,bundle,cargo. - Build caches: TypeScript
tscincremental builds, Bazel remote-cache mirrors, webpack persistent caches. - Tool downloads: large CLIs the workflow installs on every run.
- Docker layer caches on self-hosted runners (covered in Part XL).
Caches are inappropriate for anything that must be present for the
job to succeed (a terraform plan file must be an artifact, not a
cache), anything auditable (SBOMs, signed plans must be
artifacts), and anything sensitive (neither caches nor artifacts
are secrets managers).
Production discipline
- Always key a cache by the lockfile hash. A cache keyed by a fixed string never invalidates and silently serves stale bytes after the lockfile has changed.
- Treat a cache miss as expected. The first run on a new branch, the first run after a long-eviction window, and the first run after the cache store is unavailable are all misses. The workflow must succeed in all three.
- Scope the key prefix deliberately. A prefix like
pip-${ hashFiles('requirements.txt') }lets multiple ecosystems share a repository’s cache namespace without collision. - Never put secrets in a cache path. A cache is readable by anyone with workflow read permission; credentials belong in the secrets manager.
Cross-course references
- Linux for Production Sysadmins - Part XXXIV (ConfigMgmt)
discusses the package cache (
/var/cache/apt,/var/cache/dnf) that is the operating-system analogue of the CI cache. - Ansible for Production Sysadmins - Part XXXVII (RepoArch) applies the same content-addressing principle to molecule dependency caches.
- Terraform for Production Sysadmins - Parts IX-XII (State) distinguish the Terraform provider plugin cache (a cache) from Terraform state (an artifact with retention).
Quiz
Knowledge check · 4 questions
Q1. A workflow keys its pip cache with the literal string `pip` rather than `hashFiles('requirements.txt')`. What failure mode follows?
Q2. A cache miss in `actions/cache@v4` causes the workflow step to fail by default.
Q3. Name the three properties of a cache exposed by `actions/cache@v4` and explain why content-addressing matters.
Q4. Diagnose why a pipeline restores stale dependencies despite using `actions/cache@v4` and recommend the fix.
Team A's `pip install` step restores a cache entry on every run. The dependencies are six months old; a CVE was disclosed in the lockfile eight months ago and the lockfile was updated to a patched version. The CI is green, but production services are running vulnerable package versions. The team assumed the cache was correct because the step succeeded.
Passing score: 75%. Answers are checked in this browser.