Caching and Artifacts¶
Caching: skip redoing work that hasn't changed¶
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: ~/.npm
key: npm-${{ hashFiles('package-lock.json') }}
- run: npm ci
The key is what determines a hit or miss — keying on
hashFiles('package-lock.json') means the cache is reused exactly
when the lockfile hasn't changed (dependencies are identical), and a
new cache entry is created under a new key the moment the
lockfile does change. This is the same underlying principle as
Docker's layer caching —
cache reuse driven by whether the input to a step actually changed,
not by how much time has passed or how many runs have happened since.
Artifacts: passing files between jobs¶
jobs:
build:
runs-on: ubuntu-latest
steps:
- run: npm run build
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- run: ./deploy.sh dist/
Given that jobs run in genuinely separate machines (per
Fundamentals),
this upload/download round trip through GitHub's artifact storage is
the only way for deploy to see build's output — there's no
shared disk to fall back on. Artifacts are also how build outputs
survive to be inspected later (downloaded from the workflow run's UI)
even after the runner VM that produced them is gone.
Cache vs. artifact — not interchangeable¶
Caching is for speeding up repeated work across separate workflow
runs (dependencies that rarely change, re-fetched otherwise) —
it's explicitly best-effort; a cache miss just means falling back to
doing the work fresh, never a hard failure. Artifacts are for
passing specific, expected files between jobs within one run —
losing an artifact (say, download-artifact finds nothing because
the name doesn't match) is a real failure, not a fallback-to-slow-path
situation, because the deploying job has no other way to get those
files at all.
Common pitfall¶
Using a cache key that's too broad (e.g. a fixed string with no content hash at all) means the cache never invalidates when dependencies actually change — a workflow can end up testing against genuinely stale dependencies while believing it fetched the current lockfile's versions, because the cache hit served old content that was never supposed to still match. Keying on a hash of the actual lockfile (as shown above) ties cache validity directly to the input that should invalidate it, rather than to an unrelated proxy like a date or a manually bumped version number that's easy to forget to update.