npm-install caches npm and Yarn installs so CI stops re-downloading them
GitHub Action for install npm dependencies with caching without any configuration
At a glance
- What is it?
- bahmutov/npm-install is a GitHub Action that installs Node dependencies and caches the package manager cache, with no configuration needed for the common case and options for rolling caches, monorepos, custom install commands and production-only installs. It suits teams whose CI already runs npm ci or yarn --frozen-lockfile, and it adds nothing to a pipeline that does not.
- Who is it for?
- npm-install is worth adopting for a Node CI pipeline that reinstalls the same dependencies on every run, because it turns the package manager cache into something the action manages for you, and the common case is one line in the workflow.
- Can I use it commercially?
- Not without permission. GitHub finds no licence file in the repository, and without a licence all rights are reserved by default: you may read the code but not reuse it. Check the README, or ask the authors, before using it.
- Is it still maintained?
- Yes. The repository last received commits 135 days ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The premise is that a dependency install should not cost a full download every run
The action's own description is the shortest statement of the problem. It is a GitHub Action for installing npm dependencies with caching without any configuration, and the readme is direct about who it is for. If you use npm ci or yarn --frozen-lockfile on CI to install dependencies, this action is for you, and the promise is that your modules will be installed and the ~/.npm or ~/.cache/yarn folder will be cached. That is the whole model. The action replaces a bare install step with an install step that also populates and restores a cache, so a second run does not re-download tarballs that the runner image does not already carry. The phrase without any configuration matters more than it sounds, because the default setup in most repositories is a cache step that somebody wrote by hand, usually copied from a blog post, usually with a key that misses more often than it hits. The cost of the manual version is not the action, it is the maintenance of a cache key expression across a team.
The default workflow is three steps and one of them is the action
The basic example is what the readme says should cover 95 percent of use cases, and it is worth reading as a whole because the order matters. The action assumes the repository has been checked out first, because the lockfile it hashes is a file in the working tree:
name: main
on: [push]
jobs:
build-and-test:
runs-on: ubuntu-latest
name: Build and test
steps:
- uses: actions/checkout@v6
- uses: bahmutov/npm-install@v1
- run: npm tSo the sequence is checkout, then install, then your test command, and the install step is a single line with no inputs at all. The action is referenced by the v1 tag rather than a commit SHA, which is a convenience that also means the version you run can change under you, a consideration for a step that has write access to your repository. The readme links a separate example repository showing the same setup, and the CI table at the top of the readme lists the workflows the project tests itself against, including basic, shrinkwrap, Yarn, without lock file, subfolders and Node version. That table is the closest thing to a test plan: those six cases are the behaviours the project claims to support, each with its own workflow file in the repository.
Lockfile hashing decides whether the cache hits, and there is an escape hatch
By default the action uses a lock file, and the readme names the candidates as package-lock.json, npm-shrinkwrap.json or yarn.lock. The cache key is derived from the exact hash of that lock file, or of the package.json when a lock file is not used. That is the correct default for a project where the lockfile is the contract. It is also the reason the rolling cache option exists. The readme explains the problem directly. Caching by exact lockfile hash causes cache misses whenever dependencies change, which can be slower than simply reinstalling for big projects, because a miss means you pay the full download cost and the restore was wasted effort. The escape hatch for library authors is the useLockFile option, which falls back to package.json alone:
- uses: bahmutov/npm-install@v1
with:
useLockFile: falseThe readme links a rationale for that choice in the context of building libraries, where a lockfile is often not committed because consumers resolve their own versions. In that case the package.json hash is the right cache identity, since the resolved tree can differ anyway. The rolling cache is the other escape hatch and it works in the opposite direction, restoring from more keys so that a run with a changed lockfile can still reuse an older cache, with the cache expiring after a month to keep it from growing without bound.
Monorepos need one action call per package, or a multi-directory list
A repository with packages in separate folders is handled in two documented ways, and the readme is clear that the first is the recommended shape. If your repository contains packages in separate folders, you install each one separately, with one action step per package, each carrying a working-directory input:
- uses: bahmutov/npm-install@v1
with:
working-directory: app1
- uses: bahmutov/npm-install@v1
with:
working-directory: app2The alternative is to specify multiple subfolders in a single action, one subfolder per line, using a block scalar for the working-directory input:
- uses: bahmutov/npm-install@v1
with:
working-directory: |
app1
app2The separate-steps form has an advantage the readme does not spell out. Each call gets its own cache entry keyed on that package's own lockfile, so a change in app1 does not invalidate the cache for app2, whereas a single multi-directory call has to decide how to key a cache that covers both. Either way the tree is walked per package, with a package-lock.json in one and a yarn lockfile in the other, which the readme illustrates with a small directory tree. The readme also links a monorepo example repository.
Custom commands, cache prefixes and production-only installs
Three more inputs cover the cases where the default is wrong. The install-command input replaces the command the action runs, so a Yarn project that wants a silent frozen install writes it directly:
- uses: bahmutov/npm-install@v1
with:
install-command: yarn --frozen-lockfile --silentThe cache-key-prefix input exists for a specific situation, described as installing different individual tools where you want separate caches for each. The readme's example installs two different tools with useLockFile set to false and a distinct prefix, and the resulting cache keys become npm-tool-a-... and npm-tool-b-.... Without the prefix, two tools installed into the same cache directory would collide, since the cache key is derived from the package.json hash and two different tools produce two different hashes but write into the same folder. The third input is not a with block at all. To install only production dependencies, you set the NODE_ENV environment variable on the step, and the action reads it:
- uses: bahmutov/npm-install@v1
env:
NODE_ENV: productionThat is the one input where the readme's phrasing is slightly misleading, since it describes setting an environment variable rather than an action input, and the effect is that dev dependencies are not installed.
The action is also a library, and the repo is small on purpose
The readme documents an npm entry point for people writing their own action. If you are writing your own GitHub Action and want to use this one as a utility function, you import it and run it:
const { npmInstallAction } = require('npm-install')
await npmInstallAction()The repository layout supports the reading that this is a small, focused tool. The source is a single index.js, with a dist directory holding the bundled output, an action.yml describing the action interface, a test directory, and an examples directory with a folder per documented scenario, matching the six workflows the CI table lists. The package manifest shows the mechanism of distribution. The main field points at dist/index.js, the build script runs ncc build into dist, the pretest hook runs the build before the tests, and the files array publishes action.yml, index.js and dist, which is the standard pattern for a JavaScript action that must ship its compiled bundle because the runner does not install dependencies for you. The runtime dependencies are the GitHub toolkit packages for cache, core, exec and io, plus hasha for hashing, quote for shell quoting, and @bahmutov/print-env for printing environment state when debugging. Versioning is handled by semantic-release, with a renovate badge, and the manifest version is a development placeholder, 0.0.0-development, because the real version comes from the release pipeline.
Two caveats: the licence record and where the action can fail
Two practical warnings round out the picture. The first is licensing. The project metadata gives no licence at all, while the package manifest declares MIT, and the readme carries no licence section. For a build dependency in your own project that inconsistency is unlikely to matter, but if you redistribute the action or build a product on top of it, the file to check is the repository itself rather than the manifest. The second is that the action needs a registry it can reach, and it needs the package manager to be present on the runner. That is why the readme tells you to use actions/setup-node before installing dependencies when you need a specific Node version, and it shows the order explicitly, with setup-node and a node-version of 20 between the checkout and the install step. The action also has a debugging path, with two secrets that turn on verbose logging, ACTIONS_RUNNER_DEBUG and ACTIONS_STEP_DEBUG, and the readme points at the Actions debugging guide for how to use them. The last push was on 2026-06-10 and the newest release is v1.12.1 from 2026-03-19, so the tagged version is a few months behind the last commit.
Editorial conclusion
npm-install is worth adopting for a Node CI pipeline that reinstalls the same dependencies on every run, because it turns the package manager cache into something the action manages for you, and the common case is one line in the workflow. It is the wrong tool for a pipeline that has already solved caching with setup-node's own cache option, or one that installs from a registry the action cannot reach, and it should not be used where the licence position is unclear since the project metadata gives no licence even though the package manifest says MIT. Verify first that a first run without a warm cache still succeeds, that your lockfile hash changes when your dependencies change, and that the rolling cache expiry of one month matches how often you actually expect your dependencies to change. The last push was on 2026-06-10 and the newest release is v1.12.1 from 2026-03-19.
Frequently asked questions
How do I add npm-install to a GitHub Actions workflow?
Check out the repository first, then add a step using bahmutov/npm-install@v1 with no inputs for the common case. The action installs the dependencies and caches the ~/.npm or ~/.cache/yarn folder.
What does the useRollingCache option do in npm-install?
It restores the cache from more keys so a run with a changed lockfile can still reuse an older cache. The readme notes that exact lockfile hashing causes cache misses when dependencies change, which can be slower than reinstalling for big projects, and the rolling cache expires after a month.
How do I install dependencies in a monorepo with npm-install?
Call the action once per package with a working-directory input for each, which the readme presents as the recommended shape. You can also list several subfolders in a single call, one per line, using a block scalar for working-directory.
How do I install only production dependencies with npm-install?
Set the NODE_ENV environment variable to production on the step, rather than passing an input. The action reads it and skips the dev dependencies.
Can I use npm-install inside my own GitHub Action?
Yes. The readme says that if you are writing your own action and want to use this one as a utility function, you import npmInstallAction from the npm-install package and await it.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/bahmutov-npm-install)Was this useful?