Checkpoint Lab — Dependency Caching, Cache Keys, Restore Strategies, and Performance
The checkpoint turns the chapter into an auditable experiment: measure a controlled cold/warm install, mutate a synthetic lockfile so a precise key changes, deliberately use a stale key and incorrectly skip installation on a cache hit, preserve that failure, then repair the workflow and document exactly what caching can and cannot prove.
Learning objectives
- Run a controlled cold/warm cache experiment with explicit lock, toolchain, runner and key evidence.
- Mutate the synthetic lockfile and prove a precise key changes while a narrow fallback remains safe.
- Create a deliberately stale exact key and preserve the first failure caused by skipping dependency reconciliation.
- Repair the key/consumer contract and independently verify the final dependency state.
- Produce a cache evidence packet and perform exact bounded cleanup after diagnosis.
1. Checkpoint scenario and preflight
Use one disposable GitHub.com repository,
gha-cache-checkpoint. The workflow below creates a
synthetic lockfile from a typed choice, so the lockfile content is
deterministic and its SHA-256 can be compared across runs. The cache
contains only pip's public download/wheel cache. No real secret,
deployment, package publication, self-hosted runner or production
resource is involved.
-
Runner:
ubuntu-24.04; recordRUNNER_OS/RUNNER_ARCH. - Python: 3.13 via pinned setup-python v7.0.0.
- Cache action: pinned v6.1.0; Node 24 runtime.
-
Public packages:
idna==3.10; v2 additionally requestspackaging==25.0. - Top-level token permissions:
permissions: {}. - External dependency: PyPI availability. If unavailable, use the local simulation in section 8.
2. Predict the state changes before running
| Prediction | Expected state change | Independent verification |
|---|---|---|
| P1 — precise v1 first run | primary key absent → miss → successful install → cache save eligible | run log + post-cache save log + cache metadata key/ref/version |
| P2 — precise v1 repeat | same key can restore exact entry |
cache-hit=true + same lock hash/toolchain
dimensions
|
| P3 — precise v2 | lock hash changes → primary key changes; narrow v1 prefix may restore |
lock SHA differs + cache-hit=false if fallback
restored
|
| P4 — stale v1 then stale v2 | static key does not change even though lock SHA changes | same expanded key + different lock SHA |
| P5 — stale v2 with skip-on-hit | exact cache hit can skip install, leaving fresh target environment empty | install step skipped + dependency verification failure preserved |
| P6 — repaired precise v2 | dependency reconciliation runs and verifies current lock regardless of restored cache | successful verify + key/hit/lock evidence |
3. Exact checkpoint workflow
The key_mode input lets the same safe workflow
demonstrate a correct precise key or a deliberately stale static
key. The skip_install_on_exact_hit input exists only to
engineer the failure from Lesson 4. It must be false in
the repaired production pattern.
name: cache-correctness-checkpoint
on:
workflow_dispatch:
inputs:
lock_variant:
description: Synthetic dependency input
required: true
type: choice
options: [v1, v2]
default: v1
key_mode:
description: precise is correct; stale is intentionally broken
required: true
type: choice
options: [precise, stale]
default: precise
skip_install_on_exact_hit:
description: Intentionally unsafe teaching switch
required: true
type: boolean
default: false
permissions: {}
jobs:
dependency-proof:
runs-on: ubuntu-24.04
steps:
- name: Pin Python 3.13
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: '3.13'
- id: lock
name: Materialize and hash dependency input
shell: bash
env:
LOCK_VARIANT: ${{ inputs.lock_variant }}
run: |
if [[ "$LOCK_VARIANT" == 'v1' ]]; then
printf 'idna==3.10
' > requirements.lock
else
printf 'idna==3.10
packaging==25.0
' > requirements.lock
fi
sha="$(sha256sum requirements.lock | awk '{print $1}')"
echo "sha256=$sha" >> "$GITHUB_OUTPUT"
cat requirements.lock
- id: runtime
name: Inspect package-manager and toolchain state
shell: bash
run: |
cache_dir="$(python -m pip cache dir)"
py_minor="$(python -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
echo "cache_dir=$cache_dir" >> "$GITHUB_OUTPUT"
echo "py_minor=$py_minor" >> "$GITHUB_OUTPUT"
python --version
python -m pip --version
- id: key
name: Derive primary cache key
shell: bash
env:
KEY_MODE: ${{ inputs.key_mode }}
LOCK_SHA: ${{ steps.lock.outputs.sha256 }}
PY_MINOR: ${{ steps.runtime.outputs.py_minor }}
run: |
prefix="pip-$RUNNER_OS-$RUNNER_ARCH-py$PY_MINOR"
if [[ "$KEY_MODE" == 'precise' ]]; then
primary="$prefix-$LOCK_SHA"
else
primary="$prefix-static"
fi
echo "prefix=$prefix" >> "$GITHUB_OUTPUT"
echo "primary=$primary" >> "$GITHUB_OUTPUT"
printf 'primary_key=%s
' "$primary"
- id: cache
name: Restore dependency download cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ${{ steps.runtime.outputs.cache_dir }}
key: ${{ steps.key.outputs.primary }}
restore-keys: |
${{ steps.key.outputs.prefix }}-
- id: install
name: Reconcile current lockfile
if: ${{ !inputs.skip_install_on_exact_hit || steps.cache.outputs.cache-hit != 'true' }}
shell: bash
run: |
rm -rf .lab-site
start="$(python -c 'import time; print(time.time_ns())')"
python -m pip install --disable-pip-version-check --no-input --target .lab-site -r requirements.lock
end="$(python -c 'import time; print(time.time_ns())')"
ms="$(( (end - start) / 1000000 ))"
echo "install_ms=$ms" >> "$GITHUB_OUTPUT"
printf 'install_ms=%s
' "$ms"
- name: Verify installed state against current lock input
shell: bash
env:
LOCK_VARIANT: ${{ inputs.lock_variant }}
run: |
test -d .lab-site
PYTHONPATH="$PWD/.lab-site" python -S - <<'PY'
import os
import idna
print('idna', idna.__version__)
if os.environ['LOCK_VARIANT'] == 'v2':
import packaging
print('packaging', packaging.__version__)
PY
- name: Write evidence summary
if: ${{ always() }}
shell: bash
env:
LOCK_SHA: ${{ steps.lock.outputs.sha256 }}
PRIMARY_KEY: ${{ steps.key.outputs.primary }}
CACHE_HIT: ${{ steps.cache.outputs.cache-hit }}
INSTALL_MS: ${{ steps.install.outputs.install_ms }}
INSTALL_OUTCOME: ${{ steps.install.outcome }}
run: |
{
echo '### Cache correctness checkpoint'
echo "- run: $GITHUB_RUN_ID attempt $GITHUB_RUN_ATTEMPT"
echo "- source SHA: $GITHUB_SHA"
echo "- runner: $RUNNER_OS / $RUNNER_ARCH"
echo "- lock SHA-256: $LOCK_SHA"
echo "- primary key: $PRIMARY_KEY"
echo "- cache-hit: ${CACHE_HIT:-<empty>}"
echo "- install outcome: $INSTALL_OUTCOME"
echo "- install ms: ${INSTALL_MS:-skipped-or-unavailable}"
} >> "$GITHUB_STEP_SUMMARY"
4. Execute six controlled runs
| Run | Inputs | Expected interpretation |
|---|---|---|
| A | v1 / precise / false | cold or current-state miss; install runs; successful job can save precise v1 key |
| B | v1 / precise / false | exact v1 hit is possible; install still runs; compare duration without asserting speedup |
| C | v2 / precise / false |
lock SHA and primary key change; v1 may restore as prefix
fallback (false), then packaging is reconciled
and v2 key can be saved
|
| D | v1 / stale / false | creates or restores the intentionally static key; install runs |
| E | v2 / stale / true | same stale key can be an exact hit although lock SHA changed; install is skipped and verification should fail on the fresh runner |
| F | v2 / precise / false | repair: lock-derived key + dependency reconciliation; verify succeeds; preserve E as first-failure evidence |
If D happens to restore a prior static entry created during
experimentation, delete only that exact disposable cache before
restarting the D/E pair, or change the static suffix in the lab
workflow to a new bounded teaching epoch such as
static-demo2. Record the change. Do not delete
unrelated repository caches.
5. Interpret the engineered failure without hiding it
Run E is designed to fail for a causal reason: the stale key remains
identical while the lock SHA changes, and the unsafe switch tells
the workflow to skip installation after an exact hit. The restored
pip cache contains reusable downloads, not .lab-site.
Therefore verification fails on the fresh runner.
The repair is not “clear the cache and rerun.” The repair is to restore the invariant: precise keys encode correctness inputs and the package manager reconciles the current dependency contract even when the download cache hits.
6. Inspect cache-service evidence after the runs
GH_REPO='OWNER/gha-cache-checkpoint'
gh api -H 'X-GitHub-Api-Version: 2026-03-10' "repos/$GH_REPO/actions/caches?per_page=100" --jq '.actions_caches[] | {id,ref,key,version,size_in_bytes,created_at,last_accessed_at}'
Match each cache entry to the run's expanded key and ref. Record the exact static-key entry that participated in Run E before cleanup.
7. Required evidence packet
| Evidence | Record |
|---|---|
| Workflow manifest |
full action SHAs, ubuntu-24.04, Python 3.13 and
token permissions
|
| Run identities | A–F run IDs/attempts and source SHA |
| Dependency identities | lock variant and SHA-256 for each run |
| Cache identities |
expanded primary key, restore prefix,
cache-hit, cache ID/ref/version/size where
available
|
| Toolchain | Python/pip version; runner OS/arch; current cache action/runtime assumption |
| Performance | measured install ms for comparable successful runs; note network/cache-transfer variability |
| First failure | Run E skipped install + dependency verification failure; do not replace it with Run F |
| Repair | Run F precise key and successful lockfile reconciliation |
| Security boundary | no secrets cached; manual trusted trigger; restored cache treated as untrusted input |
| Limitations | tiny public PyPI example; no claim that warm always means faster; cache not release evidence |
8. Free/local faithful simulation
If GitHub Actions or PyPI is unavailable, create two local
directories: .cache-store/ and a fresh
.run-env/. Derive a key from uname, Python
major/minor and sha256sum requirements.lock. Store a
harmless synthetic “download” file under the key directory. On the
next run, restore it into a fresh workspace, but still execute a
local install/reconciliation script that creates
.run-env/installed.txt from the current lockfile. Then
deliberately use a static key and skip reconciliation to reproduce
the same stale-hit failure.
This simulation proves keying and consumer semantics, but it does not validate GitHub cache branch scope, service-side cache version, retention/eviction, cache tokens or network transfer performance. Record those limitations.
9. Exact bounded cleanup
After preserving evidence, list cache IDs and delete only the disposable entries created by this lab. In the repository UI, use Actions → Caches and verify the key. From an authenticated workstation, an exact-ID REST deletion is also possible:
GH_REPO='OWNER/gha-cache-checkpoint'
CACHE_ID='123456789' # copy from read-only list; verify key/ref first
gh api -X DELETE -H 'X-GitHub-Api-Version: 2026-03-10' "repos/$GH_REPO/actions/caches/$CACHE_ID"
Finally delete the throwaway repository if desired. No cloud, package registry, runner, environment or deployment rollback remains.
10. What Chapter 14 adds to the operating model
You can now optimize dependency work without making cache state authoritative: keys encode correctness inputs, fallback is explicitly stale, cache metadata is inspectable, hits are measured rather than worshiped, sensitive or poisoned entries are treated as security state, and workflows remain correct after eviction. Chapter 15 moves to Service Containers, Job Containers, Docker Builds, and Integration-Test Environments, where filesystem/network isolation and container lifecycle become first-class execution inputs.
Knowledge check
What is the causal bug in Run E?
The static key remains an exact match after the dependency input changes, and the workflow incorrectly treats that exact cache hit as permission to skip installation on a fresh runner.
Why is Run F a better repair than deleting all caches?
It repairs the key and consumer contract while preserving Run E and its cache metadata as first-failure evidence. Deleting everything would hide the cause and create unrelated cold starts.
What two identities must be compared when the lockfile changes?
The lockfile SHA-256 and the expanded primary cache key; a precise design should change the key when the correctness-relevant lock input changes.
Does cache-hit=true prove the current dependencies
are installed?
No. It only proves an exact cache record matched. The cached path and consumer behavior determine whether current dependencies are actually present and valid.
What does Chapter 15 add after this cache chapter?
Containerized execution and service-network lifecycle: job/service containers, Docker builds and integration-test environments rather than cross-run cache reuse.
Official references and version notes
- GitHub Docs — Dependency caching — cache purpose, artifact distinction and cache-security boundary.
- GitHub Docs — Dependency caching reference — key matching, restore keys, branch scope, versioning, limits and eviction.
- GitHub Docs — Managing caches — read-only inspection and exact cache deletion.
-
actions/cache v6.1.0
— explicit cache action pinned to
55cc8345863c7cc4c66a329aec7e433d2d1c52a9. -
actions/setup-python v7.0.0
— Python setup action pinned to
5fda3b95a4ea91299a34e894583c3862153e4b97. -
setup-python dependency caching
— built-in pip/pipenv/Poetry caching and
cache-dependency-path.
Version-sensitive behavior was rechecked on
2026-09-09. Mandatory examples target GitHub.com
and ubuntu-24.04. actions/cache v6.1.0
is pinned by full commit SHA and runs on Node 24; keep self-hosted
runners current enough to execute Node 24 actions (GitHub
documents runner 2.327.1 as the Node 24 minimum in current
official action guidance). Cache entries are scoped by key, cache
version and ref/branch visibility. Exact primary-key matches
report cache-hit == 'true'; prefix/restore-key
matches report 'false'; a total miss yields an empty
value. The cache action's post-save runs on successful jobs; do
not rely on deprecated save-always. GitHub currently
defaults cache inactivity retention to 7 days and repository cache
storage to 10 GB; eligible repositories/organizations can
configure higher limits, so performance/storage assumptions must
be recorded rather than hard-coded. Current repository cache
settings can exceed the historical 10 GB/7-day defaults on
eligible plans; the lab requires no paid cache expansion and
should remain tiny.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this
address.