From 950bf213132772edf01f88b7ff785ae1f06eba98 Mon Sep 17 00:00:00 2001 From: Yam Borodetsky Date: Mon, 21 Sep 2026 22:31:19 +0500 Subject: [PATCH 1/5] Bind later nub invocations to an explicit node-version via NODE_EXECUTABLE. PATH already fronted the requested Node, but nub still followed the project pin. Exporting NODE_EXECUTABLE makes nub / nub run use the same binary without rewriting pin files. Co-authored-by: Cursor --- .github/workflows/smoke.yml | 36 +++++++++++++++++++++ README.md | 14 ++++----- action.yml | 63 +++++++++++++++++++++++-------------- 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index ce059cb..4e30d14 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -158,6 +158,39 @@ jobs: echo "node-version output: ${{ steps.nub.outputs.node-version }}" [[ "${{ steps.nub.outputs.node-version }}" == 20* ]] || { echo "::error::expected resolved pin 20.x"; exit 1; } + # Explicit node-version must bind later nub invocations, not only PATH node. + # A committed pin of 20 plus node-version: 22 is the matrix-job case: without + # NODE_EXECUTABLE, `nub` would still run 20.x from .node-version. + explicit-binds-nub: + name: explicit node-version binds later nub (${{ matrix.os }}) + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - name: fixture pinned to 20 (input will request 22) + shell: bash + run: echo "20" > .node-version + - uses: ./ + with: + node-version: "22" + - name: later nub uses 22.x, not the committed 20 pin + shell: bash + run: | + set -euo pipefail + echo "NODE_EXECUTABLE=${NODE_EXECUTABLE:-}" + [ -n "${NODE_EXECUTABLE:-}" ] || { echo "::error::NODE_EXECUTABLE was not exported"; exit 1; } + printf 'console.log(process.versions.node)\n' > "$RUNNER_TEMP/nub-ver.js" + nv="$(nub "$RUNNER_TEMP/nub-ver.js" | tr -d '\r' | tail -n1)" + echo "nub ran Node $nv" + [[ "$nv" == 22.* ]] || { echo "::error::nub ran Node $nv, expected 22.x (explicit node-version); the .node-version pin is 20"; exit 1; } + path_nv="$(node -p 'process.versions.node' | tr -d '\r')" + echo "PATH node: $path_nv" + [[ "$path_nv" == 22.* ]] || { echo "::error::PATH node is $path_nv, expected 22.x"; exit 1; } + echo "explicit node-version binds later nub — OK" + eager-provision: name: eager provision (no input) installs the project pin (${{ matrix.os }}) strategy: @@ -183,6 +216,8 @@ jobs: nub node ls | grep -E '(^|[^0-9])20\.' || { echo "::error::eager provision did not install the pinned Node 20.x"; exit 1; } echo "node-version output: ${{ steps.nub.outputs.node-version }}" [[ "${{ steps.nub.outputs.node-version }}" == 20* ]] || { echo "::error::expected resolved pin 20.x, got ${{ steps.nub.outputs.node-version }}"; exit 1; } + # No explicit node-version input → do not override the project pin. + [ -z "${NODE_EXECUTABLE:-}" ] || { echo "::error::NODE_EXECUTABLE is set without an explicit node-version: '${NODE_EXECUTABLE}'"; exit 1; } no-pin: name: no project pin does not fail the action (${{ matrix.os }}) @@ -431,6 +466,7 @@ jobs: set -euo pipefail # provision-node: false leaves the node-version output empty. [ -z "${{ steps.nub.outputs.node-version }}" ] || { echo "::error::node-version output is '${{ steps.nub.outputs.node-version }}' with provision-node: false, expected empty"; exit 1; } + [ -z "${NODE_EXECUTABLE:-}" ] || { echo "::error::NODE_EXECUTABLE is set with provision-node: false: '${NODE_EXECUTABLE}'"; exit 1; } d="$RUNNER_TEMP/additive" && mkdir -p "$d/local-dep" && cd "$d" cat > package.json <<'PKG' { "name": "additive", "version": "1.0.0", "dependencies": { "local-dep": "file:local-dep" }, diff --git a/README.md b/README.md index 48b68dc..74b7ca7 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Because the eager provision reads the project's pin files off disk, **`actions/c # after - uses: nubjs/setup-nub@v0 with: - node-version: 20 # provisions Node 20 and fronts it on the global PATH + node-version: 20 # PATH node and later `nub` both use 20 cache: npm # accepted; caching is on by default regardless of value registry-url: https://registry.npmjs.org ``` @@ -60,19 +60,17 @@ Like `actions/setup-node`, setup-nub provisions a Node toolchain and **adds its Which version is fronted: -- **With an explicit `node-version` / `node-version-file`** — that version is authoritative for the PATH (matching setup-node): it is provisioned and its bin dir is fronted. `nub` itself still runs the **project's** declared pin at invocation time; if the two differ, the action emits a warning. -- **With no input (the common case)** — the project's own pinned Node is resolved (`devEngines.runtime` → `.node-version` → `.nvmrc` → `engines.node`), provisioned up front so later steps don't pay a lazy download, and its bin dir is fronted. +- **With an explicit `node-version` / `node-version-file`** — that version is authoritative for the rest of the job (matching setup-node): it is provisioned, its bin dir is fronted on PATH, and `NODE_EXECUTABLE` is exported so later `nub` / `nub run` use the same binary without rewriting pin files on disk. +- **With no input (the common case)** — the project's own pinned Node is resolved (`devEngines.runtime` → `.node-version` → `.nvmrc` → `engines.node`), provisioned up front so later steps don't pay a lazy download, and its bin dir is fronted. Later `nub` invocations keep following the project's pin. - **No pin and no input** — nothing is provisioned and nothing is fronted (no error); the runner's preinstalled Node stays on PATH and nub provisions lazily at runtime. -The remaining nuance vs setup-node: when an explicit `node-version` is set, it governs the **global PATH**, but `nub