Skip to content

Commit fc3ef84

Browse files
authored
fix: handle npm install failures gracefully instead of crashing installer (#24)
The installer crashed with exit status 1 when npm install failed during Phase 3, because unguarded run_command calls allowed set -e to abort the entire post-install action. Changes: - Wrap all npm/yarn install calls in error handlers so failures produce helpful retry messages instead of crashing the installer - Replace mapfile with a bash 3.2-compatible while-read loop (macOS ships bash 3.2 which lacks mapfile, causing an unbound variable crash) - Use test -s instead of test -f when waiting for .cspell.json in the container, so the wait detects empty files from partial filesystem sync - Only emit "Node toolchain installed" when installation actually succeeds Fixes #23
1 parent c2d4206 commit fc3ef84

2 files changed

Lines changed: 86 additions & 12 deletions

File tree

dcq-install.sh

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,11 @@ maybe_install_missing_root_deps() {
435435
return 1
436436
fi
437437

438-
mapfile -t missing_node_deps_array <<< "$missing_node_deps"
438+
# Avoid mapfile (requires bash 4+; macOS ships bash 3.2).
439+
missing_node_deps_array=()
440+
while IFS= read -r _line; do
441+
[ -n "$_line" ] && missing_node_deps_array+=("$_line")
442+
done <<< "$missing_node_deps"
439443
if [ "$suppress_list" -ne 1 ]; then
440444
emit 'Detected missing Drupal JS tooling dependencies in package.json (%d):\n' "${#missing_node_deps_array[@]}"
441445
for dep in "${missing_node_deps_array[@]}"; do
@@ -467,13 +471,22 @@ maybe_install_missing_root_deps() {
467471
done
468472
if [ "$package_manager" = "npm" ]; then
469473
cmd=( "$ddev_cmd" "exec" "bash" "-lc" "cd /var/www/html && npm install --save-dev --package-lock${deps_cmd}" )
470-
run_command "${cmd[@]}"
474+
if ! run_command "${cmd[@]}"; then
475+
emit 'npm install --save-dev failed. You can retry manually.\n'
476+
return 1
477+
fi
471478
emit 'Node dependencies added (project root).\n'
472479
cmd=( "$ddev_cmd" "exec" "bash" "-lc" "cd /var/www/html && npm install --package-lock" )
473-
run_command "${cmd[@]}"
480+
if ! run_command "${cmd[@]}"; then
481+
emit 'npm install failed. You can retry manually.\n'
482+
return 1
483+
fi
474484
else
475485
cmd=( "$ddev_cmd" "exec" "bash" "-lc" "cd /var/www/html && yarn add -D${deps_cmd}" )
476-
run_command "${cmd[@]}"
486+
if ! run_command "${cmd[@]}"; then
487+
emit 'yarn add failed. You can retry manually.\n'
488+
return 1
489+
fi
477490
emit 'Node dependencies added (project root).\n'
478491
fi
479492
return 0
@@ -996,8 +1009,8 @@ expand_cspell_config() {
9961009

9971010
emit 'Expanding .cspell.json with project-specific settings...\n'
9981011

999-
if ! wait_for_container_file "$ddev_cmd" "$container_cspell" 20 1; then
1000-
emit 'Skipping CSpell expansion (.cspell.json did not become visible in the container after waiting).\n'
1012+
if ! wait_for_container_file "$ddev_cmd" "$container_cspell" 20 1 "readable"; then
1013+
emit 'Skipping CSpell expansion (.cspell.json not readable in the container after waiting).\n'
10011014
return 0
10021015
fi
10031016

@@ -1079,14 +1092,23 @@ command_available() {
10791092
}
10801093

10811094
wait_for_container_file() {
1095+
# Wait for a file to appear in the container. When require_readable is set
1096+
# to a non-empty value the check additionally verifies that the file has
1097+
# non-zero size (test -s), which guards against filesystem-sync race
1098+
# conditions where the inode is visible but the content has not landed yet.
10821099
local ddev_cmd="$1"
10831100
local path="$2"
10841101
local max_attempts="${3:-20}"
10851102
local delay_seconds="${4:-1}"
1103+
local require_readable="${5:-}"
10861104
local attempts=0
1105+
local test_flag="-f"
1106+
if [ -n "$require_readable" ]; then
1107+
test_flag="-s"
1108+
fi
10871109

10881110
while [ "$attempts" -lt "$max_attempts" ]; do
1089-
if "$ddev_cmd" exec test -f "$path" >/dev/null 2>&1; then
1111+
if "$ddev_cmd" exec test "$test_flag" "$path" >/dev/null 2>&1; then
10901112
return 0
10911113
fi
10921114
attempts=$((attempts + 1))
@@ -2215,9 +2237,16 @@ if [ "$core_package_json_present" -eq 1 ]; then
22152237
else
22162238
cmd=( "$ddev_cmd" "exec" "bash" "-lc" "cd /var/www/html && yarn install" )
22172239
fi
2218-
run_command "${cmd[@]}"
2240+
if run_command "${cmd[@]}"; then
2241+
node_install_done=1
2242+
else
2243+
emit 'JS dependency install failed. You can retry manually:\n'
2244+
emit ' ddev exec bash -lc "cd /var/www/html && %s install"\n' "${root_pm:-npm}"
2245+
fi
2246+
fi
2247+
if [ "$node_install_done" -eq 1 ]; then
2248+
emit 'Node toolchain installed (project root).\n'
22192249
fi
2220-
emit 'Node toolchain installed (project root).\n'
22212250
fi
22222251
fi
22232252
fi

tests/test.bats

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1360,6 +1360,20 @@ SH
13601360
assert_output --partial "Skipping CSpell expansion (Drupal core dictionary files are not available at web/core/misc/cspell)."
13611361
}
13621362

1363+
@test "container test -s rejects empty files and accepts non-empty files" {
1364+
# Validates the mechanism behind wait_for_container_file's readability check.
1365+
# An empty file (simulating a partially-synced inode) must fail test -s so
1366+
# that the installer waits for content to arrive rather than handing a zero-
1367+
# byte file to prepare-cspell.php.
1368+
set -u -o pipefail
1369+
1370+
run ddev exec bash -lc 'truncate -s 0 /tmp/dcq-empty-test && test -s /tmp/dcq-empty-test'
1371+
assert_failure
1372+
1373+
run ddev exec bash -lc 'printf "{}" > /tmp/dcq-nonempty-test && test -s /tmp/dcq-nonempty-test'
1374+
assert_success
1375+
}
1376+
13631377
@test "remove cleans ddev assets and shims" {
13641378
set -u -o pipefail
13651379
run ddev add-on get "${DIR}"
@@ -1401,7 +1415,7 @@ SH
14011415

14021416
run ddev add-on get "${DIR}"
14031417
assert_success
1404-
assert_output --partial "Node dependencies added (project root)."
1418+
assert_output --partial "Node toolchain installed (project root)."
14051419
assert_container_file_exist "/var/www/html/package-lock.json"
14061420
assert_container_file_exist "/var/www/html/node_modules/eslint-plugin-no-jquery/package.json"
14071421
assert_container_file_exist "/var/www/html/node_modules/stylelint-prettier/package.json"
@@ -1424,7 +1438,7 @@ SH
14241438

14251439
run ddev add-on get "${DIR}"
14261440
assert_success
1427-
assert_output --partial "Node dependencies added (project root)."
1441+
assert_output --partial "Node toolchain installed (project root)."
14281442
assert_container_file_exist "/var/www/html/yarn.lock"
14291443
assert_container_file_exist "/var/www/html/node_modules/eslint-plugin-no-jquery/package.json"
14301444
assert_container_file_exist "/var/www/html/node_modules/stylelint-prettier/package.json"
@@ -1441,12 +1455,43 @@ SH
14411455

14421456
run ddev add-on get "${DIR}"
14431457
assert_success
1444-
assert_output --partial "Node dependencies added (project root)."
1458+
assert_output --partial "Node toolchain installed (project root)."
14451459
assert_container_file_exist "/var/www/html/package-lock.json"
14461460
assert_container_file_exist "/var/www/html/node_modules/eslint-plugin-no-jquery/package.json"
14471461
assert_container_file_exist "/var/www/html/node_modules/stylelint-prettier/package.json"
14481462
}
14491463

1464+
# bats test_tags=node
1465+
@test "node install failure does not crash the installer" {
1466+
set -u -o pipefail
1467+
export DCQ_INSTALL_DEPS=skip
1468+
export DCQ_INSTALL_NODE_DEPS=root
1469+
mkdir -p web/core
1470+
write_stub_package_json "web/core/package.json"
1471+
1472+
# Pre-create a root package.json that includes the same deps as the core
1473+
# stub (so find_missing_node_deps returns nothing and we reach the fallback
1474+
# npm-install path) PLUS an unresolvable dependency so npm install fails.
1475+
# The installer should handle this gracefully instead of crashing the
1476+
# entire post-install action via set -e.
1477+
cat > package.json <<'JSON'
1478+
{
1479+
"name": "dcq-fail-test",
1480+
"private": true,
1481+
"devDependencies": {
1482+
"eslint-plugin-no-jquery": "^3.1.1",
1483+
"stylelint-prettier": "^5.0.3",
1484+
"this-package-should-not-exist-dcq-test": "99999.0.0"
1485+
}
1486+
}
1487+
JSON
1488+
1489+
run bash -lc "ddev add-on get \"${DIR}\" 2>&1"
1490+
assert_success
1491+
assert_output --partial "JS dependency install failed."
1492+
assert_output --partial "You can retry manually"
1493+
}
1494+
14501495
# bats test_tags=full
14511496
@test "fresh install (full)" {
14521497
set -eu -o pipefail

0 commit comments

Comments
 (0)