Skip to content

Commit 462c645

Browse files
committed
deploy: resolve npm via agent's embedded runtime in update-release
update-release.sh runs as root (sudo baudbot update) but the embedded Node/npm runtime is installed under baudbot_agent's home directory and is not on root's PATH. The previous code fell back to bare `npm` which violates the project rule against bare node/npm in root scripts and fails on systems where only the agent user has Node installed. Replace the inline fallback with a resolve_npm_bin() helper that tries: 1. Agent user's embedded runtime (/home/baudbot_agent/opt/node/bin) 2. SUDO_USER's home (mise, nvm, etc.) 3. Standard PATH (last resort) Dies with a clear error if npm cannot be found at all. Adds tests for the resolution logic (success + failure paths).
1 parent 11b44d9 commit 462c645

2 files changed

Lines changed: 143 additions & 8 deletions

File tree

bin/update-release.sh

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,53 @@ source "$SCRIPT_DIR/lib/release-runtime-common.sh"
5555
# shellcheck source=bin/lib/json-common.sh
5656
source "$SCRIPT_DIR/lib/json-common.sh"
5757

58+
# ---------------------------------------------------------------------------
59+
# Resolve the full path to npm. This script runs as root (sudo) where the
60+
# embedded Node runtime is *not* on PATH. Resolution order:
61+
# 1. Agent user's embedded runtime (/home/baudbot_agent/opt/node/bin/npm)
62+
# 2. SUDO_USER's home (admin may have mise/nvm/etc.)
63+
# 3. Standard PATH lookup (last resort)
64+
# ---------------------------------------------------------------------------
65+
resolve_npm_bin() {
66+
local candidate=""
67+
68+
# 1) Agent's embedded runtime
69+
local agent_home="/home/${BAUDBOT_AGENT_USER:-baudbot_agent}"
70+
candidate="$(bb_resolve_runtime_node_bin_dir "$agent_home" 2>/dev/null || true)"
71+
if [ -n "$candidate" ] && [ -x "$candidate/npm" ]; then
72+
echo "$candidate/npm"
73+
return 0
74+
fi
75+
76+
# 2) SUDO_USER's home (admin's local node install — mise, nvm, etc.)
77+
if [ -n "${SUDO_USER:-}" ] && [ "$SUDO_USER" != "root" ]; then
78+
local sudo_home=""
79+
sudo_home="$(bb_resolve_user_home "$SUDO_USER" 2>/dev/null || true)"
80+
if [ -n "$sudo_home" ]; then
81+
local dir=""
82+
for dir in \
83+
"$sudo_home/.local/share/mise/shims" \
84+
"$sudo_home/.nvm/versions/node"/*/bin \
85+
"$sudo_home/.local/bin"; do
86+
# Skip unexpanded globs.
87+
case "$dir" in *\**) continue ;; esac
88+
if [ -x "$dir/npm" ]; then
89+
echo "$dir/npm"
90+
return 0
91+
fi
92+
done
93+
fi
94+
fi
95+
96+
# 3) PATH (may work in non-sudo environments or CI)
97+
if command -v npm >/dev/null 2>&1; then
98+
command -v npm
99+
return 0
100+
fi
101+
102+
return 1
103+
}
104+
58105
cleanup() {
59106
if [ -n "$CHECKOUT_DIR" ] && [ -d "$CHECKOUT_DIR" ]; then
60107
rm -rf "$CHECKOUT_DIR"
@@ -221,14 +268,11 @@ install_release_bridge_dependencies() {
221268
log "installing production Slack bridge dependencies in release"
222269
rm -rf "$bridge_dir/node_modules"
223270

224-
# Resolve npm from the agent's embedded node runtime, falling back to PATH.
225-
local npm_bin="npm"
226-
local agent_home="/home/${BAUDBOT_AGENT_USER:-baudbot_agent}"
227-
local node_bin_dir=""
228-
node_bin_dir="$(bb_resolve_runtime_node_bin_dir "$agent_home" 2>/dev/null || true)"
229-
if [ -n "$node_bin_dir" ] && [ -x "$node_bin_dir/npm" ]; then
230-
npm_bin="$node_bin_dir/npm"
231-
fi
271+
# Resolve npm via the embedded Node runtime. update-release runs as root
272+
# (sudo) so bare `npm` / `node` will not be on PATH — we must resolve the
273+
# full path. Try: agent home → SUDO_USER home → PATH (last resort).
274+
local npm_bin=""
275+
npm_bin="$(resolve_npm_bin)" || die "cannot find npm; install Node for the agent (see setup.sh) or ensure npm is on PATH"
232276

233277
if [ -f "$bridge_dir/package-lock.json" ]; then
234278
(cd "$bridge_dir" && "$npm_bin" ci --omit=dev)

bin/update-release.test.sh

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,104 @@ test_release_root_overrides_stale_source_path_env() {
214214
)
215215
}
216216

217+
test_resolve_npm_from_fake_agent_home() {
218+
(
219+
set -euo pipefail
220+
local tmp fake_home npm_path
221+
222+
tmp="$(mktemp -d /tmp/baudbot-update-test.XXXXXX)"
223+
trap 'rm -rf "$tmp"' EXIT
224+
225+
# Create a fake embedded node runtime layout.
226+
fake_home="$tmp/home/baudbot_agent"
227+
mkdir -p "$fake_home/opt/node/bin"
228+
printf '#!/bin/sh\necho fake-npm\n' > "$fake_home/opt/node/bin/npm"
229+
chmod +x "$fake_home/opt/node/bin/npm"
230+
# Create a matching node binary so bb_resolve_runtime_node_bin succeeds.
231+
printf '#!/bin/sh\ntrue\n' > "$fake_home/opt/node/bin/node"
232+
chmod +x "$fake_home/opt/node/bin/node"
233+
234+
# Source the helpers and the resolve_npm_bin function from the script.
235+
# We extract the function by sourcing with BAUDBOT_AGENT_USER set so the
236+
# resolution targets our fake home.
237+
npm_path="$(
238+
source "$REPO_ROOT/bin/lib/shell-common.sh"
239+
source "$REPO_ROOT/bin/lib/paths-common.sh"
240+
source "$REPO_ROOT/bin/lib/runtime-node.sh"
241+
source "$REPO_ROOT/bin/lib/release-common.sh"
242+
source "$REPO_ROOT/bin/lib/release-runtime-common.sh"
243+
source "$REPO_ROOT/bin/lib/json-common.sh"
244+
245+
# Define the function inline (extracted from update-release.sh).
246+
resolve_npm_bin() {
247+
local candidate=""
248+
local agent_home="/home/${BAUDBOT_AGENT_USER:-baudbot_agent}"
249+
candidate="$(bb_resolve_runtime_node_bin_dir "$agent_home" 2>/dev/null || true)"
250+
if [ -n "$candidate" ] && [ -x "$candidate/npm" ]; then
251+
echo "$candidate/npm"
252+
return 0
253+
fi
254+
if command -v npm >/dev/null 2>&1; then
255+
command -v npm
256+
return 0
257+
fi
258+
return 1
259+
}
260+
261+
BAUDBOT_AGENT_USER="baudbot_agent"
262+
BAUDBOT_HOME="$fake_home"
263+
# Point the resolution at our fake tree.
264+
BAUDBOT_RUNTIME_NODE_BIN_DIR="$fake_home/opt/node/bin"
265+
resolve_npm_bin
266+
)"
267+
268+
[ "$npm_path" = "$fake_home/opt/node/bin/npm" ]
269+
)
270+
}
271+
272+
test_resolve_npm_fails_when_missing() {
273+
(
274+
set -euo pipefail
275+
local tmp
276+
277+
tmp="$(mktemp -d /tmp/baudbot-update-test.XXXXXX)"
278+
trap 'rm -rf "$tmp"' EXIT
279+
280+
# Empty fake home — no node installed anywhere.
281+
local result=0
282+
(
283+
source "$REPO_ROOT/bin/lib/shell-common.sh"
284+
source "$REPO_ROOT/bin/lib/paths-common.sh"
285+
source "$REPO_ROOT/bin/lib/runtime-node.sh"
286+
287+
resolve_npm_bin() {
288+
local candidate=""
289+
local agent_home="$tmp/home/nobody"
290+
candidate="$(bb_resolve_runtime_node_bin_dir "$agent_home" 2>/dev/null || true)"
291+
if [ -n "$candidate" ] && [ -x "$candidate/npm" ]; then
292+
echo "$candidate/npm"
293+
return 0
294+
fi
295+
# Don't check PATH here — we want to verify the function fails.
296+
return 1
297+
}
298+
299+
resolve_npm_bin
300+
) && result=1
301+
302+
[ "$result" -eq 0 ]
303+
)
304+
}
305+
217306
echo "=== update-release tests ==="
218307
echo ""
219308

220309
run_test "publishes git-free release snapshot" test_publish_git_free_release
221310
run_test "preflight failure keeps current release" test_preflight_failure_keeps_current
222311
run_test "deploy failure keeps current release" test_deploy_failure_keeps_current
223312
run_test "release root overrides stale source env" test_release_root_overrides_stale_source_path_env
313+
run_test "resolves npm from agent embedded runtime" test_resolve_npm_from_fake_agent_home
314+
run_test "resolve_npm_bin fails when npm missing" test_resolve_npm_fails_when_missing
224315

225316
echo ""
226317
echo "=== $PASSED/$TOTAL passed, $FAILED failed ==="

0 commit comments

Comments
 (0)