Cache Early, Debug Loudly, Vet Every Action
Why caching npm installs is worth doing before it's slow, the flag that shows every command a step actually runs, and what to check before trusting someone else's action.
Right now, npm ci finishes in a few seconds. That's exactly the wrong time to decide caching doesn't matter yet.
This is the same instinct as pinning your Node version and dependencies before drift becomes a problem: fix the thing while it's a one-line change, not once it's already slow.
Cache before it's a problem, not after
Nobody adds new dependencies to a two-file proof-of-concept. A stable project with multiple contributors is a different story: packages get added continuously, and every one of them adds install time.
Caching is one line to add now, and a much bigger cleanup to add later once a slow install is already costing the team real time on every single run.
- uses: actions/setup-node@v4
with:
node-version: 22.22.0
cache: npmThat's the whole change for a Node project. setup-node already knows where npm keeps its cache and how to key it off your lockfile, so cache: npm is enough.
If your language doesn't have that built in, the manual version is actions/cache:
- uses: actions/cache@v4
with:
path: ~/.npm
key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}The key is built from a hash of your lockfile. Change a dependency, the hash changes, and that's a cache miss, a fresh install that gets cached again for next time. Leave the lockfile alone, and every run after the first one restores instantly instead of reinstalling from scratch.
See every command a step actually runs
The other stage-two habit worth building: turning on step debugging when something fails and the reason isn't obvious.
env:
ACTIONS_STEP_DEBUG: trueSet this as a repository secret (not a plain variable), and every step starts printing the exact commands it runs as it runs them, the same idea as set -x in a shell script. Normally you only see a script's output. With this on, you see the commands generating that output too.
One real caveat: if a script embeds a secret in a command, that command becomes visible in the log. Turn this on to diagnose a specific failure, not as a permanent setting.
An action is a dependency you didn't write
Every uses: line in a workflow pulls in code you didn't write and probably haven't read. Treat it exactly like you'd treat an npm package: something to check before you trust it, not after.
An action nobody has touched in three years, with a handful of stars and no security policy, is a real supply chain risk, not a hypothetical one. It runs with whatever access your workflow already grants it, the exact same concern behind never handing a job broader access than the task actually needs. Actions published by GitHub itself or by the platform vendor (AWS, Docker) are the safest default. A community action with a large, active user base and a visible security policy is the next tier. An action from an account you've never heard of, created two days ago, is the one to skip, especially anywhere near a secret.
The Essentials
- Cache dependencies before the install is slow, not after. For a Node project,
cache: npmonsetup-nodeis the entire change. ACTIONS_STEP_DEBUG: trueshows every command a step runs, useful for one failure, risky to leave on permanently.- A marketplace action is a dependency. Check its update history, real usage, and security advisories before a workflow depends on it.
Further Reading and Watching
Keep reading