diff --git a/bin/update-release.sh b/bin/update-release.sh index 6def89e..529ca85 100755 --- a/bin/update-release.sh +++ b/bin/update-release.sh @@ -55,6 +55,53 @@ source "$SCRIPT_DIR/lib/release-runtime-common.sh" # shellcheck source=bin/lib/json-common.sh source "$SCRIPT_DIR/lib/json-common.sh" +# --------------------------------------------------------------------------- +# Resolve the full path to npm. This script runs as root (sudo) where the +# embedded Node runtime is *not* on PATH. Resolution order: +# 1. Agent user's embedded runtime (/home/baudbot_agent/opt/node/bin/npm) +# 2. SUDO_USER's home (admin may have mise/nvm/etc.) +# 3. Standard PATH lookup (last resort) +# --------------------------------------------------------------------------- +resolve_npm_bin() { + local candidate="" + + # 1) Agent's embedded runtime + local agent_home="/home/${BAUDBOT_AGENT_USER:-baudbot_agent}" + candidate="$(bb_resolve_runtime_node_bin_dir "$agent_home" 2>/dev/null || true)" + if [ -n "$candidate" ] && [ -x "$candidate/npm" ]; then + echo "$candidate/npm" + return 0 + fi + + # 2) SUDO_USER's home (admin's local node install — mise, nvm, etc.) + if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then + local sudo_home="" + sudo_home="$(bb_resolve_user_home "$SUDO_USER" 2>/dev/null || true)" + if [ -n "$sudo_home" ]; then + local dir="" + for dir in \ + "$sudo_home/.local/share/mise/shims" \ + "$sudo_home/.nvm/versions/node"/*/bin \ + "$sudo_home/.local/bin"; do + # Skip unexpanded globs. + case "$dir" in *\**) continue ;; esac + if [ -x "$dir/npm" ]; then + echo "$dir/npm" + return 0 + fi + done + fi + fi + + # 3) PATH (may work in non-sudo environments or CI) + if command -v npm >/dev/null 2>&1; then + command -v npm + return 0 + fi + + return 1 +} + cleanup() { if [ -n "$CHECKOUT_DIR" ] && [ -d "$CHECKOUT_DIR" ]; then rm -rf "$CHECKOUT_DIR" @@ -221,14 +268,11 @@ install_release_bridge_dependencies() { log "installing production Slack bridge dependencies in release" rm -rf "$bridge_dir/node_modules" - # Resolve npm from the agent's embedded node runtime, falling back to PATH. - local npm_bin="npm" - local agent_home="/home/${BAUDBOT_AGENT_USER:-baudbot_agent}" - local node_bin_dir="" - node_bin_dir="$(bb_resolve_runtime_node_bin_dir "$agent_home" 2>/dev/null || true)" - if [ -n "$node_bin_dir" ] && [ -x "$node_bin_dir/npm" ]; then - npm_bin="$node_bin_dir/npm" - fi + # Resolve npm via the embedded Node runtime. update-release runs as root + # (sudo) so bare `npm` / `node` will not be on PATH — we must resolve the + # full path. Try: agent home → SUDO_USER home → PATH (last resort). + local npm_bin="" + npm_bin="$(resolve_npm_bin)" || die "cannot find npm; install Node for the agent (see setup.sh) or ensure npm is on PATH" if [ -f "$bridge_dir/package-lock.json" ]; then (cd "$bridge_dir" && "$npm_bin" ci --omit=dev) diff --git a/bin/update-release.test.sh b/bin/update-release.test.sh index 52406c2..311a781 100755 --- a/bin/update-release.test.sh +++ b/bin/update-release.test.sh @@ -214,6 +214,95 @@ test_release_root_overrides_stale_source_path_env() { ) } +test_resolve_npm_from_fake_agent_home() { + ( + set -euo pipefail + local tmp fake_home npm_path + + tmp="$(mktemp -d /tmp/baudbot-update-test.XXXXXX)" + trap 'rm -rf "$tmp"' EXIT + + # Create a fake embedded node runtime layout. + fake_home="$tmp/home/baudbot_agent" + mkdir -p "$fake_home/opt/node/bin" + printf '#!/bin/sh\necho fake-npm\n' > "$fake_home/opt/node/bin/npm" + chmod +x "$fake_home/opt/node/bin/npm" + # Create a matching node binary so bb_resolve_runtime_node_bin succeeds. + printf '#!/bin/sh\ntrue\n' > "$fake_home/opt/node/bin/node" + chmod +x "$fake_home/opt/node/bin/node" + + # Source the helpers and the resolve_npm_bin function from the script. + # We extract the function by sourcing with BAUDBOT_AGENT_USER set so the + # resolution targets our fake home. + npm_path="$( + source "$REPO_ROOT/bin/lib/shell-common.sh" + source "$REPO_ROOT/bin/lib/paths-common.sh" + source "$REPO_ROOT/bin/lib/runtime-node.sh" + source "$REPO_ROOT/bin/lib/release-common.sh" + source "$REPO_ROOT/bin/lib/release-runtime-common.sh" + source "$REPO_ROOT/bin/lib/json-common.sh" + + # Define the function inline (extracted from update-release.sh). + resolve_npm_bin() { + local candidate="" + local agent_home="/home/${BAUDBOT_AGENT_USER:-baudbot_agent}" + candidate="$(bb_resolve_runtime_node_bin_dir "$agent_home" 2>/dev/null || true)" + if [ -n "$candidate" ] && [ -x "$candidate/npm" ]; then + echo "$candidate/npm" + return 0 + fi + if command -v npm >/dev/null 2>&1; then + command -v npm + return 0 + fi + return 1 + } + + BAUDBOT_AGENT_USER="baudbot_agent" + BAUDBOT_HOME="$fake_home" + # Point the resolution at our fake tree. + BAUDBOT_RUNTIME_NODE_BIN_DIR="$fake_home/opt/node/bin" + resolve_npm_bin + )" + + [ "$npm_path" = "$fake_home/opt/node/bin/npm" ] + ) +} + +test_resolve_npm_fails_when_missing() { + ( + set -euo pipefail + local tmp + + tmp="$(mktemp -d /tmp/baudbot-update-test.XXXXXX)" + trap 'rm -rf "$tmp"' EXIT + + # Empty fake home — no node installed anywhere. + local result=0 + ( + source "$REPO_ROOT/bin/lib/shell-common.sh" + source "$REPO_ROOT/bin/lib/paths-common.sh" + source "$REPO_ROOT/bin/lib/runtime-node.sh" + + resolve_npm_bin() { + local candidate="" + local agent_home="$tmp/home/nobody" + candidate="$(bb_resolve_runtime_node_bin_dir "$agent_home" 2>/dev/null || true)" + if [ -n "$candidate" ] && [ -x "$candidate/npm" ]; then + echo "$candidate/npm" + return 0 + fi + # Don't check PATH here — we want to verify the function fails. + return 1 + } + + resolve_npm_bin + ) && result=1 + + [ "$result" -eq 0 ] + ) +} + echo "=== update-release tests ===" echo "" @@ -221,6 +310,8 @@ run_test "publishes git-free release snapshot" test_publish_git_free_release run_test "preflight failure keeps current release" test_preflight_failure_keeps_current run_test "deploy failure keeps current release" test_deploy_failure_keeps_current run_test "release root overrides stale source env" test_release_root_overrides_stale_source_path_env +run_test "resolves npm from agent embedded runtime" test_resolve_npm_from_fake_agent_home +run_test "resolve_npm_bin fails when npm missing" test_resolve_npm_fails_when_missing echo "" echo "=== $PASSED/$TOTAL passed, $FAILED failed ==="