A drop-in for actions/setup-node — swap one line:

steps:
  - uses: actions/checkout@v4
  - uses: actions/setup-node@v4
  - uses: nubjs/setup-nub@v0
  - run: nub install
  - run: nub run test

Everything that follows resolves to the project's pinned Node — the same contract as setup-node. Bare node, npm, and npx in later steps run the pinned version, because the action fronts that Node's bin directory on the runner's global PATH.

- uses: actions/checkout@v4
- uses: nubjs/setup-nub@v0
- run: node --version   # the project's pinned Node, not the runner default
- run: nub install

Read the full docs on github.com. This action accepts the same inputs.

How it works

The action installs the nub CLI from the release archive for the runner's platform, verified against the release's sha256 sidecar, into the runner tool cache, then provisions the project's pinned Node into Nub's cache during the setup step, before any of your steps run. If the archive install fails, the action falls back to npm install -g @nubjs/nub. The pin is resolved from the project's Node-version sources, in this order:

  1. package.json#/devEngines/runtime
  2. .node-version
  3. .nvmrc
  4. .tool-versions (the asdf/mise file's nodejs or node line)
  5. package.json#/engines/node

That resolved Node's bin directory is added to the global PATH, so bare node/npm/npx and every subsequent step see the pinned version. With no pin declared, the eager step skips and Nub provisions at runtime.

Because the pin lives in the repository, actions/checkout must run before this action.

Beside setup-node

The action also runs after actions/setup-node in a workflow that keeps it. The install line is what changes:

  - uses: actions/setup-node@v4
    with:
      node-version: 22
      cache: npm
- uses: nubjs/setup-nub@v0
  with:
    provision-node: false
- run: npm ci
- run: nub install --frozen-lockfile
  - run: npm test

nub install --frozen-lockfile reads the existing package-lock.json unchanged, and npm test runs the real npm as before. provision-node: false leaves the Node that setup-node put on PATH in place; without it this action fronts the project's own pin. The cache: npm line can stay: it restores npm's own cache, which Nub does not read, and this action caches Nub's store by default.

setup-node, with Nub installed

nubjs/nub/setup-node takes the inputs and outputs of actions/setup-node and installs nub beside it:

- uses: actions/checkout@v4
- uses: nubjs/nub/setup-node@v0
- run: npm ci
- run: npm test

Two defaults differ from the action above. With no node-version input, the runner's Node stays when the project's pin allows it or when there is no pin; only a pin that excludes the runner's Node is downloaded and put first on PATH, with a notice in the job log. And the package-manager shims are on by default: npm, pnpm and yarn in later steps run the version the project pins in packageManager or devEngines, provisioned on demand. provision-node: false and shim: false turn each off. The inputs are listed in its README.

The install step only

nubjs/nub/install replaces the line that runs npm ci, pnpm install --frozen-lockfile, yarn install --immutable or bun install --frozen-lockfile, and changes nothing else:

  - uses: actions/setup-node@v4
    with:
      node-version: 22
      cache: npm
- run: npm ci
- uses: nubjs/nub/install@v0
  - run: npm test

Nub reads the lockfile unchanged, installs it into node_modules, and caches its store across runs, keyed on the lockfile. Node and the package managers on PATH are left alone. working-directory points the install at a subdirectory, frozen-lockfile: false lets it update the lockfile as npm install does, and shim: true adds the package-manager shims for later steps. The inputs are listed in its README.

npm ci, on Nub

nubjs/nub/npm-ci is the npm ci step for a project with a package-lock.json. It installs exactly what the lockfile records, under npm ci's own contract, and leaves the lockfile as it found it:

  - uses: actions/setup-node@v4
    with:
      node-version: 22
      cache: npm
- run: npm ci
- uses: nubjs/nub/npm-ci@v0
  with:
    lockfile: package-lock.json
  - run: npm test

Every package is installed at the version and from the resolved URL the lockfile records and checked against its integrity hash; nothing is resolved against the registry. node_modules is removed first, the tree is hoisted as npm lays it out, every lifecycle script runs as it does under npm and under the job's node (a .nvmrc or engines pin in the project is not consulted and no Node is downloaded), and the install fails when the lockfile and package.json disagree. The action hashes the lockfile before and after the install and fails if a byte changed; the hash is its lockfile-sha256 output. args takes npm ci's own flags (--omit=dev, --ignore-scripts); a flag the engine does not honor fails the action rather than running npm.

install takes any lockfile and applies Nub's own layout and build-script posture. npm-ci applies npm's, so a workflow that ran npm ci behaves as it did. The inputs are listed in its README.

Inputs

InputDefaultDescription
nub-versionlatestVersion of Nub to install — any semver range npm understands.
node-versionProvision this version and front it on PATH instead of the project pin.
node-version-fileRead a version from a file and front it on PATH instead of the project pin.
cacheautoCache Nub's store and provisioned Node toolchains. Auto-enables on a lockfile or packageManager/devEngines; an explicit value wins.
package-manager-cachetrueSet false to turn off the automatic caching above.
cache-dependency-pathLockfile path(s) whose hash keys the cache.
cache-key-prefixPrefix injected into the cache key to scope or bust caches independently.
working-directorycheckout rootDirectory to resolve the pin and lockfile from, for monorepo subdirectories.
registry-urlRegistry to set up for auth; writes a temporary user-level .npmrc via NPM_CONFIG_USERCONFIG.
scopeScope for a scoped registry; falls back to the repository owner for GitHub Packages.
always-authfalseAuthenticate on every registry request.
tokengithub.tokenToken for GitHub-API rate-limit relief when resolving Nub's version range.
shimfalseRun nub pm shim after install and put the shim directory first on PATH, so npm, pnpm, and yarn in later steps run the package manager the project pins.
provision-nodetrueSet false to leave Node alone: nothing provisioned, nothing fronted on PATH, and the node-version output empty. For a job where actions/setup-node already ran.

The setup-node inputs check-latest, architecture, mirror, and mirror-token are accepted and ignored. The version input is a deprecated alias for nub-version and emits a warning.

nub-version

Pin the CLI version to install — any semver range npm understands. Defaults to the latest release:

- uses: nubjs/setup-nub@v0
  with:
    nub-version: ^0.1.0

node-version

Front a specific version on PATH for this run instead of the project pin. The action provisions it during setup and puts it on the global PATH, so bare node downstream is this version:

- uses: nubjs/setup-nub@v0
  with:
    node-version: 26.10.0

Nub still runs the project's declared pin when invoked as nub, so the action warns if this input differs from that pin. Omit it to let Nub resolve and provision the project pin automatically.

node-version-file

Read a Node version from a file, then provision and front it on PATH. Accepts .node-version, .nvmrc, and package.json (reads devEngines.runtime, then engines.node):

- uses: nubjs/setup-nub@v0
  with:
    node-version-file: .node-version

cache

Caching is on by default when the project has a lockfile or a package.json that declares packageManager or devEngines, mirroring setup-node. Set the input explicitly to override:

- uses: nubjs/setup-nub@v0
  with:
    cache: true   # force on; `false` forces off

A package-manager name (npm/pnpm/yarn/bun) is accepted for setup-node compatibility and treated as on — Nub keeps one store regardless of package manager.

The cache key is derived from the project's lockfile and the Node pin, with the lockfile auto-detected in this order:

  1. pnpm-lock.yaml
  2. package-lock.json
  3. bun.lock
  4. bun.lockb
  5. yarn.lock

package-manager-cache

Turns the automatic caching above off without setting cache:

- uses: nubjs/setup-nub@v0
  with:
    package-manager-cache: false

cache-dependency-path

Override the auto-detected lockfile for cache keying — a lockfile path, glob, or newline-delimited list:

- uses: nubjs/setup-nub@v0
  with:
    cache-dependency-path: |
      app/pnpm-lock.yaml
      packages/*/pnpm-lock.yaml

A restore-keys ladder gives a partial warm hit even when the lockfile changes.

cache-key-prefix

Inject a prefix into the cache key (nub-<os>-<arch>-<prefix>-<hash>) to scope or bust caches independently of the lockfile:

- uses: nubjs/setup-nub@v0
  with:
    cache-key-prefix: v2

working-directory

Resolve the Node pin and lockfile from a subdirectory rather than the checkout root — for monorepos where package.json/.node-version live below the root:

- uses: nubjs/setup-nub@v0
  with:
    working-directory: apps/web

registry-url

Set up authenticated registry access. The action writes a temporary user-level .npmrc via NPM_CONFIG_USERCONFIG and wires the auth token to NODE_AUTH_TOKEN:

- uses: nubjs/setup-nub@v0
  with:
    registry-url: https://registry.npmjs.org
- run: nub install
  env:
    NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

For GitHub Packages, point at the registry and the scope defaults to the repository owner:

- uses: nubjs/setup-nub@v0
  with:
    registry-url: https://npm.pkg.github.com
- run: nub install
  env:
    NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Read the full docs on docs.github.com.

scope

Scope for a scoped registry. Falls back to the repository owner for npm.pkg.github.com. The .npmrc written by registry-url follows the same contract as setup-node.

always-auth

Write always-auth=true into the .npmrc to authenticate on every registry request.

token

Token for GitHub-API rate-limit relief when resolving Nub's version range (and Node downloads on GHES). Defaults to github.token on github.com.

provision-node

Leave Node alone. Nothing is provisioned, nothing is fronted on PATH, and the node-version output is empty. For a job where actions/setup-node already put the wanted Node on PATH; nub still resolves the project's pin at its own invocation:

- uses: actions/setup-node@v4
  with:
    node-version: 26.10.0
- uses: nubjs/setup-nub@v0
  with:
    provision-node: false

shim

Run nub pm shim after install and put the shim directory first on PATH. In a project that pins a package manager through packageManager or devEngines.packageManager, npm, pnpm, and yarn in later steps run that pinned version, provisioned on demand. In an unpinned project they fall through to the runner's own tool. The shims do not route those commands into Nub's own installer; use nub install for that.

- uses: nubjs/setup-nub@v0
  with:
    shim: true
- run: pnpm install --frozen-lockfile   # the version packageManager pins

Outputs

OutputDescription
nub-versionThe installed Nub version.
node-versionThe Node version provisioned during setup; empty when nothing was provisioned.
cache-hitWhether an exact store-cache match was restored; empty on a miss, mirroring actions/cache.
caching-enabledWhether caching is active for this run, independent of whether a cache was hit.

Differences from setup-node

Two behaviors differ:

  • The cache input is a boolean rather than a package-manager name. A package-manager name still works (it is treated as on), since Nub keeps a single store regardless of package manager.
  • The registry .npmrc is written fresh to $RUNNER_TEMP and pointed at via NPM_CONFIG_USERCONFIG, rather than merged into the user .npmrc. Both are parsed identically by npm and Nub's package manager.
  • Node manager — how Nub provisions and pins Node versions.
  • Package manager — what nub install reads, including the .npmrc the action writes.