From 9c8f84f24b983210aaef0526a043e95eeb5c595b Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Tue, 17 Feb 2026 22:50:18 -0800 Subject: [PATCH 1/5] Set PHPCS default scope to docroot and test docroot merge --- drupal-code-quality/config-amendments/.phpcs.dcq.xml | 5 +++++ tests/test.bats | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/drupal-code-quality/config-amendments/.phpcs.dcq.xml b/drupal-code-quality/config-amendments/.phpcs.dcq.xml index da933ce..6338f45 100644 --- a/drupal-code-quality/config-amendments/.phpcs.dcq.xml +++ b/drupal-code-quality/config-amendments/.phpcs.dcq.xml @@ -4,5 +4,10 @@ + + __DOCROOT__ + + __DOCROOT__/core/** + **/contrib/** **/node_modules/** __DOCROOT__/sites/*/files/** diff --git a/tests/test.bats b/tests/test.bats index 052b9e9..3ede975 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -934,8 +934,16 @@ PY assert_success run grep -n "web/core" ".cspell.json" assert_failure + run grep -n '' ".phpcs.xml" + assert_success + run grep -n "docroot" ".phpcs.xml" + assert_success + run grep -n "docroot/core/\\*\\*" ".phpcs.xml" + assert_success run grep -n "docroot/sites" ".phpcs.xml" assert_success + run grep -n "__DOCROOT__" ".phpcs.xml" + assert_failure # Verify PHPStan config uses custom docroot run grep -q "docroot/modules/custom" phpstan.neon From d946a974900540e229ce759922745b8b06e45ea8 Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Tue, 24 Mar 2026 07:46:34 -0500 Subject: [PATCH 2/5] Add upstream config sync script, detection accuracy tests, and volume cleanup Add a maintainer script that fetches the latest config files from Drupal core and GitLab CI templates, compares them against local assets, and optionally applies updates. A weekly GitHub Actions workflow detects upstream drift and opens an issue when changes are found. Expand the full install test with detection accuracy assertions for every wrapper command: true positives (tools catch known violations), true negatives (clean code passes), and exclusion path tests (contrib code is not flagged). Clean up Docker volumes (mariadb, mutagen, snapshots) in test teardown to prevent accumulation across test runs. --- .github/workflows/upstream-config-check.yml | 87 +++++++ README.md | 25 ++ scripts/sync-upstream-configs.sh | 255 ++++++++++++++++++++ tests/test.bats | 149 ++++++++++++ 4 files changed, 516 insertions(+) create mode 100644 .github/workflows/upstream-config-check.yml create mode 100755 scripts/sync-upstream-configs.sh diff --git a/.github/workflows/upstream-config-check.yml b/.github/workflows/upstream-config-check.yml new file mode 100644 index 0000000..5b06e6c --- /dev/null +++ b/.github/workflows/upstream-config-check.yml @@ -0,0 +1,87 @@ +name: upstream-config-check + +on: + schedule: + # Weekly on Mondays at 09:15 UTC + - cron: '15 09 * * 1' + + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + check-upstream: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Check upstream configs + id: sync + run: | + output=$(./scripts/sync-upstream-configs.sh 2>&1) || exit_code=$? + echo "$output" + # Save output for issue body (escape for GH Actions) + { + echo "SYNC_OUTPUT<> "$GITHUB_ENV" + echo "exit_code=${exit_code:-0}" >> "$GITHUB_OUTPUT" + + - name: Create or update issue on changes + if: steps.sync.outputs.exit_code == '1' + uses: actions/github-script@v7 + with: + script: | + const title = 'Upstream config changes detected'; + const label = 'upstream-sync'; + const body = `The weekly upstream config check found changes.\n\n\`\`\`\n${process.env.SYNC_OUTPUT}\n\`\`\`\n\nRun \`./scripts/sync-upstream-configs.sh --update\` locally to apply, then \`DCQ_FULL_TESTS=1 bats --jobs 4 ./tests/test.bats\` to validate.`; + + // Find existing open issue + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: label, + }); + + if (issues.data.length > 0) { + // Update existing issue + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: issues.data[0].number, + body: body, + }); + console.log(`Updated issue #${issues.data[0].number}`); + } else { + // Ensure label exists + try { + await github.rest.issues.getLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + }); + } catch { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: label, + color: '0075ca', + description: 'Automated upstream config drift detection', + }); + } + + // Create new issue + const issue = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: [label], + }); + console.log(`Created issue #${issue.data.number}`); + } diff --git a/README.md b/README.md index f879030..1bf4eb0 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,31 @@ The template points PHP tooling at `.ddev/drupal-code-quality/tooling/bin` and J extension recommendations, `overwrite` to back up and replace, `skip` to handle manually, or unset to prompt. +## Maintaining upstream configs + +The add-on ships config files sourced from Drupal core and the Drupal GitLab CI +templates. A sync script checks for upstream changes: + +```bash +# Check for upstream config changes (report only) +./scripts/sync-upstream-configs.sh + +# Apply changes to local asset files +./scripts/sync-upstream-configs.sh --update + +# Override upstream branches +./scripts/sync-upstream-configs.sh --branch-core=11.x --branch-templates=main +``` + +After applying updates, run the full test suite to validate configs still work: + +```bash +DCQ_FULL_TESTS=1 bats --jobs 4 ./tests/test.bats +``` + +A weekly GitHub Actions workflow (`upstream-config-check.yml`) automatically +checks for upstream drift and opens an issue when changes are detected. + ## Uninstall Removing the add-on cleans up the `.ddev` payload (commands, shims, assets, diff --git a/scripts/sync-upstream-configs.sh b/scripts/sync-upstream-configs.sh new file mode 100755 index 0000000..09739ca --- /dev/null +++ b/scripts/sync-upstream-configs.sh @@ -0,0 +1,255 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Fetch latest upstream config files from Drupal core and GitLab CI templates, +# compare against local assets, and optionally apply updates. +# +# Usage: +# ./scripts/sync-upstream-configs.sh # Check mode: report diffs +# ./scripts/sync-upstream-configs.sh --update # Apply auto-updateable changes +# ./scripts/sync-upstream-configs.sh --help # Show usage +# +# Options: +# --branch-core=BRANCH Drupal core branch (default: 11.x) +# --branch-templates=BRANCH GitLab templates branch (default: main) + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/.." && pwd)" + +# Defaults +branch_core="11.x" +branch_templates="main" +do_update=false +base_url="https://git.drupalcode.org" + +# Color helpers (respect NO_COLOR) +if [[ -z "${NO_COLOR:-}" ]] && [[ -t 1 ]]; then + RED='\033[0;31m' + GREEN='\033[0;32m' + YELLOW='\033[0;33m' + BOLD='\033[1m' + RESET='\033[0m' +else + RED='' GREEN='' YELLOW='' BOLD='' RESET='' +fi + +usage() { + cat <&2; exit 2 ;; + esac +done + +# Temp directory with cleanup +tmp_dir="$(mktemp -d)" +trap 'rm -rf "$tmp_dir"' EXIT + +# Counters +total=0 +up_to_date=0 +changed=0 +updated=0 +fetch_errors=0 +patch_errors=0 + +# --- File manifest --- +# Format: local_path|upstream_url|transform|header_extra +# transform: none, phpstan, prepare-cspell +# header_extra: additional header line after #ddev-generated (empty for most) + +declare -a MANIFEST=( + # Drupal core files + "drupal-code-quality/assets/.eslintrc.json|${base_url}/project/drupal/-/raw/${branch_core}/core/.eslintrc.json|none|" + "drupal-code-quality/assets/.eslintrc.passing.json|${base_url}/project/drupal/-/raw/${branch_core}/core/.eslintrc.passing.json|none|" + "drupal-code-quality/assets/.eslintrc.jquery.json|${base_url}/project/drupal/-/raw/${branch_core}/core/.eslintrc.jquery.json|none|" + "drupal-code-quality/assets/.stylelintrc.json|${base_url}/project/drupal/-/raw/${branch_core}/core/.stylelintrc.json|none|" + "drupal-code-quality/assets/.prettierrc.json|${base_url}/project/drupal/-/raw/${branch_core}/core/.prettierrc.json|none|" + # GitLab templates files + "drupal-code-quality/assets/phpstan.neon|${base_url}/project/gitlab_templates/-/raw/${branch_templates}/assets/phpstan.neon|phpstan|# Source: ${base_url}/project/gitlab_templates/-/blob/${branch_templates}/assets/phpstan.neon" + "drupal-code-quality/assets/.phpcs.xml|${base_url}/project/gitlab_templates/-/raw/${branch_templates}/assets/phpcs.xml.dist|none|" + "drupal-code-quality/assets/.cspell.json|${base_url}/project/gitlab_templates/-/raw/${branch_templates}/assets/.cspell.json|none|" + # Scripts + "drupal-code-quality/tooling/scripts/prepare-cspell.php|${base_url}/project/gitlab_templates/-/raw/${branch_templates}/scripts/prepare-cspell.php|prepare-cspell|" +) + +# --- Transform functions --- + +# Strip the includes/BASELINE_PLACEHOLDER block from upstream phpstan.neon +transform_phpstan() { + local input="$1" output="$2" + sed -n '/^parameters:/,$p' "$input" > "$output" +} + +# Apply DCQ patch to prepare-cspell.php: replace single-line $non_project_directories +# with our expanded array for custom code scanning. +transform_prepare_cspell() { + local input="$1" output="$2" + if ! grep -q '\$non_project_directories = \["\$webRoot"' "$input"; then + return 1 # Patch target not found + fi + + # Replace the single-line assignment with our expanded multi-line array + sed '/\$non_project_directories = \["\$webRoot",/c\ +// Exclude specific directories but allow scanning custom code\ +$non_project_directories = [\ + "$webRoot/core",\ + "$webRoot/modules/contrib",\ + "$webRoot/themes/contrib",\ + "$webRoot/profiles/contrib",\ + "vendor",\ + "node_modules",\ + ".git",\ + "recipes",\ +];' "$input" > "$output" +} + +# Strip DCQ headers from local file for comparison +strip_dcq_header() { + local file="$1" + # Remove first line if #ddev-generated, then remove next line if # Source: + sed '1{/^#ddev-generated$/d;}' "$file" | sed '1{/^# Source:/d;}' +} + +# --- Main loop --- + +printf "${BOLD}Syncing upstream configs...${RESET}\n" +printf "Core branch: %s | Templates branch: %s\n\n" "$branch_core" "$branch_templates" + +for entry in "${MANIFEST[@]}"; do + IFS='|' read -r local_path upstream_url transform header_extra <<< "$entry" + local_file="${repo_root}/${local_path}" + filename="$(basename "$local_path")" + total=$((total + 1)) + + printf "${BOLD}%-40s${RESET} " "$local_path" + + # Fetch upstream + fetched="${tmp_dir}/${filename}.upstream" + http_code=$(curl --fail --silent --output "$fetched" --write-out '%{http_code}' "$upstream_url" 2>/dev/null || true) + + if [[ ! -f "$fetched" ]] || [[ ! -s "$fetched" ]] || [[ "$http_code" -ge 400 ]]; then + printf "${RED}FETCH FAILED (HTTP %s)${RESET}\n" "$http_code" + fetch_errors=$((fetch_errors + 1)) + continue + fi + + # Apply transform + transformed="${tmp_dir}/${filename}.transformed" + case "$transform" in + phpstan) + transform_phpstan "$fetched" "$transformed" + ;; + prepare-cspell) + if ! transform_prepare_cspell "$fetched" "$transformed"; then + printf "${YELLOW}PATCH TARGET NOT FOUND${RESET}\n" + printf " The upstream file has changed structure. Manual review required.\n" + printf " Upstream: %s\n" "$upstream_url" + patch_errors=$((patch_errors + 1)) + changed=$((changed + 1)) + continue + fi + ;; + none) + cp "$fetched" "$transformed" + ;; + esac + + # Strip DCQ header from local for comparison + if [[ ! -f "$local_file" ]]; then + printf "${RED}LOCAL FILE MISSING${RESET}\n" + changed=$((changed + 1)) + continue + fi + + local_stripped="${tmp_dir}/${filename}.local-stripped" + strip_dcq_header "$local_file" > "$local_stripped" + + # Compare + diff_output=$(diff -u --label "local: ${local_path}" --label "upstream" "$local_stripped" "$transformed" 2>/dev/null || true) + + if [[ -z "$diff_output" ]]; then + printf "${GREEN}up to date${RESET}\n" + up_to_date=$((up_to_date + 1)) + else + printf "${RED}CHANGED${RESET}\n" + changed=$((changed + 1)) + + # Show diff + echo "$diff_output" | head -50 + diff_lines=$(echo "$diff_output" | wc -l) + if [[ "$diff_lines" -gt 50 ]]; then + printf " ... (%d more lines)\n" $((diff_lines - 50)) + fi + echo + + # Apply update if requested + if $do_update; then + { + echo "#ddev-generated" + if [[ -n "$header_extra" ]]; then + echo "$header_extra" + fi + cat "$transformed" + } > "$local_file" + printf " ${GREEN}Updated.${RESET}\n" + updated=$((updated + 1)) + fi + fi +done + +# --- Summary --- +echo +printf "${BOLD}Summary:${RESET} %d checked, %d up to date, %d changed" "$total" "$up_to_date" "$changed" +if [[ "$fetch_errors" -gt 0 ]]; then + printf ", ${RED}%d fetch errors${RESET}" "$fetch_errors" +fi +if [[ "$patch_errors" -gt 0 ]]; then + printf ", ${YELLOW}%d patch conflicts${RESET}" "$patch_errors" +fi +if $do_update && [[ "$updated" -gt 0 ]]; then + printf ", ${GREEN}%d updated${RESET}" "$updated" +fi +echo + +if [[ "$changed" -gt 0 ]] && ! $do_update; then + echo + printf "Run with ${BOLD}--update${RESET} to apply changes to auto-updateable files.\n" +fi + +if [[ "$changed" -gt 0 ]] || { $do_update && [[ "$updated" -gt 0 ]]; }; then + echo + printf "Suggested: ${BOLD}DCQ_FULL_TESTS=1 bats --jobs 4 ./tests/test.bats${RESET}\n" +fi + +# Exit code +if [[ "$fetch_errors" -gt 0 ]]; then + exit 2 +elif [[ "$changed" -gt 0 ]] && ! $do_update; then + exit 1 +else + exit 0 +fi diff --git a/tests/test.bats b/tests/test.bats index 3ede975..504a991 100644 --- a/tests/test.bats +++ b/tests/test.bats @@ -105,6 +105,20 @@ load_bats_helpers() { return 1 fi } + + refute_output() { + if [ "${1:-}" = "--partial" ]; then + local unexpected="${2:-}" + case "$output" in + *"$unexpected"*) echo "Expected output NOT to contain: $unexpected"; return 1 ;; + esac + return 0 + fi + if [ "${output:-}" = "${1:-}" ]; then + echo "Expected output NOT to equal: ${1:-}" + return 1 + fi + } } setup_file() { @@ -578,6 +592,49 @@ CSS a { color: #12; } CSS + # Exclusion path fixture: bad code in contrib that tools should NOT flag. + local contrib_dir="web/modules/contrib/dcq_fake" + mkdir -p "${contrib_dir}" + cat > "${contrib_dir}/dcq_fake.module" <<'PHP' + "${contrib_dir}/bad.js" <<'JS' +const x = "wrong quotes and missing semi" +JS + cat > "${contrib_dir}/bad.css" <<'CSS' +a { color: RED; } +CSS + + # Clean fixture: valid custom code that should pass all checks. + local clean_dir="web/modules/custom/dcq_clean" + mkdir -p "${clean_dir}" + cat > "${clean_dir}/dcq_clean.info.yml" <<'YAML' +name: DCQ Clean +type: module +description: 'Clean fixture module that should pass all checks.' +core_version_requirement: ^11 +package: Testing +YAML + cat > "${clean_dir}/dcq_clean.module" <<'PHP' + "web/cspell-test.json" <<'JSON' { "version": "0.2", @@ -597,6 +654,14 @@ teardown() { # Perform cleanup operations if [ -n "${PROJNAME:-}" ]; then ddev delete -Oy "${PROJNAME}" >/dev/null 2>&1 + # Remove leftover Docker volumes (mariadb, mutagen, snapshots). + # The name filter is a substring match, so PROJNAME catches both + # PROJNAME-mariadb and ddev-PROJNAME-snapshots patterns. + local vols + vols="$(docker volume ls --quiet --filter "name=${PROJNAME}" 2>/dev/null || true)" + if [ -n "$vols" ]; then + echo "$vols" | xargs docker volume rm 2>/dev/null || true + fi fi # Persist TESTDIR if running inside GitHub Actions. Useful for uploading test result artifacts @@ -1185,6 +1250,10 @@ PY assert_success run wait_for_container_path "/var/www/html/web/themes/custom/dcq_theme/css/fixable.css" assert_success + run wait_for_container_path "/var/www/html/web/modules/contrib/dcq_fake/dcq_fake.module" + assert_success + run wait_for_container_path "/var/www/html/web/modules/custom/dcq_clean/dcq_clean.module" + assert_success # Batch git setup commands into single exec call run ddev exec bash -lc "command -v git >/dev/null && cd /var/www/html && (git rev-parse --is-inside-work-tree >/dev/null 2>&1 || git init >/dev/null) && printf 'unrelated change' > unrelated.txt" @@ -1201,6 +1270,82 @@ PY assert_output --partial "==> cspell" assert_output --partial "Summary:" + # ------------------------------------------------------- + # Detection accuracy: true positives (tools catch issues) + # ------------------------------------------------------- + + # PHPCS detects coding standard violations + run ./.ddev/drupal-code-quality/tooling/bin/phpcs web/modules/custom/dcq_test/dcq_test.module + assert_failure + assert_output --partial "TODO" + assert_output --partial "dcq_test.module" + + run ./.ddev/drupal-code-quality/tooling/bin/phpcs web/modules/custom/dcq_test/dcq_fixable.php + assert_failure + assert_output --partial "dcq_fixable.php" + + # PHPStan detects undefined variables + run ./.ddev/drupal-code-quality/tooling/bin/phpstan web/modules/custom/dcq_test/dcq_test.module + assert_failure + assert_output --partial "undefined" + + # ESLint detects rule violations in check mode + run ./.ddev/drupal-code-quality/tooling/bin/eslint web/themes/custom/dcq_theme/js/fixable.js + assert_failure + assert_output --partial "fixable.js" + + # ESLint passes on file that doesn't violate configured rules + run ./.ddev/drupal-code-quality/tooling/bin/eslint web/themes/custom/dcq_theme/js/unfixable.js + assert_success + + # Stylelint detects style violations in check mode + run ./.ddev/drupal-code-quality/tooling/bin/stylelint web/themes/custom/dcq_theme/css/fixable.css + assert_failure + assert_output --partial "fixable.css" + + run ./.ddev/drupal-code-quality/tooling/bin/stylelint web/themes/custom/dcq_theme/css/unfixable.css + assert_failure + assert_output --partial "unfixable.css" + + # Prettier detects formatting issues in check mode + run ./.ddev/drupal-code-quality/tooling/bin/prettier web/themes/custom/dcq_theme/js/prettier.js + assert_failure + assert_output --partial "prettier.js" + + # Composer validate passes on valid composer.json + run ./.ddev/drupal-code-quality/tooling/bin/composer-validate + assert_success + + # ----------------------------------------------------------- + # Detection accuracy: true negatives (clean code passes) + # ----------------------------------------------------------- + + # PHPCS passes on clean code + run ./.ddev/drupal-code-quality/tooling/bin/phpcs web/modules/custom/dcq_clean/dcq_clean.module + assert_success + + # PHPStan passes on clean code + run ./.ddev/drupal-code-quality/tooling/bin/phpstan web/modules/custom/dcq_clean/dcq_clean.module + assert_success + + # ----------------------------------------------------------- + # Detection accuracy: exclusion paths (contrib not flagged) + # ----------------------------------------------------------- + + # PHPCS default run should NOT include contrib fixtures + run ./.ddev/drupal-code-quality/tooling/bin/phpcs + assert_failure + refute_output --partial "dcq_fake.module" + + # ESLint default run should NOT include contrib fixtures + run ./.ddev/drupal-code-quality/tooling/bin/eslint + assert_failure + refute_output --partial "contrib" + + # ------------------------------------------------------- + # CSpell detection + # ------------------------------------------------------- + run ./.ddev/drupal-code-quality/tooling/bin/cspell lint --no-config-search -c web/cspell-test.json modules/custom/dcq_test/README.md assert_failure assert_output --partial "modlue" @@ -1210,6 +1355,10 @@ PY assert_output --partial "modlue" assert_output --partial "roottypo" + # ------------------------------------------------------- + # Fix commands + # ------------------------------------------------------- + before_phpcbf="$(read_container_file /var/www/html/web/modules/custom/dcq_test/dcq_fixable.php)" run ./.ddev/drupal-code-quality/tooling/bin/phpcbf web/modules/custom/dcq_test/dcq_fixable.php if [ "$status" -ne 0 ] && [ "$status" -ne 1 ]; then From 94c1da4213d9153b99e1212d58f76c86d0c872c5 Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Tue, 24 Mar 2026 08:39:02 -0500 Subject: [PATCH 3/5] Fix cspell comparison to ignore arrays populated by prepare-cspell.php The local .cspell.json asset has populated ignorePaths, dictionaries, dictionaryDefinitions, and words arrays (expanded by prepare-cspell.php at install time). Upstream has empty arrays. Strip these from both sides before comparing so the script only flags structural changes to fields like description, flagWords, and overrides. Also revert accidental asset updates from test run. --- scripts/sync-upstream-configs.sh | 33 +++++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/scripts/sync-upstream-configs.sh b/scripts/sync-upstream-configs.sh index 09739ca..552585d 100755 --- a/scripts/sync-upstream-configs.sh +++ b/scripts/sync-upstream-configs.sh @@ -78,7 +78,7 @@ patch_errors=0 # --- File manifest --- # Format: local_path|upstream_url|transform|header_extra -# transform: none, phpstan, prepare-cspell +# transform: none, phpstan, cspell, prepare-cspell # header_extra: additional header line after #ddev-generated (empty for most) declare -a MANIFEST=( @@ -91,7 +91,7 @@ declare -a MANIFEST=( # GitLab templates files "drupal-code-quality/assets/phpstan.neon|${base_url}/project/gitlab_templates/-/raw/${branch_templates}/assets/phpstan.neon|phpstan|# Source: ${base_url}/project/gitlab_templates/-/blob/${branch_templates}/assets/phpstan.neon" "drupal-code-quality/assets/.phpcs.xml|${base_url}/project/gitlab_templates/-/raw/${branch_templates}/assets/phpcs.xml.dist|none|" - "drupal-code-quality/assets/.cspell.json|${base_url}/project/gitlab_templates/-/raw/${branch_templates}/assets/.cspell.json|none|" + "drupal-code-quality/assets/.cspell.json|${base_url}/project/gitlab_templates/-/raw/${branch_templates}/assets/.cspell.json|cspell|" # Scripts "drupal-code-quality/tooling/scripts/prepare-cspell.php|${base_url}/project/gitlab_templates/-/raw/${branch_templates}/scripts/prepare-cspell.php|prepare-cspell|" ) @@ -104,6 +104,21 @@ transform_phpstan() { sed -n '/^parameters:/,$p' "$input" > "$output" } +# Strip arrays populated by prepare-cspell.php from a .cspell.json file, +# leaving only structural fields (description, language, flagWords, overrides, etc.) +# for comparison. Uses python3 for reliable JSON handling. +strip_cspell_expanded_arrays() { + local input="$1" output="$2" + python3 -c " +import json, sys +data = json.load(open(sys.argv[1])) +for key in ('ignorePaths', 'dictionaries', 'dictionaryDefinitions', 'words'): + data[key] = [] +json.dump(data, sys.stdout, indent=4, ensure_ascii=False) +print() +" "$input" > "$output" +} + # Apply DCQ patch to prepare-cspell.php: replace single-line $non_project_directories # with our expanded array for custom code scanning. transform_prepare_cspell() { @@ -157,12 +172,18 @@ for entry in "${MANIFEST[@]}"; do continue fi - # Apply transform + # Apply transform to fetched file transformed="${tmp_dir}/${filename}.transformed" case "$transform" in phpstan) transform_phpstan "$fetched" "$transformed" ;; + cspell) + # Upstream has empty arrays; local has arrays populated by prepare-cspell.php. + # Normalize both sides by stripping those arrays so we only compare + # structural fields (description, language, flagWords, overrides, etc.). + strip_cspell_expanded_arrays "$fetched" "$transformed" + ;; prepare-cspell) if ! transform_prepare_cspell "$fetched" "$transformed"; then printf "${YELLOW}PATCH TARGET NOT FOUND${RESET}\n" @@ -188,6 +209,12 @@ for entry in "${MANIFEST[@]}"; do local_stripped="${tmp_dir}/${filename}.local-stripped" strip_dcq_header "$local_file" > "$local_stripped" + # For cspell, also strip expanded arrays from the local copy + if [[ "$transform" == "cspell" ]]; then + strip_cspell_expanded_arrays "$local_stripped" "${local_stripped}.tmp" + mv "${local_stripped}.tmp" "$local_stripped" + fi + # Compare diff_output=$(diff -u --label "local: ${local_path}" --label "upstream" "$local_stripped" "$transformed" 2>/dev/null || true) From bb1dc3b387e16cff48736e4e271cf74c7ee85bcc Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Tue, 24 Mar 2026 15:44:18 -0500 Subject: [PATCH 4/5] Update upstream config --- drupal-code-quality/assets/.eslintrc.jquery.json | 3 +-- drupal-code-quality/assets/.stylelintrc.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/drupal-code-quality/assets/.eslintrc.jquery.json b/drupal-code-quality/assets/.eslintrc.jquery.json index 14ee4dc..543513b 100644 --- a/drupal-code-quality/assets/.eslintrc.jquery.json +++ b/drupal-code-quality/assets/.eslintrc.jquery.json @@ -54,5 +54,4 @@ "no-jquery/no-when": 2, "no-jquery/no-wrap": 0 } -} - +} \ No newline at end of file diff --git a/drupal-code-quality/assets/.stylelintrc.json b/drupal-code-quality/assets/.stylelintrc.json index fbe1b41..2da1706 100644 --- a/drupal-code-quality/assets/.stylelintrc.json +++ b/drupal-code-quality/assets/.stylelintrc.json @@ -381,7 +381,7 @@ "selector-id-pattern": "^[a-z][-_a-z0-9]*$", "selector-no-vendor-prefix": null, "shorthand-property-no-redundant-values": null, - "unit-allowed-list": ["ch", "deg", "dpcm", "em", "ex", "fr", "ms", "rem", "%", "s", "px", "vw", "vh", "cqw", "cqh", "cqi", "cqb", "cqmin", "cqmax"], + "unit-allowed-list": ["ch", "deg", "dpcm", "em", "ex", "fr", "ms", "rem", "%", "s", "px", "vw", "dvw", "svw", "lvw", "vh", "dvh", "svh", "lvh", "cqw", "cqh", "cqi", "cqb", "cqmin", "cqmax"], "value-keyword-case": ["lower", { "camelCaseSvgKeywords": true, "ignoreProperties": [ From b4f37eee634912f84f812be2bc6898f543a08fe8 Mon Sep 17 00:00:00 2001 From: Bob McDonald Date: Tue, 24 Mar 2026 15:48:11 -0500 Subject: [PATCH 5/5] Ensure trailing newlines on assets for DDEV addon checker The DDEV addon update checker requires files to end with a newline, but some upstream files lack one. Add trailing newline to .eslintrc.jquery.json and normalize fetched files in the sync script so the difference doesn't cause false positives. --- drupal-code-quality/assets/.eslintrc.jquery.json | 2 +- scripts/sync-upstream-configs.sh | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/drupal-code-quality/assets/.eslintrc.jquery.json b/drupal-code-quality/assets/.eslintrc.jquery.json index 543513b..1578f79 100644 --- a/drupal-code-quality/assets/.eslintrc.jquery.json +++ b/drupal-code-quality/assets/.eslintrc.jquery.json @@ -54,4 +54,4 @@ "no-jquery/no-when": 2, "no-jquery/no-wrap": 0 } -} \ No newline at end of file +} diff --git a/scripts/sync-upstream-configs.sh b/scripts/sync-upstream-configs.sh index 552585d..3871148 100755 --- a/scripts/sync-upstream-configs.sh +++ b/scripts/sync-upstream-configs.sh @@ -199,6 +199,12 @@ for entry in "${MANIFEST[@]}"; do ;; esac + # Ensure trailing newline on transformed file (DDEV addon checker requires it, + # but some upstream files lack one) + if [[ -s "$transformed" ]] && [[ "$(tail -c 1 "$transformed" | wc -l)" -eq 0 ]]; then + echo "" >> "$transformed" + fi + # Strip DCQ header from local for comparison if [[ ! -f "$local_file" ]]; then printf "${RED}LOCAL FILE MISSING${RESET}\n"