Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,15 @@ During installation, the add-on copies Drupal.org GitLab CI template default
config files into the project root. If conflicts are detected, you can choose to
back up and replace, skip, or abort. Skipping a config may diverge from the
Drupal.org GitLab CI template defaults. The installer will prompt for:
- Accept recommended settings (default: no). Choosing yes applies the
- A pre-prompt summary of recommended defaults.
- Accept recommended settings (default: yes). Press Enter to apply the
recommended defaults without further prompts.
- Conflict handling (default: skip unless you choose replace/abort).
- PHP tooling dependencies (install `drupal/core-dev` or run `ddev composer install`).
- PHPStan default level (keep GitLab CI template level 0 or choose a local level 0-10; recommend 3).
- Node toolchain install in the project root and package manager selection.
- Missing Drupal JS dependencies when a root `package.json` exists.
- Optional `.gitignore` update for `dcq-reports/`.
- Optional `.gitignore` update for `dcq-reports/` (default: yes).
- IDE settings (merge/overwrite/skip when templates are available).
The installer runs in bash so it does not require host PHP.

Expand Down Expand Up @@ -217,7 +218,8 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J
to skip, or unset to prompt (default: install in the project root). The
installer selects npm/yarn based on existing lockfiles.
- `DCQ_INSTALL_GITIGNORE`: `add`/`true` to add `dcq-reports/` to `.gitignore`
without prompting, `skip`/`false` to skip, or unset to prompt when interactive.
without prompting, `skip`/`false` to skip, or unset to prompt when interactive
(default: yes).
- `DCQ_INSTALL_IDE_SETTINGS`: `merge` to add missing VS Code settings and
extension recommendations, `overwrite` to back up and replace, `skip` to
handle manually, or unset to prompt.
Expand Down
109 changes: 74 additions & 35 deletions dcq-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -509,7 +509,7 @@ prompt_node_install_action() {

emit 'ESLint, Prettier, and Stylelint require several packages to function properly.\n'
emit '\n'
emit '[i]nstall these in the project root, [s]kip node module installation (default: install): '
emit '[i]nstall in the project root, [s]kip (default: install): '
if ! IFS= read -r -u "$PROMPT_IN_FD" choice; then
choice=""
fi
Expand Down Expand Up @@ -602,26 +602,32 @@ prompt_yes_no() {
}

prompt_recommended_settings() {
# Prompt for accepting recommended settings (default: no).
# Prompt for accepting recommended settings (default: yes).
if [ "${PROMPT_AVAILABLE:-0}" -ne 1 ]; then
return 1
fi

printf 'Accept recommended settings? (y/N) ' >&"$PROMPT_OUT_FD"
local answer=""
if ! IFS= read -r -u "$PROMPT_IN_FD" answer; then
answer=""
fi
answer="$(string_lower "$answer")"
if [ -z "$answer" ]; then
return 1
if prompt_yes_no "Accept recommended settings for this install?" 0; then
return 0
fi
case "$answer" in
y|yes) return 0 ;;
esac
return 1
}

print_recommended_settings_summary() {
# Show what the one-step recommended install will do before prompting.
if [ "${PROMPT_AVAILABLE:-0}" -ne 1 ]; then
return
fi

emit '\nRecommended defaults for this install:\n'
emit ' - If a copied config file already exists, the installer will create a backup and then replace the file.\n'
emit ' - Install missing PHP tooling via drupal/core-dev, including PHPStan, PHPCS, PHPCBF, and Drupal coding standards.\n'
emit ' - Install Node tooling in the project root, including ESLint, Stylelint, Prettier, and CSpell dependencies.\n'
emit ' - Set phpstan.neon level to 3 as a practical local default.\n'
emit ' - Merge DCQ VS Code/Codium settings into .vscode/settings.json and extension recommendations into .vscode/extensions.json.\n'
emit " - Add 'dcq-reports/' to .gitignore so generated check logs and patch previews are not committed.\n"
}

set_default_env() {
local name="$1"
local value="$2"
Expand Down Expand Up @@ -809,7 +815,7 @@ maybe_add_gitignore_reports() {

emit 'Add %s to .gitignore to avoid committing report logs.\n' "$entry"
printf '\n'
if prompt_yes_no "Add '${entry}' to .gitignore?" 1; then
if prompt_yes_no "Add '${entry}' to .gitignore?" 0; then
if [ -f "$gitignore" ]; then
printf '\n%s\n' "$entry" >>"$gitignore"
else
Expand Down Expand Up @@ -929,6 +935,11 @@ expand_cspell_config() {
local ddev_approot="${DDEV_APPROOT:-$app_root}"
local prepare_script="${ddev_approot}/.ddev/drupal-code-quality/tooling/scripts/prepare-cspell.php"
local cspell_config="${app_root%/}/.cspell.json"
local docroot="${DCQ_DOCROOT:-web}"
local ddev_cmd="${DDEV_EXECUTABLE:-ddev}"
local container_cspell="/var/www/html/.cspell.json"
local container_core_cspell_dir="/var/www/html/${docroot}/core/misc/cspell"
local container_prepare_script="/mnt/ddev_config/drupal-code-quality/tooling/scripts/prepare-cspell.php"


# Check if CSpell config exists
Expand All @@ -944,28 +955,36 @@ expand_cspell_config() {
fi

# Run prepare-cspell.php in the container to expand .cspell.json
if command_available "${DDEV_EXECUTABLE:-ddev}"; then
emit 'Expanding .cspell.json with project-specific settings...\n'
local ddev_cmd="${DDEV_EXECUTABLE:-ddev}"

# Run the script in container from project root
# Capture both stdout and stderr, but don't fail the installer if it errors
# Pass the docroot via _WEB_ROOT environment variable
local output
if output=$("$ddev_cmd" exec bash -c "cd /var/www/html && export _WEB_ROOT='${DCQ_DOCROOT:-web}' && php .ddev/drupal-code-quality/tooling/scripts/prepare-cspell.php" 2>&1); then
if echo "$output" | grep -q "Writing json"; then
emit 'Successfully expanded .cspell.json\n'
else
emit 'CSpell expansion completed (no changes needed)\n'
fi
if ! command_available "$ddev_cmd"; then
emit 'DDEV not available; skipping CSpell expansion.\n'
return 0
fi

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

if ! wait_for_container_file "$ddev_cmd" "$container_cspell" 20 1; then
emit 'Skipping CSpell expansion (.cspell.json did not become visible in the container after waiting).\n'
return 0
fi

if ! "$ddev_cmd" exec test -f "${container_core_cspell_dir}/dictionary.txt" >/dev/null 2>&1 \
|| ! "$ddev_cmd" exec test -f "${container_core_cspell_dir}/drupal-dictionary.txt" >/dev/null 2>&1; then
emit 'Skipping CSpell expansion (Drupal core dictionary files are not available at %s).\n' "${docroot}/core/misc/cspell"
return 0
fi

local output
if output=$("$ddev_cmd" exec bash -lc "cd /var/www/html && export _WEB_ROOT='${docroot}' _CSPELL_DICTIONARY='.cspell-project-words.txt' && php '${container_prepare_script}'" 2>&1); then
if echo "$output" | grep -q "Writing json"; then
emit 'Successfully expanded .cspell.json\n'
else
emit 'Skipping CSpell expansion: prepare-cspell.php failed.\n'
if [ -n "$output" ]; then
emit '%s\n' "$output"
fi
emit 'CSpell expansion completed (no changes needed)\n'
fi
else
emit 'DDEV not available; skipping CSpell expansion.\n'
emit 'Skipping CSpell expansion (prepare-cspell.php failed).\n'
if truthy "${DCQ_VERBOSE:-0}" && [ -n "$output" ]; then
emit 'CSpell expansion error output:\n%s\n' "$output"
fi
fi

# Always return success - CSpell expansion is optional
Expand Down Expand Up @@ -1025,6 +1044,23 @@ command_available() {
command -v "$1" >/dev/null 2>&1
}

wait_for_container_file() {
local ddev_cmd="$1"
local path="$2"
local max_attempts="${3:-20}"
local delay_seconds="${4:-1}"
local attempts=0

while [ "$attempts" -lt "$max_attempts" ]; do
if "$ddev_cmd" exec test -f "$path" >/dev/null 2>&1; then
return 0
fi
attempts=$((attempts + 1))
sleep "$delay_seconds"
done
return 1
}

detect_package_manager() {
local dir="$1"
local has_yarn=0
Expand Down Expand Up @@ -1616,8 +1652,11 @@ fi
# In non-interactive mode, or if user accepts, set recommended defaults for any unset vars.
recommended_mode=0
if [ "$non_interactive" -eq 0 ] && [ "${PROMPT_AVAILABLE:-0}" -eq 1 ]; then
print_recommended_settings_summary
if prompt_recommended_settings; then
recommended_mode=1
else
emit 'Using manual mode. You will be prompted for each setting.\n'
fi
fi

Expand Down Expand Up @@ -1711,9 +1750,9 @@ if [ "${#missing_tools[@]}" -gt 0 ]; then
fi

if [ "$action" = "install" ]; then
question="Recommend installing PHP dev tools from composer.lock. Proceed?"
question="Install PHP dev tools from composer.lock now?"
else
question="Recommend installing drupal/core-dev as a dev dependency to install PHP code quality tools and Drupal coding standards. Proceed?"
question="Install drupal/core-dev now to provide PHP code quality tools and Drupal coding standards?"
fi

should_install=0
Expand Down
24 changes: 12 additions & 12 deletions tests/test-installer-deps-prompt.bats
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,8 @@ env.pop("DDEV_NONINTERACTIVE", None)

cmd = ["bash", os.path.join(addon_root, "dcq-install.sh")]
prompts = {
b"Accept recommended settings? (y/N)": b"n\n",
b"Recommend installing drupal/core-dev as a dev dependency": b"n\n",
b"Accept recommended settings for this install? [Y/n]": b"n\n",
b"Install drupal/core-dev now to provide PHP code quality tools": b"n\n",
}

seen = set()
Expand Down Expand Up @@ -141,8 +141,8 @@ code = os.waitstatus_to_exitcode(status)
sys.exit(code)
PY
[ "$status" -eq 0 ]
[[ "$output" == *"Accept recommended settings? (y/N)"* ]]
[[ "$output" == *"Recommend installing drupal/core-dev as a dev dependency"* ]]
[[ "$output" == *"Accept recommended settings for this install? [Y/n]"* ]]
[[ "$output" == *"Install drupal/core-dev now to provide PHP code quality tools"* ]]
[[ "$output" == *"Project root tooling configs updated"* ]]

run_search "^composer require --dev drupal/core-dev --with-all-dependencies$" "${DDEV_STUB_LOG}"
Expand Down Expand Up @@ -179,7 +179,7 @@ env.pop("DDEV_NONINTERACTIVE", None)

cmd = ["bash", os.path.join(addon_root, "dcq-install.sh")]
prompts = {
b"Accept recommended settings? (y/N)": b"y\n",
b"Accept recommended settings for this install? [Y/n]": b"y\n",
}

seen = set()
Expand Down Expand Up @@ -226,7 +226,7 @@ code = os.waitstatus_to_exitcode(status)
sys.exit(code)
PY
[ "$status" -eq 0 ]
[[ "$output" == *"Accept recommended settings? (y/N)"* ]]
[[ "$output" == *"Accept recommended settings for this install? [Y/n]"* ]]

run_search "^composer config --no-plugins allow-plugins\\.tbachert/spi false$" "${DDEV_STUB_LOG}"
[ "$status" -eq 0 ]
Expand Down Expand Up @@ -288,7 +288,7 @@ env.pop("DDEV_NONINTERACTIVE", None)

cmd = ["bash", os.path.join(addon_root, "dcq-install.sh")]
prompts = {
b"Accept recommended settings? (y/N)": b"n\n",
b"Accept recommended settings for this install? [Y/n]": b"n\n",
b"VS Code/Codium settings/extensions: choose merge, overwrite (with backup), or skip.": b"\n",
}

Expand Down Expand Up @@ -336,7 +336,7 @@ code = os.waitstatus_to_exitcode(status)
sys.exit(code)
PY
[ "$status" -eq 0 ]
[[ "$output" == *"Accept recommended settings? (y/N)"* ]]
[[ "$output" == *"Accept recommended settings for this install? [Y/n]"* ]]
[[ "$output" == *"VS Code/Codium settings/extensions: choose merge, overwrite (with backup), or skip."* ]]
[[ "$output" == *"Skipping IDE settings/extensions install."* ]]
[ ! -f "${APP_ROOT}/.vscode/settings.json" ]
Expand Down Expand Up @@ -421,11 +421,11 @@ for key in (

cmd = ["bash", os.path.join(addon_root, "dcq-install.sh")]
prompts = {
b"Accept recommended settings? (y/N)": b"n\n",
b"Recommend installing drupal/core-dev as a dev dependency": b"n\n",
b"Accept recommended settings for this install? [Y/n]": b"n\n",
b"Install drupal/core-dev now to provide PHP code quality tools": b"n\n",
b"Conflict at ": b"s\n",
b"Set phpstan.neon level (0-10) (default: 0):": b"0\n",
b"[i]nstall these in the project root, [s]kip node module installation (default: install):": b"s\n",
b"[i]nstall in the project root, [s]kip (default: install):": b"s\n",
b"Add 'dcq-reports/' to .gitignore?": b"n\n",
b"VS Code/Codium settings/extensions: choose merge, overwrite (with backup), or skip.": b"\n",
}
Expand Down Expand Up @@ -474,7 +474,7 @@ code = os.waitstatus_to_exitcode(status)
sys.exit(code)
PY
[ "$status" -eq 0 ]
[[ "$output" == *"Recommend installing drupal/core-dev as a dev dependency"* ]]
[[ "$output" == *"Install drupal/core-dev now to provide PHP code quality tools"* ]]
[[ "$output" == *"Conflict at"* ]]
[[ "$output" == *"ESLint, Prettier, and Stylelint require several packages to function properly."* ]]
[[ "$output" == *"Add 'dcq-reports/' to .gitignore?"* ]]
Expand Down
37 changes: 28 additions & 9 deletions tests/test.bats
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ teardown() {
health_checks
}

@test "installer prompt accepts recommended settings" {
@test "installer prompt defaults to recommended settings" {
set -u -o pipefail
unset DCQ_NONINTERACTIVE
unset DDEV_NONINTERACTIVE
Expand All @@ -659,7 +659,7 @@ import select
import sys

cmd = ["ddev", "add-on", "get", "${DIR}"]
prompt = b"Accept recommended settings? (y/N)"
prompt = b"Accept recommended settings for this install? [Y/n]"
sent = False
buf = b""

Expand All @@ -678,7 +678,7 @@ try:
sys.stdout.buffer.flush()
buf += data
if (not sent) and (prompt in buf):
os.write(fd, b"y\\n")
os.write(fd, b"\\n")
sent = True
except OSError:
pass
Expand All @@ -688,7 +688,8 @@ code = os.waitstatus_to_exitcode(status)
sys.exit(code)
PY
assert_success
assert_output --partial "Accept recommended settings? (y/N)"
assert_output --partial "Recommended defaults for this install:"
assert_output --partial "Accept recommended settings for this install? [Y/n]"
assert_phpstan_level "3"
if command -v rg >/dev/null 2>&1; then
run rg -n "^dcq-reports/$" ".gitignore"
Expand Down Expand Up @@ -730,8 +731,8 @@ import sys
cmd = ["ddev", "add-on", "get", "${DIR}"]
# Answer 'n' to recommended settings, then respond to individual prompts
responses = {
b"Accept recommended settings? (y/N)": b"n\n",
b"Recommend installing drupal/core-dev as a dev dependency": b"n\n",
b"Accept recommended settings for this install? [Y/n]": b"n\n",
b"Install drupal/core-dev now to provide PHP code quality tools": b"n\n",
b"back up and replace, skip, or abort? [replace/skip/abort]": b"skip\n",
b"Install Node toolchain": b"n\n",
b"PHPStan level": b"0\n",
Expand Down Expand Up @@ -771,11 +772,13 @@ code = os.waitstatus_to_exitcode(status)
sys.exit(code)
PY
assert_success
assert_output --partial "Accept recommended settings? (y/N)"
assert_output --partial "Recommended defaults for this install:"
assert_output --partial "Accept recommended settings for this install? [Y/n]"
# Verify individual prompts appeared (user declined recommended settings)
assert_output --partial "Recommend installing drupal/core-dev as a dev dependency"
assert_output --partial "Install drupal/core-dev now to provide PHP code quality tools"
assert_output --partial "VS Code/Codium settings/extensions: choose merge, overwrite (with backup), or skip."
assert_output --partial "Skipping IDE settings/extensions install."
assert_output --partial "Add 'dcq-reports/' to .gitignore? [Y/n]"
assert_output --partial "Set phpstan.neon level"
# PHPStan level should be 0 (not the recommended 3) since user chose it
assert_phpstan_level "0"
Expand Down Expand Up @@ -1039,8 +1042,15 @@ PHP

@test "cspell config is expanded during installation" {
set -u -o pipefail
run ddev add-on get "${DIR}"

# Provide minimal Drupal core dictionary files so expansion can run.
run ddev exec bash -lc 'mkdir -p /var/www/html/web/core/misc/cspell && printf "drupal\n" > /var/www/html/web/core/misc/cspell/drupal-dictionary.txt && printf "dictionary\n" > /var/www/html/web/core/misc/cspell/dictionary.txt'
assert_success

run bash -lc "ddev add-on get \"${DIR}\" 2>&1"
assert_success
assert_output --partial "Expanding .cspell.json with project-specific settings..."
assert_output --partial "Successfully expanded .cspell.json"

# Verify expanded dictionaries array includes Drupal and project-words
run grep -q '"drupal"' .cspell.json
Expand Down Expand Up @@ -1070,6 +1080,15 @@ PHP
assert_file_exist ".cspell-project-words.txt"
}

@test "cspell expansion explains skip when Drupal core dictionaries are missing" {
set -u -o pipefail

run bash -lc "ddev add-on get \"${DIR}\" 2>&1"
assert_success
assert_output --partial "Expanding .cspell.json with project-specific settings..."
assert_output --partial "Skipping CSpell expansion (Drupal core dictionary files are not available at web/core/misc/cspell)."
}

@test "remove cleans ddev assets and shims" {
set -u -o pipefail
run ddev add-on get "${DIR}"
Expand Down