iwantcoding.com
🔥 Daily 👥 Rooms 🏆 Top Log in Sign up

Caching

CI cache speeds up pipelines by re-using artifacts (deps, build output, Docker layers) between runs. The hard part is keying: same key = cache hit; key changes too often = always miss.

Per-platform caches + Docker layer cache

EXAMPLE
# 1) The core idea
# Pipelines repeat the same work (npm install, gradle build, pip install).
# Cache them — keyed by file hashes (package-lock.json, pom.xml, etc.).
# Cache miss → install fresh + save. Cache hit → restore + skip install.

# === GitHub Actions ===

# 2) Built-in setup-* caches
- uses: actions/setup-node@v4
  with:
      node-version: 20
      cache: 'npm'                # auto-caches ~/.npm keyed by package-lock.json

- uses: actions/setup-python@v5
  with:
      python-version: '3.12'
      cache: 'pip'                # caches ~/.cache/pip

- uses: actions/setup-java@v4
  with:
      distribution: temurin
      java-version: 21
      cache: gradle               # caches ~/.gradle

- uses: actions/setup-go@v5
  with:
      go-version: '1.22'
      cache: true                 # caches ~/go/pkg/mod

# 3) Manual cache — actions/cache@v4
- uses: actions/cache@v4
  with:
      path: |
          ~/.cache/pip
          ${{ github.workspace }}/.venv
      key:          ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
      restore-keys: |
          ${{ runner.os }}-pip-

# 4) Multi-path cache — Cypress / Playwright binaries + node_modules
- uses: actions/cache@v4
  with:
      path: |
          ~/.cache/Cypress
          node_modules
      key:          ${{ runner.os }}-cypress-${{ hashFiles('**/package-lock.json') }}
      restore-keys: ${{ runner.os }}-cypress-

# === GitLab CI ===

# 5) GitLab cache — global + per-job
cache:
    key: { files: [package-lock.json] }
    paths: [.npm/, node_modules/]

build:
    script:
        - npm ci --prefer-offline --cache .npm
        - npm run build

# Per-environment cache
build-prod:
    cache:
        key: 'prod-${CI_COMMIT_REF_SLUG}'
        paths: [dist/, .npm/]
        policy: pull-push    # pull-only on read-only jobs

# === Docker layer cache ===

# 6) BuildKit + GitHub Actions cache (GHA backend)
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v6
  with:
      context:   .
      push:      true
      tags:      ghcr.io/me/myapp:${{ github.sha }}
      cache-from: type=gha
      cache-to:   type=gha,mode=max

# 7) BuildKit + Registry-based cache (works on any CI)
docker buildx build \
    --push \
    -t ghcr.io/me/myapp:${TAG} \
    --cache-from type=registry,ref=ghcr.io/me/myapp:buildcache \
    --cache-to   type=registry,ref=ghcr.io/me/myapp:buildcache,mode=max \
    .

# 8) Inline cache (simpler, baked into the image)
docker buildx build \
    --push \
    -t ghcr.io/me/myapp:${TAG} \
    --cache-to type=inline \
    --cache-from ghcr.io/me/myapp:latest \
    .

# === Cache key strategy ===

# 9) Good keys
# - hashFiles('package-lock.json')           → invalidates on lockfile change
# - hashFiles('Cargo.lock')
# - hashFiles('go.sum')
# - hashFiles('poetry.lock', 'pyproject.toml')

# 10) Avoid these as keys
#   • Source file hashes — invalidate every commit
#   • Date — invalidates every day, even when nothing changed
#   • Branch name only — caches won't share between feature branches

# 11) Restore-keys — fall back to a slightly older cache
key:           os-pip-${{ hashFiles('requirements.txt') }}
restore-keys: |
    os-pip-
# If exact match miss, restore newest cache with prefix 'os-pip-' and run a partial install.

# === Cache hierarchy: per-language patterns ===

# Node.js
paths: ['~/.npm', 'node_modules']
key:   '${{ runner.os }}-node-${{ hashFiles('package-lock.json') }}'

# Python (pip)
paths: ['~/.cache/pip']
key:   '${{ runner.os }}-pip-${{ hashFiles('requirements*.txt') }}'

# Python (poetry)
paths: ['~/.cache/pypoetry/virtualenvs']
key:   '${{ runner.os }}-poetry-${{ hashFiles('poetry.lock') }}'

# Rust
paths: ['~/.cargo/registry', '~/.cargo/git', 'target']
key:   '${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }}'
# Use Swatinem/rust-cache@v2 for a smart default

# Go
paths: ['~/go/pkg/mod', '~/.cache/go-build']
key:   '${{ runner.os }}-go-${{ hashFiles('go.sum') }}'

# Gradle / Maven
paths: ['~/.gradle/caches', '~/.gradle/wrapper']
key:   '${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle', '**/gradle-wrapper.properties') }}'

# === Cleanup + cost ===

# 12) GitHub Actions cache has limits:
#   - 10 GB per repo, evicted by LRU
#   - Keys not used in 7 days are deleted
# Don't bloat: don't cache things faster to rebuild than to restore.

# 13) Don't cache secrets! Caches are scoped per repo, but a public fork can run an action
#     and potentially read the cache. Never put env vars / .env files in cached paths.

# === Anti-patterns ===

# 14) Useless caches
#   • Caching dist/ across PRs (source changes invalidate it; rebuild is the same speed)
#   • Caching test results (too small to bother)
#   • Caching with a date-based key (always misses)

# 15) When to invalidate intentionally
# - After a CVE in a dep — bump lockfile, cache invalidates automatically
# - Quarterly cleanup — delete the cache UI in CI to start fresh
# - When debugging suspect 'works on stale cache, fails on fresh' — clear + rebuild

# 16) Measure the win
#   • CI minutes BEFORE: npm install = 90s, build = 30s
#   • CI minutes AFTER:  cache hit on lockfile → npm install = 8s
#   • Save ~80s per pipeline → 1000 runs/month = 22 hours of CI saved

# 17) Layered caching strategy for a typical Node app
# 1. setup-node with cache: 'npm'
# 2. actions/cache for node_modules keyed on package-lock.json
# 3. actions/cache for build output keyed on package-lock + git ref (with restore-keys fallback)
# 4. docker/build-push-action with cache-from/to: type=gha
# Combined: 5-min pipelines become 30 seconds on incremental changes.

Why it matters

Key the cache on lockfile hashes; never on source file or date. Pair with restore-keys for partial hits. Cache dependencies, not source builds — the latter are usually faster to recompute than restore.

Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.

Example

Example
- name: Cache pnpm store
  uses: actions/cache@v4
  with:
      path: ~/.pnpm-store
      key: pnpm-${{ hashFiles('pnpm-lock.yaml') }}
      restore-keys: pnpm-
Try it Yourself »

Exercise

Cache action repository name.

uses: actions/ @v4

Discussion

Loading…