Test, build, and share data

Separate caches from artifacts

Use dependency caches to save time and workflow artifacts to preserve outputs or evidence between jobs.

Caches and artifacts solve different problems. Mix them up and you ship the wrong bytes or trust a stale dependency tree.

A cache speeds up the next run. An artifact is output from this run that another job or a human must download.

Never point a deploy step at a cache path expecting immutability. Cache eviction can delete what you thought was a release.

Caches are optimization

Dependency caches key off lockfiles. GitHub restores a previous node_modules or npm cache when the key matches.

- uses: actions/setup-node@v4
  with:
    node-version: '22'
    cache: npm
- run: npm ci

If the cache misses, npm ci still works from the lockfile. A cache hit is faster, not safer.

A stale cache can hide a broken lockfile until someone clears it. That is why I still run npm ci, not npm install, in CI.

Artifacts are named outputs

Upload exact build output when another job needs it:

- uses: actions/upload-artifact@v4
  with:
    name: site-${{ github.sha }}
    path: dist/
    retention-days: 14

The deploy job downloads that artifact. Staging and production should promote the same file hash, not rebuild from scratch.

Test reports and screenshots also belong in artifacts, not caches.

Prove the difference

Restore a bad cache on purpose: change a dependency in package.json but keep an old lockfile key until CI looks green with wrong packages. Then run a clean install without cache. The failure shows why cache is not proof.

Download the build artifact and inspect it. The tarball should match what tests exercised.

Name artifacts with the commit SHA or run id so you never overwrite the only copy of a good build.

Try this on your own project: list what you cache versus what you upload. Move anything release-related out of cache and into artifacts.

Lesson completed