Chapter 14Lesson 05~240 minutes

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.

Checkpoint labPrecise keyStale keyPerformance evidenceCleanup

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; record RUNNER_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 requests packaging==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?

Why is Run F a better repair than deleting all caches?

What two identities must be compared when the lockfile changes?

Does cache-hit=true prove the current dependencies are installed?

What does Chapter 15 add after this cache chapter?

Next chapter concept

Make integration environments explicit

Chapter 15 applies the same state-and-evidence discipline to service containers, job containers and Docker-based integration tests.

Official references and version notes

Version and compatibility note

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.