Skip to content
Merged
12 changes: 12 additions & 0 deletions .buildkite/hooks/pre-command
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/usr/bin/env bash

Comment thread
mokagio marked this conversation as resolved.
# Allow for the creation of an .nvmrc file in the CI machine.
# This allows testing the plugin fallback to .nvmrc without a confusing instance in the repo root.

set -euo pipefail

if [[ -z "${NVM_BUILDKITE_PLUGIN_TEST_NVMRC_VERSION:-}" ]]; then
exit 0
fi

printf '%s\n' "$NVM_BUILDKITE_PLUGIN_TEST_NVMRC_VERSION" > .nvmrc
38 changes: 36 additions & 2 deletions .buildkite/pipeline.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
steps:
- command: make test

- command: make validate

- label: ":node: Validate {{ matrix.node }} via version property"
Expand All @@ -23,8 +25,40 @@ steps:
- mac
- default

- label: ":node: Do nothing when no .nvmrc and no Node.js version specified"
command: echo noop
- label: ":windows: :node: Validate {{ matrix.node }} on Windows"
command: bash .buildkite/verify_node.sh {{ matrix.node }}
env:
# nvm v0.39.4 hangs on the Windows agent when no .nvmrc exists in the
# working directory: its ancestor walk for one loops forever under the
# agent's bash. The version property below still drives the install; this
# .nvmrc only exists to short-circuit that walk.
NVM_BUILDKITE_PLUGIN_TEST_NVMRC_VERSION: "{{ matrix.node }}"
plugins:
- automattic/nvm#${BUILDKITE_COMMIT}:
version: "{{ matrix.node }}"
agents:
queue: windows
matrix:
setup:
node:
- v20.19.5
- v24.15.0

- label: ":windows: :node: Validate .nvmrc fallback on Windows"
env:
# Create an .nvmrc file to verify the plugin reads it
NVM_BUILDKITE_PLUGIN_TEST_NVMRC_VERSION: 20.19.5
command: bash .buildkite/verify_node.sh "$(cat .nvmrc)"
plugins:
- automattic/nvm#${BUILDKITE_COMMIT}
agents:
queue: windows

- label: ":node: Validate .nvmrc fallback"
env:
# Create an .nvmrc file to verify the plugin reads it
NVM_BUILDKITE_PLUGIN_TEST_NVMRC_VERSION: 24.18.0
command: .buildkite/verify_node.sh "$(cat .nvmrc)"
plugins:
- automattic/nvm#${BUILDKITE_COMMIT}

Expand Down
13 changes: 11 additions & 2 deletions .buildkite/verify_node.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,16 @@ echo "Checking if the active Node.js version is '$1'"
expected_version=$(nvm version-remote "$1")
echo "'$1' is resolved to the Node.js version $expected_version"

current_version=$(nvm current)
echo "Currently activated Node.js version is $current_version"
# On Windows, `nvm current` reports `system` after `--no-use`; PATH has the real version.
case "$(uname -s 2>/dev/null || true)" in
CYGWIN* | MINGW* | MSYS*)
current_version=$(node --version)
echo "Current node executable version is $current_version"
;;
*)
current_version=$(nvm current)
echo "Currently activated Node.js version is $current_version"
;;
esac

test "$current_version" == "$expected_version"
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Use `make test` for shell helper tests and `make validate` for plugin schema validation.
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
@AGENTS.md
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
test:
bash tests/nvm-plugin-helpers-test.sh
bash tests/pre-command-smoke-test.sh

validate:
docker run -it --rm -v "$(shell pwd):/plugin:ro" buildkite/plugin-linter --id automattic/nvm
Comment on lines +1 to 6
78 changes: 78 additions & 0 deletions hooks/lib/nvm-buildkite-plugin.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env bash

# Sourced helper only, so no set options. Up to callers to set strict mode before loading it.
#
# nvm walks PWD with slash trimming, Windows paths can make that loop forever.
# These helpers allow preventing that behavior.

nvm_plugin_should_normalize_windows_pwd() {
local shell_name="$1"
local current_pwd="$2"

case "$shell_name" in
CYGWIN* | MINGW* | MSYS*) ;;
*) return 1 ;;
esac

case "$current_pwd" in
*\\* | ?:*) return 0 ;;
*) return 1 ;;
esac
}

nvm_plugin_windows_pwd_to_posix() {
local current_pwd="$1"
local drive
local drive_lower
local rest

case "$current_pwd" in
[A-Za-z]:*)
drive="${current_pwd:0:1}"
drive_lower="$(printf '%s' "$drive" | tr '[:upper:]' '[:lower:]')"
rest="${current_pwd:2}"
rest="${rest//\\//}"

printf '/%s%s\n' "$drive_lower" "$rest"
return 0
;;
\\\\*)
printf '%s\n' "${current_pwd//\\//}"
return 0
;;
*)
return 1
;;
esac
}

nvm_plugin_normalize_windows_pwd_for_nvm() {
local shell_name
shell_name="$(uname -s 2>/dev/null || true)"

if ! nvm_plugin_should_normalize_windows_pwd "$shell_name" "${PWD:-}"; then
return 0
fi

local normalized_pwd
if ! normalized_pwd="$(nvm_plugin_windows_pwd_to_posix "$PWD")"; then
echo "Cannot normalize Windows PWD for nvm from ${PWD}"
return 0
fi

if [[ -z "$normalized_pwd" || "$normalized_pwd" == "$PWD" ]]; then
echo "Cannot normalize Windows PWD for nvm from ${PWD}"
return 0
fi
Comment on lines +57 to +66

if [[ ! -d "$normalized_pwd" ]]; then

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we detect a Windows PWD that needs normalization but the normalized path does not exist, the helper logs and returns success, then we continue into source "$NVM_DIR/nvm.sh" --no-use. Since we're trying to avoid nvm getting stuck on that bad PWD, should this case fail fast instead? 🤔

echo "Cannot normalize Windows PWD for nvm because ${normalized_pwd} is not a directory"
return 0
fi

echo "Normalizing Windows PWD for nvm from ${PWD} to ${normalized_pwd}"
cd "$normalized_pwd" || {
echo "Failed to normalize Windows PWD for nvm to ${normalized_pwd}"
return 0
}
}
6 changes: 6 additions & 0 deletions hooks/pre-command
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

set -euo pipefail

plugin_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=hooks/lib/nvm-buildkite-plugin.bash
source "$plugin_dir/hooks/lib/nvm-buildkite-plugin.bash"

echo "~~~ :node: Installing nvm"

[[ -f .nvmrc || -n "${BUILDKITE_PLUGIN_NVM_VERSION:-}" ]] || {
Expand Down Expand Up @@ -37,6 +41,8 @@ echo "Installing nvm in $NVM_DIR"

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.4/install.sh | PROFILE=/dev/null bash

nvm_plugin_normalize_windows_pwd_for_nvm

# shellcheck source=/dev/null
source "$NVM_DIR/nvm.sh" --no-use

Expand Down
33 changes: 33 additions & 0 deletions tests/nvm-plugin-helpers-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
#!/usr/bin/env bash

set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
# shellcheck source=hooks/lib/nvm-buildkite-plugin.bash
source "$repo_root/hooks/lib/nvm-buildkite-plugin.bash"

fail() {
echo "FAIL: $*" >&2
exit 1
}

nvm_plugin_should_normalize_windows_pwd "MSYS_NT-10.0" 'C:\buildkite-agent\repo' || fail "MSYS backslash PWD should be normalized"
nvm_plugin_should_normalize_windows_pwd "CYGWIN_NT-10.0" 'C:/buildkite-agent/repo' || fail "Cygwin drive PWD should be normalized"

if nvm_plugin_should_normalize_windows_pwd "MSYS_NT-10.0" "/c/buildkite-agent/repo"; then
fail "MSYS POSIX PWD should not be normalized"
fi

if nvm_plugin_should_normalize_windows_pwd "linux-gnu" 'C:\buildkite-agent\repo'; then
fail "non-Windows shell PWD should not be normalized"
fi

[[ "$(nvm_plugin_windows_pwd_to_posix 'C:\buildkite-agent\repo')" == "/c/buildkite-agent/repo" ]] || fail "backslash drive path should convert to POSIX path"
[[ "$(nvm_plugin_windows_pwd_to_posix 'D:/buildkite-agent/repo')" == "/d/buildkite-agent/repo" ]] || fail "slash drive path should convert to POSIX path"
[[ "$(nvm_plugin_windows_pwd_to_posix '\\server\share\repo')" == "//server/share/repo" ]] || fail "UNC path should convert to POSIX path"

if nvm_plugin_windows_pwd_to_posix "/c/buildkite-agent/repo" >/dev/null; then
fail "POSIX path should not be converted"
fi

echo "All helper tests passed."
71 changes: 71 additions & 0 deletions tests/pre-command-smoke-test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
#!/usr/bin/env bash

set -euo pipefail

repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT

mkdir -p "$tmpdir/bin" "$tmpdir/work"

cat > "$tmpdir/bin/curl" <<'CURL'
#!/usr/bin/env bash
cat <<'INSTALLER'
#!/usr/bin/env bash
mkdir -p "$NVM_DIR"
cat > "$NVM_DIR/nvm.sh" <<'NVM'
nvm() {
case "$1" in
install)
echo "fake nvm install ${*:2}"
;;
use)
echo "fake nvm use ${*:2}"
;;
*)
echo "unsupported nvm command: $*" >&2
return 1
;;
esac
}
NVM
INSTALLER
CURL
chmod +x "$tmpdir/bin/curl"

cat > "$tmpdir/bin/node" <<'NODE'
#!/usr/bin/env bash
if [[ "${1:-}" == "--version" ]]; then
echo "v20.19.5"
elif [[ "${1:-}" == "-" ]]; then
cat >/dev/null
echo "Hello Node.js"
else
echo "unsupported node command: $*" >&2
exit 1
fi
NODE
chmod +x "$tmpdir/bin/node"

(
export NVM_BUILDKITE_PLUGIN_TEST_NVMRC_VERSION=20.19.5

cd "$tmpdir/work"
bash "$repo_root/.buildkite/hooks/pre-command"
)

(
export PATH="$tmpdir/bin:$PATH"
export BUILDKITE_JOB_ID="pre-command-smoke"
export TMPDIR="$tmpdir/tmp"
mkdir -p "$TMPDIR"

cd "$tmpdir/work"
bash "$repo_root/hooks/pre-command"
) > "$tmpdir/pre-command.out"

grep -F "fake nvm install --no-progress" "$tmpdir/pre-command.out" >/dev/null
grep -F "fake nvm use" "$tmpdir/pre-command.out" >/dev/null
grep -F "Node.js and nvm successfully set up." "$tmpdir/pre-command.out" >/dev/null

echo "pre-command smoke test passed."