Repository: civictheme/uikit
Pull request #588: Added build script to setup local Drupal site for SDC development.
Author: login: AlexSkrypnyk
Labels: name: PR: Needs review
description: Pull request needs a review from assigned developers
Created: 2025-05-12T10:07:58Z | Merged: 2025-05-14T13:00:32Z

This PR adds scripts that allow to run a local Drupal site required when working with SDCs.

Without this, it requires a lot of tweaking to setup of a local Drupal and run `sdc_devel` to get the validation going.

## Checklist before requesting a review

- [ ] I have formatted the subject to include the issue number
  as `[#123] Verb in past tense with a period at the end.`
- [x] I have provided information in the `Changed` section about WHY something was
  done if this was a bespoke implementation.
- [x] I have performed a self-review of my code.
- [ ] I have commented on my code, particularly in hard-to-understand areas.
- [ ] I have added tests that prove my fix is effective or that my feature works.
- [ ] I have run new and existing relevant tests locally with my changes,
  and they have passed.
- [ ] I have provided screenshots, where applicable.

## Changed

1. Added Drupal build script to `tools/sdc`. This sets up a Drupal site with `sdc_devel` + rules exclusions in 35 seconds. The site is setup under `tools/sdc/build` dir, which is `.gitignore`'d.
2. Updated GHA CI to use the same script.
3. Added the `ct_dev` module with exception handling for the `sdc_devel` module.
4. Updated `README.md`

Takes 35 seconds to install Drupal site.

https://github.com/user-attachments/assets/13ed8671-1e1f-4d0e-b53e-e0308d362340



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
  - Added a build script to automate setup, provisioning, and management of the CivicTheme SDC Drupal environment.
  - Introduced a CivicTheme Development module with utilities for development and custom validation rule exclusions.
  - Added documentation for SDC development tools and schema validation.

- **Chores**
  - Updated the workflow to use a simplified build process with SQLite and removed MySQL/Drupal installation steps.
  - Broadened the version constraint for a development dependency to allow more flexibility.
  - Added a `.gitignore` to exclude build artifacts from version control.

- **Documentation**
  - Expanded README files with instructions for validating SDC schema and using new build tools.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.github/workflows/test-drupal.yml b/.github/workflows/test-drupal.yml
index 66073a092..ff5f4b6a2 100644
--- a/.github/workflows/test-drupal.yml
+++ b/.github/workflows/test-drupal.yml
@@ -14,71 +14,40 @@ jobs:
   test-drupal-sdc:
     runs-on: ubuntu-latest
 
-    services:
-      mysql:
-        image: mysql:8.0
-        env:
-          MYSQL_ALLOW_EMPTY_PASSWORD: yes
-          MYSQL_DATABASE: drupal
-        ports:
-          - 3306:3306
-        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+    env:
+      DRUPAL_VERSION: 11.1
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
-
-      - name: Setup PHP
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: '8.3'
-          extensions: mbstring, xml, ctype, iconv, mysql, curl, intl, gd, zip
-          tools: composer:v2
+        uses: actions/checkout@v4
 
       - name: Cache Composer dependencies
-        uses: actions/cache@v3
-        with:
-          path: ~/.composer/cache
-          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
-          restore-keys: |
-            ${{ runner.os }}-composer-
-
-      - name: Setup Drupal project
-        uses: bluehorndigital/setup-drupal@v1.1.0
+        uses: actions/cache@v4
         with:
-          path: drupal
-          version: '^11.1'
-          dependencies: 'drush/drush'
+          path: /tmp/composer-cache
+          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
 
-      - name: Add Drupal dependencies
-        run: composer require drupal/sdc_devel
-        working-directory: drupal
-
-      - name: Install Drupal site
-        run: vendor/bin/drush site-install minimal --db-url=mysql://root:@127.0.0.1:3306/drupal --account-name=admin --account-pass=admin --site-name="Drupal 11" -y
-        working-directory: drupal
-
-      - name: Create custom theme
+      - name: Upgrade sqlite3
         run: |
-          mkdir -p web/themes/custom/civictheme_sdc/components
-          cat << EOF > web/themes/custom/civictheme_sdc/civictheme_sdc.info.yml
-          name: CivicTheme SDC
-          type: theme
-          description: 'A theme for testing SDC components'
-          base theme: stark
-          core_version_requirement: ^11
-          EOF
-        working-directory: drupal
+          wget https://www.sqlite.org/2024/sqlite-autoconf-3450300.tar.gz -O /tmp/sqlite.tar.gz
+          tar -xzf /tmp/sqlite.tar.gz -C /tmp
+          cd /tmp/sqlite-autoconf-3450300
+          ./configure CFLAGS="-DSQLITE_ENABLE_COLUMN_METADATA=1" --prefix=/usr/local
+          make && sudo make install
+          sudo ldconfig
 
-      - name: Copy SDC components into custom theme
-        run: cp -r ${{ github.workspace }}/packages/sdc/components/* drupal/web/themes/custom/civictheme_sdc/components/
+      - name: Setup PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: 8.3
+          extensions: gd, sqlite, pdo_sqlite
+          coverage: pcov
 
-      - name: Enable theme and modules
-        run: |
-          vendor/bin/drush en sdc_devel -y
-          vendor/bin/drush theme:enable civictheme_sdc
-          vendor/bin/drush config:set system.theme default civictheme_sdc -y
-        working-directory: drupal
+      - name: Build the Drupal site
+        run: ./build.sh
+        working-directory: tools/sdc
+        env:
+          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Lint SDC components
         run: |
@@ -88,5 +57,5 @@ jobs:
             echo "Validation failed: Warning found in output."
             exit 1
           fi
-        working-directory: drupal
+        working-directory: tools/sdc/build
         continue-on-error: ${{ vars.CI_DRUPAL_SDC_LINT_IGNORE_FAILURE == '1' }}
diff --git a/.github/workflows/visual-regression.yml b/.github/workflows/visual-regression.yml
index 23135e82f..87a235865 100644
--- a/.github/workflows/visual-regression.yml
+++ b/.github/workflows/visual-regression.yml
@@ -5,7 +5,7 @@ on:
     branches:
       - main
       - 'project/**'
-      - 'project/**'
+      - 'feature/**'
 
 jobs:
   visual-regression:
diff --git a/.gitignore b/.gitignore
index af3f3289b..ff91a1ba3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,7 @@
 .logs
 .tmp
 .twig-cs-fixer.cache
+build
 composer.lock
 dist
 node_modules
diff --git a/README.md b/README.md
index afef5c5f8..0b31814fd 100644
--- a/README.md
+++ b/README.md
@@ -156,6 +156,10 @@ a non-zero status if any component needs updating.
 > Note: If the SDC and Twig implementations significantly diverge in the future, we will switch
 > to only updating the docblock headers rather than the entire component.
 
+#### Validating SDC schema
+
+See `tools/sdc/README.md` for more information on how to validate the SDC schema.
+
 ### Check and fix code style
 
     npm run lint
diff --git a/composer.json b/composer.json
index 960ef8a54..4775fda72 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,7 @@
         "php": ">=8.2"
     },
     "require-dev": {
-        "twig/twig": "~3.20.0",
+        "twig/twig": "^3.16.0",
         "vincentlanglet/twig-cs-fixer": "^3.0"
     },
     "scripts": {
diff --git a/tools/sdc/.gitignore b/tools/sdc/.gitignore
new file mode 100644
index 000000000..378eac25d
--- /dev/null
+++ b/tools/sdc/.gitignore
@@ -0,0 +1 @@
+build
diff --git a/tools/sdc/README.md b/tools/sdc/README.md
new file mode 100755
index 000000000..df6aa2d9f
--- /dev/null
+++ b/tools/sdc/README.md
@@ -0,0 +1,33 @@
+# SDC tools
+
+This directory contains scripts used for SDC development.
+
+These can be used locally and in the CI environment.
+
+SDC components are validated using `sdc_devel` module running on a vanilla
+Drupal site.
+
+Run the commands from the `tools/sdc` directory.
+
+To build and run a local Drupal site with SDC components, run the following commands:
+```bash
+./build.sh                   # Run assemble, start, provision
+```
+
+Or run these commands separately:
+```bash
+./build.sh assemble          # Run only assemble
+./build.sh provision         # Run only provision
+./build.sh start             # Run only start
+./build.sh stop              # Run only stop
+```
+
+To validate the SDC schema, run the following command:
+
+```bash
+build/vendor/bin/drush sdc-devel:validate civictheme_sdc
+```
+
+To add exclusions to the validation rules, update the `ct_dev/ct_dev.module` file.
+This module is symlinked into the built Drupal site, so changes will be reflected
+momentarily.
diff --git a/tools/sdc/build.sh b/tools/sdc/build.sh
new file mode 100755
index 000000000..791225edb
--- /dev/null
+++ b/tools/sdc/build.sh
@@ -0,0 +1,260 @@
+#!/usr/bin/env bash
+##
+# Combined script for CivicTheme SDC environment.
+#
+# Usage:
+#   ./build.sh                   # Run assemble, start, provision
+#   ./build.sh assemble          # Run only assemble
+#   ./build.sh provision         # Run only provision
+#   ./build.sh start             # Run only start
+#   ./build.sh stop              # Run only stop
+#
+# shellcheck disable=SC2015,SC2094,SC2002
+
+set -eu
+[ -n "${DEBUG:-}" ] && set -x
+
+#-------------------------------------------------------------------------------
+# Variables (passed from environment; provided for reference only).
+#-------------------------------------------------------------------------------
+
+# Drupal core version to use.
+DRUPAL_VERSION="${DRUPAL_VERSION:-11}"
+
+# Webserver hostname.
+WEBSERVER_HOST="${WEBSERVER_HOST:-localhost}"
+
+# Webserver port.
+WEBSERVER_PORT="${WEBSERVER_PORT:-8000}"
+
+# Webserver wait timeout.
+WEBSERVER_WAIT_TIMEOUT="${WEBSERVER_WAIT_TIMEOUT:-5}"
+
+# Drupal profile to use when installing the site.
+DRUPAL_PROFILE="${DRUPAL_PROFILE:-standard}"
+
+#-------------------------------------------------------------------------------
+
+# @formatter:off
+note() { printf "       %s\n" "${1}"; }
+info() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[34m[INFO] %s\033[0m\n" "${1}" || printf "[INFO] %s\n" "${1}"; }
+pass() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[32m[ OK ] %s\033[0m\n" "${1}" || printf "[ OK ] %s\n" "${1}"; }
+fail() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[31m[FAIL] %s\033[0m\n" "${1}" || printf "[FAIL] %s\n" "${1}"; }
+# @formatter:on
+
+drush() { "build/vendor/bin/drush" -r "$(pwd)/build/web" -y "$@"; }
+
+extension="civictheme_sdc"
+
+#-------------------------------------------------------------------------------
+# ASSEMBLE FUNCTION
+#-------------------------------------------------------------------------------
+run_assemble() {
+  echo "==============================="
+  echo "         🏗️ ASSEMBLE           "
+  echo "==============================="
+  echo
+
+  info "Validate tools."
+  ! command -v git >/dev/null && fail "ERROR: Git is required for this script to run." && exit 1
+  ! command -v php >/dev/null && fail "ERROR: PHP is required for this script to run." && exit 1
+  php -r 'exit(version_compare(PHP_VERSION, "8.3.0", ">=") ? 0 : (fwrite(STDERR, "ERROR: PHP 8.3.0 or higher is required. Found: " . PHP_VERSION . "\n") || 1));'
+  ! command -v composer >/dev/null && fail "ERROR: Composer (https://getcomposer.org/) is required for this script to run." && exit 1
+  ! command -v sqlite3 >/dev/null && fail "ERROR: sqlite3 is required for this script to run." && exit 1
+  pass "Tools are valid."
+
+  # Make sure Composer doesn't run out of memory.
+  export COMPOSER_MEMORY_LIMIT=-1
+
+  # Reset the environment.
+  if [ -d "build" ]; then
+    info "Removing existing build directory."
+    chmod -Rf 777 "build" >/dev/null || true
+    rm -rf "build" >/dev/null || true
+    pass "Existing build directory removed."
+  fi
+
+  info "Creating Drupal ${DRUPAL_VERSION} codebase."
+  composer create-project --no-interaction --no-scripts --no-progress --prefer-dist --no-install "drupal/recommended-project:${DRUPAL_VERSION}" "build"
+  rm build/composer.lock >/dev/null || true
+
+  info "Merging configuration from extension's composer.json."
+  php -r "echo json_encode(array_replace_recursive(json_decode(file_get_contents('../../composer.json'), true),json_decode(file_get_contents('build/composer.json'), true)),JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES);" >"build/composer2.json" && mv -f "build/composer2.json" "build/composer.json"
+
+  if [ -n "${GITHUB_TOKEN:-}" ]; then
+    info "Adding GitHub authentication token if provided."
+    composer --working-dir="build" config --global github-oauth.github.com "${GITHUB_TOKEN}"
+    composer --working-dir="build" config --global github-oauth.github.com | grep -q "gh" || fail "GitHub token not added."
+    pass "GitHub token added."
+  fi
+
+  info "Creating custom directories."
+  mkdir -p build/web/modules/custom build/web/themes/custom
+
+  info "Installing dependencies."
+  composer --working-dir="build" install
+
+  info "Installing development dependencies."
+  composer --working-dir="build" require drupal/sdc_devel drush/drush
+
+  info "Creating custom theme"
+  mkdir -p build/web/themes/custom/civictheme_sdc
+  cat <<EOF >build/web/themes/custom/civictheme_sdc/civictheme_sdc.info.yml
+name: CivicTheme SDC
+type: theme
+description: 'A theme for testing SDC components'
+base theme: stark
+core_version_requirement: ^$DRUPAL_VERSION
+EOF
+
+  info "Symlinking SDC components into custom theme"
+  ln -s "$(pwd)/../../packages/sdc/components" build/web/themes/custom/civictheme_sdc/components
+
+  info "Symlinking development module"
+  ln -s "$(pwd)/ct_dev" build/web/modules/custom/ct_dev
+
+  echo
+  echo "==============================="
+  echo "    🏗 ASSEMBLE COMPLETE ✅   "
+  echo "==============================="
+  echo
+}
+
+#-------------------------------------------------------------------------------
+# PROVISION FUNCTION
+#-------------------------------------------------------------------------------
+run_provision() {
+  echo "==============================="
+  echo "         🚀 PROVISION          "
+  echo "==============================="
+  echo
+
+  # Database file path.
+  db_file="/tmp/site_${extension}.sqlite"
+
+  info "Installing Drupal into SQLite database ${db_file}."
+  db_status=$(drush status --field=db-status 2>/dev/null || echo "Not connected")
+  if [ "${db_status}" = "Connected" ]; then
+    drush sql:drop -y || true >/dev/null
+  fi
+  drush site-install "${DRUPAL_PROFILE}" -y --db-url="sqlite://localhost/${db_file}" --account-name=admin install_configure_form.enable_update_status_module=NULL install_configure_form.enable_update_status_emails=NULL
+  pass "Drupal installed."
+
+  drush status
+
+  info "Enabling extension ${extension}."
+  drush theme:enable "${extension}"
+
+  info "Clearing caches."
+  drush cr
+
+  info "Enabling development modules."
+  drush pm:enable sdc_devel ct_dev
+  drush theme:enable civictheme_sdc
+
+  info "Setting the default theme to ${extension}."
+  drush config:set system.theme default "${extension}" -y
+
+  info "Pre-warming caches."
+  curl -s "http://${WEBSERVER_HOST}:${WEBSERVER_PORT}" >/dev/null
+
+  echo
+  echo "==============================="
+  echo "   🚀 PROVISION COMPLETE  ✅  "
+  echo "==============================="
+  echo
+  echo "Site URL:            http://${WEBSERVER_HOST}:${WEBSERVER_PORT}"
+  echo -n "One-time login link: "
+  drush -l "http://${WEBSERVER_HOST}:${WEBSERVER_PORT}" uli --no-browser
+  echo
+}
+
+#-------------------------------------------------------------------------------
+# START FUNCTION
+#-------------------------------------------------------------------------------
+run_start() {
+  echo "==============================="
+  echo "      💻 START ENVIRONMENT     "
+  echo "==============================="
+  echo
+
+  info "Stopping previously started services, if any."
+  killall -9 php >/dev/null 2>&1 || true
+
+  info "Starting the PHP webserver."
+  nohup php -S "${WEBSERVER_HOST}:${WEBSERVER_PORT}" -t "$(pwd)/build/web" "$(pwd)/build/web/.ht.router.php" >/tmp/php.log 2>&1 &
+
+  note "Waiting ${WEBSERVER_WAIT_TIMEOUT} seconds for the server to be ready."
+  sleep "${WEBSERVER_WAIT_TIMEOUT}"
+
+  note "Checking that the server was started."
+  netstat_opts='-tulpn'
+  [ "$(uname)" == "Darwin" ] && netstat_opts='-anv' || true
+  netstat "${netstat_opts[@]}" | grep -q "${WEBSERVER_PORT}" || (echo "ERROR: Unable to start inbuilt PHP server" && cat /tmp/php.log && exit 1)
+
+  pass "Server started successfully."
+
+  info "Checking that the server can serve content."
+  curl -s -o /dev/null -w "%{http_code}" -L -I "http://${WEBSERVER_HOST}:${WEBSERVER_PORT}" | grep -q 200 || (echo "ERROR: Server is started, but site cannot be served" && exit 1)
+  pass "Server can serve content."
+
+  echo
+  echo "==============================="
+  echo "    💻 ENVIRONMENT READY  ✅  "
+  echo "==============================="
+  echo
+  echo "Directory : $(pwd)/build/web"
+  echo "URL       : http://${WEBSERVER_HOST}:${WEBSERVER_PORT}"
+  echo
+}
+
+#-------------------------------------------------------------------------------
+# STOP FUNCTION
+#-------------------------------------------------------------------------------
+run_stop() {
+  echo "==============================="
+  echo "      💻 STOP ENVIRONMENT      "
+  echo "==============================="
+  echo
+
+  info "Stopping previously started services, if any."
+  killall -9 php >/dev/null 2>&1 || true
+  sleep 1
+  pass "Services stopped."
+
+  echo
+  echo "==============================="
+  echo "   💻 ENVIRONMENT STOPPED ✅  "
+  echo "==============================="
+  echo
+}
+
+#-------------------------------------------------------------------------------
+# MAIN SCRIPT
+#-------------------------------------------------------------------------------
+if [ $# -gt 0 ]; then
+  case "$1" in
+  assemble)
+    run_assemble
+    ;;
+  provision)
+    run_provision
+    ;;
+  start)
+    run_start
+    ;;
+  stop)
+    run_stop
+    ;;
+  *)
+    echo "Error: Unknown argument '$1'"
+    echo "Usage: $0 [assemble|provision|start|stop]"
+    echo "       If no argument is provided, will run assemble, provision, and start in sequence."
+    exit 1
+    ;;
+  esac
+else
+  run_assemble
+  run_start
+  run_provision
+fi
diff --git a/tools/sdc/ct_dev/ct_dev.info.yml b/tools/sdc/ct_dev/ct_dev.info.yml
new file mode 100644
index 000000000..87c5d3cae
--- /dev/null
+++ b/tools/sdc/ct_dev/ct_dev.info.yml
@@ -0,0 +1,5 @@
+name: CivicTheme Development module
+type: module
+description: 'Development utilities for CivicTheme.'
+package: Custom
+core_version_requirement: ^10 || ^11
diff --git a/tools/sdc/ct_dev/ct_dev.module b/tools/sdc/ct_dev/ct_dev.module
new file mode 100644
index 000000000..4f843104d
--- /dev/null
+++ b/tools/sdc/ct_dev/ct_dev.module
@@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * CivicTheme Development module.
+ */
+
+/**
+ * Implements hook_twig_validator_rule_info_alter().
+ */
+function ct_dev_twig_validator_rule_info_alter(array &$info) {
+  // @see https://www.drupal.org/project/sdc_devel/issues/3517321
+  $rule_name_ignore = -1;
+  $rule_name_allow = 0;
+  $rule_name_error = 3;
+  $rule_name_warning = 4;
+  $rule_name_notice = 5;
+
+  $info['filter']['rule_on_name'][$rule_name_ignore][] = 'raw';
+}
diff --git a/tools/visual-diff/config/config.json b/tools/visual-diff/config/config.json
index c5f363103..098c995e3 100644
--- a/tools/visual-diff/config/config.json
+++ b/tools/visual-diff/config/config.json
@@ -1,4 +1,12 @@
 {
+  "masking": {
+    "selectors": [
+      ".ct-iframe",
+      ".ct-map--canvas",
+      ".ct-video-player",
+      ".ct-video"
+    ]
+  },
   "screenshot_sources": {
     "current_branch": {
       "packages": {
diff --git a/tools/visual-diff/lib/config.mjs b/tools/visual-diff/lib/config.mjs
index 2299a3a3f..aea7b8d57 100644
--- a/tools/visual-diff/lib/config.mjs
+++ b/tools/visual-diff/lib/config.mjs
@@ -46,6 +46,14 @@ export function loadConfig() {
 
   // Return default configuration if file doesn't exist or there's an error
   return {
+    masking: {
+      selectors: [
+        '.ct-iframe',
+        '.ct-map--canvas',
+        '.ct-video-player',
+        '.ct-video',
+      ],
+    },
     screenshot_sources: {
       current_branch: {
         packages: {
diff --git a/tools/visual-diff/lib/screenshot.mjs b/tools/visual-diff/lib/screenshot.mjs
index 8a61045ad..952ed102c 100644
--- a/tools/visual-diff/lib/screenshot.mjs
+++ b/tools/visual-diff/lib/screenshot.mjs
@@ -11,6 +11,7 @@ import handler from 'serve-handler';
 import { Cluster } from 'puppeteer-cluster';
 import { ensureDirectory } from './screenshot-set-manager.mjs';
 import { determineOptimalConcurrency } from './utils.mjs';
+import { loadConfig } from './config.mjs';
 
 /**
  * Start a static server for the given directory.
@@ -75,6 +76,7 @@ export async function captureScreenshots({
   port = 6006,
   concurrency = determineOptimalConcurrency(),
 }) {
+  const config = loadConfig();
   ensureDirectory(outputDir);
   let server = null;
   try {
@@ -95,9 +97,29 @@ export async function captureScreenshots({
         headless: true,
       },
     });
-    await cluster.task(async ({ page, data: { storyUrl, filePath } }) => {
-      console.log(`Capturing: ${storyUrl} to ${filePath}`);
+    await cluster.task(async ({ page, data: { storyUrl, filePath, selectors } }) => {
       await page.goto(storyUrl, { waitUntil: ['domcontentloaded', 'networkidle0'] });
+      console.log(`Capturing: ${storyUrl} to ${filePath}`);
+      for (const selector of selectors) {
+        // String JS script passed into page context.
+        // eslint-disable-next-line no-await-in-loop
+        await page.evaluate(
+          `document.querySelectorAll('${selector}').forEach((el) => {
+              const rect = el.getBoundingClientRect();
+              const mask = document.createElement('visual-diff-mask');
+              mask.style.position = 'absolute';
+              mask.style.top = rect.top + 'px';
+              mask.style.left = rect.left + 'px';
+              mask.style.zIndex = 'calc(infinity)';
+              mask.style.display = 'block';
+              mask.style.width = Math.ceil(rect.width) + 'px';
+              mask.style.height = Math.ceil(rect.height) + 'px';
+              mask.style.pointerEvents = 'none';
+              mask.style.background = 'magenta';
+              document.body.appendChild(mask);
+            });`,
+        );
+      }
       await page.screenshot({
         path: filePath,
         fullPage: true,
@@ -112,7 +134,7 @@ export async function captureScreenshots({
         fs.mkdirSync(fileDir, { recursive: true });
       }
       const storyUrl = `${url}/iframe?id=${storyId}&viewMode=story`;
-      cluster.queue({ storyUrl, filePath });
+      cluster.queue({ storyUrl, filePath, selectors: config.masking.selectors || [] });
     });
     await cluster.idle();
     await cluster.close();
diff --git a/tools/visual-diff/lib/server.mjs b/tools/visual-diff/lib/server.mjs
index f42703c3d..a2a279ca7 100644
--- a/tools/visual-diff/lib/server.mjs
+++ b/tools/visual-diff/lib/server.mjs
@@ -8,8 +8,9 @@ import http from 'http';
 import fs from 'fs';
 import path from 'path';
 import { fileURLToPath } from 'url';
-import { loadScreenshotSets as loadConfig, getScreenshotPath } from './screenshot-set-manager.mjs';
+import { loadScreenshotSets, getScreenshotPath } from './screenshot-set-manager.mjs';
 import { formatDisplayName } from './utils.mjs';
+import { loadConfig } from './config.mjs';
 
 const filename = fileURLToPath(import.meta.url);
 const dirname = path.dirname(filename);
@@ -71,16 +72,20 @@ function generateComparisonList(config) {
  * @returns {string} The HTML for the home page.
  */
 function generateLandingPage() {
+  const screenshotSets = loadScreenshotSets();
   const config = loadConfig();
   const templatePath = path.join(TEMPLATES_DIR, 'homepage.html');
 
   try {
     const template = fs.readFileSync(templatePath, 'utf8');
-    return template.replace('{{COMPARISON_LIST}}', generateComparisonList(config));
+    const maskingSelectors = config.masking.selectors.map((selector) => `<li>${selector}</li>`).join('');
+    return template
+      .replace('{{COMPARISON_LIST}}', generateComparisonList(screenshotSets))
+      .replace('{{MASK_SELECTORS}}', maskingSelectors);
   } catch (error) {
     console.error(`Error reading template file: ${error.message}`);
     return `<html><body><h1>RegViz - Visual Regression Reports</h1>
-      <div>${generateComparisonList(config)}</div></body></html>`;
+      <div>${generateComparisonList(screenshotSets)}</div></body></html>`;
   }
 }
 
diff --git a/tools/visual-diff/lib/templates/homepage.html b/tools/visual-diff/lib/templates/homepage.html
index 115984ca7..06603b472 100644
--- a/tools/visual-diff/lib/templates/homepage.html
+++ b/tools/visual-diff/lib/templates/homepage.html
@@ -96,6 +96,7 @@ <h1>Visual Diff - Visual Regression Reports</h1>
   <div class="section">
     <h2>Comparison Reports</h2>
     {{COMPARISON_LIST}}
+    <p>Note the following selectors are masked which may obscure differences, update <code>tools/visual-diff/config/config.json</code> to adjust the list: {{MASK_SELECTORS}}</p>
   </div>
 </body>
 </html>


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @AlexSkrypnyk, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request introduces a set of scripts within a `.devtools` directory to facilitate the setup of a local Drupal environment for SDC (Single Directory Components) development. These scripts automate the process of assembling the codebase, starting/stopping the PHP server, and provisioning the Drupal website. The PR also updates the GHA CI configuration to utilize these scripts and updates the `README.md` file with instructions on how to use the new development tools.

### Highlights
* **New devtools scripts**: The PR adds `assemble.sh`, `start.sh`, `provision.sh`, and `stop.sh` scripts to the `.devtools` directory, providing a streamlined way to build, run, and provision a local Drupal site for SDC development.
* **GHA CI Updates**: The GitHub Actions CI configuration is updated to use the newly added devtools scripts, ensuring consistency between local and CI environments.
* **README.md update**: The `README.md` file is updated to include instructions on using the new devtools scripts for local Drupal site setup and SDC schema validation.
* **Composer Configuration**: The composer.json file is modified to use a specific version of twig.

### Changelog
<details>
<summary>Click here to see the changelog</summary>

* **.devtools/README.md**
  * Added a README file explaining the purpose and usage of the devtools scripts.
  * Provides basic instructions on how to use the scripts to assemble, start, provision, and stop the local Drupal environment.
* **.devtools/assemble.sh**
  * Introduces a script to assemble the Drupal codebase, including project code and dependencies.
  * Allows specifying Drupal core versions and retrieves the scaffold from drupal-composer/drupal-project.
  * Builds the Drupal site codebase with the current extension and its dependencies, adds development dependencies, and installs composer dependencies.
  * Includes logic to merge the extension's composer.json configuration into the build's composer.json.
  * Creates a custom theme and copies SDC components into it.
* **.devtools/provision.sh**
  * Introduces a script to provision the Drupal website using the existing codebase.
  * Installs Drupal using an SQLite database, enables modules, and generates a one-time login link.
  * Enables the specified extension (theme or module) and clears caches.
  * Enables suggested and development modules, including `sdc_devel`.
  * Sets the default theme to `civictheme_sdc`.
* **.devtools/start.sh**
  * Introduces a script to start the PHP webserver for the Drupal environment.
  * Stops any previously running PHP services and starts the built-in PHP server.
  * Includes checks to ensure the server has started successfully and can serve content.
* **.devtools/stop.sh**
  * Introduces a script to stop the PHP webserver.
  * Kills any running PHP processes.
* **.gitignore**
  * Added `build` directory to the `.gitignore` file to prevent it from being tracked.
* **README.md**
  * Added a section on validating SDC schema, including instructions on how to build and run a local Drupal site using the devtools scripts.
  * Provides instructions on how to run the `drush sdc-devel:validate` command.
* **composer.json**
  * Downgraded twig/twig version from ~3.20.0 to ~3.16.0

</details>


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> A script to start,
> A site to create,
> Then provision,
> For SDC vision,
> Dev's workflow elevate.


state: COMMENTED
author_association: NONE
submitted_at: 2025-05-12T10:08:12Z
user: login: gemini-code-assist[bot]
body: ## Code Review
This PR introduces helpful scripts for setting up a local Drupal environment for SDC development. The scripts seem well-structured and provide a convenient way to assemble, start, provision, and stop a local Drupal site. The updates to the README.md are also beneficial for guiding users on how to use these scripts. Overall, this is a valuable addition to the project.

### Summary of Findings
* **Missing error handling**: In `assemble.sh`, the script attempts to remove the existing build directory using `chmod` and `rm -rf`. While the script uses `|| true` to ignore errors, it might be better to log these failures for debugging purposes. This applies to other similar commands as well.
* **Conditional GitHub token configuration**: In `assemble.sh`, the script configures the global Composer GitHub token. It might be better to configure this token only for the project, to avoid unintended side effects on the user's global Composer configuration.
* **Inconsistent use of `info` and `note` functions**: The scripts use both `info` and `note` functions for logging. It would be good to have a consistent approach for using these functions.

### Merge Readiness
The pull request is a valuable addition to the project. However, I recommend addressing the identified issues before merging. Specifically, improving error handling, refining the GitHub token configuration, and ensuring consistent logging practices would enhance the reliability and maintainability of the scripts. I am unable to directly approve this pull request, and recommend that others review and approve this code before merging.
state: CHANGES_REQUESTED
author_association: NONE
submitted_at: 2025-05-12T10:08:24Z
user: login: AlexSkrypnyk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T10:10:25Z
user: login: AlexSkrypnyk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-12T10:11:04Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>♻️ Duplicate comments (2)</summary><blockquote>

<details>
<summary>composer.json (1)</summary><blockquote>

`15-15`: **Confirm rationale for `twig/twig` version change.**
The `twig/twig` constraint was downgraded to `~3.16.0`. Ensure this aligns with Drupal 11.1 compatibility and verify there are no known security vulnerabilities in the chosen version.

</blockquote></details>
<details>
<summary>.github/workflows/test-drupal.yml (1)</summary><blockquote>

`17-19`: **Enable a Drupal version matrix for broader CI coverage.**  
Parametrizing `DRUPAL_VERSION` in a matrix allows testing against multiple Drupal versions without duplicating jobs. For example:
```yaml
strategy:
  matrix:
    drupal-version: [11.1, 10.0, 9.5]
env:
  DRUPAL_VERSION: ${{ matrix.drupal-version }}
```

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (16)</summary><blockquote>

<details>
<summary>README.md (2)</summary><blockquote>

`161-162`: **Add missing article “the”.**
Insert “the” before “`sdc_devel` module” for grammatical correctness:
```diff
- SDC components are validated using `sdc_devel` module running on a vanilla
+ SDC components are validated using the `sdc_devel` module running on a vanilla
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~161-~161: You might be missing the article “the” here.
Context: ...ema  SDC components are validated using `sdc_devel` module running on a vanilla  ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

---

`164-164`: **Add comma for clarity.**
Insert a comma after “components”:
```diff
- To build and run a local Drupal site with SDC components run the following commands:
+ To build and run a local Drupal site with SDC components, run the following commands:
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~164-~164: A comma might be missing here.
Context: ...ld and run a local Drupal site with SDC components run the following commands: ```bash ./d...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

</details>

</details>

</blockquote></details>
<details>
<summary>.devtools/stop.sh (1)</summary><blockquote>

`13-13`: **Remove unused `note()` function.**
The `note()` helper is defined but never called. Consider removing it to reduce dead code.

</blockquote></details>
<details>
<summary>.devtools/provision.sh (5)</summary><blockquote>

`29-34`: **Consider centralizing logging helper functions.**  
The `note`, `info`, `pass`, and `fail` functions are duplicated across multiple scripts (`assemble.sh`, `start.sh`, `stop.sh`, and now `provision.sh`). Extract these into a shared `devtools/common.sh` and source it at the top of each script to DRY up your code and simplify future maintenance.

---

`73-77`: **Optimize suggested modules discovery.**  
You're using `cat composer.json | jq … | sed … | cut …`. Since you’ve disabled the shellcheck warning for `cat`, a cleaner approach is to call `jq` directly on the file, e.g.:  
```bash
drupal_suggests=$(jq -r '.suggest? // {} | keys[]' composer.json | sed 's|drupal/||')
```  
This reduces the number of subprocesses and makes the pipeline more readable.

---

`79-83`: **Reuse extension variable for consistency.**  
You enable `sdc_devel` and `civictheme_sdc` directly, but you’ve already declared `extension="civictheme_sdc"`. Consider also parameterizing the dev module name (e.g., `dev_module="sdc_devel"`) and reusing these variables to avoid hard-coding and ease future changes.

---

`85-87`: **Fail on cache pre-warm errors.**  
The current `curl -s` invocation will silently proceed if the request fails. To ensure provision errors are surfaced, use `curl --fail` or inspect the HTTP status code and exit non-zero on failure.

---

`93-96`: **Use the Drush wrapper for consistency.**  
For the one-time login link, call your `drush()` wrapper (e.g., `drush uli --no-browser`) instead of invoking the global `drush`. This guarantees the correct root path (`-r build/web`) and `-y` flag are always applied.

</blockquote></details>
<details>
<summary>.devtools/start.sh (3)</summary><blockquote>

`26-30`: **Centralize logging helpers.**  
The `note`, `info`, `pass`, and `fail` functions mirror those in your other devtools scripts. Extract them into a shared `devtools/common.sh` to keep logging consistent and reduce duplication.

---

`49-52`: **Improve port availability check.**  
`netstat` may not be installed or may be deprecated on some environments. Consider adding a fallback to `ss -tulpn` or using `lsof -iTCP:${WEBSERVER_PORT}`, which can improve cross-platform reliability.

---

`55-57`: **Enhance failure diagnostics on HTTP check.**  
When the `curl` check fails, it’s helpful to output the last few lines of `/tmp/php.log` or the actual HTTP status for debugging. Consider capturing the response code in a variable and printing it upon failure.

</blockquote></details>
<details>
<summary>.github/workflows/test-drupal.yml (1)</summary><blockquote>

`30-38`: **Optimize SQLite upgrade step.**  
Compiling SQLite from source on every CI run can add significant time. Consider installing via apt (`sudo apt-get install sqlite3 libsqlite3-dev`) or using a pre-built binary/action that caches the compiled artifact to speed up the workflow.

</blockquote></details>
<details>
<summary>.devtools/assemble.sh (4)</summary><blockquote>

`38-43`: **Centralize logging helper functions.**  
The `note`, `info`, `pass`, and `fail` functions are repeated in all four devtools scripts. Extract them into a single `devtools/common.sh` to reduce duplication and ensure consistent output across your tooling.

---

`105-113`: **Prefer JSON-aware updates to `composer.json`.**  
Using `sed` regex to pin versions and update stability is brittle. Consider using `jq` to programmatically set:
```bash
jq '.require["drupal/core"]="^'"$DRUPAL_VERSION"'" | .["minimum-stability"]="'$drupal_version_stability'" | .["prefer-stable"]='$drupal_version_prefer_stable \
  build/composer.json > tmp && mv tmp build/composer.json
```  
This approach is more robust against formatting changes.

---

`121-123`: **Clean up commented-out code.**  
The block for merging `composer.dev.json` is commented out. If it's no longer needed, delete it to reduce clutter; otherwise, add a `TODO` explaining when it should be re-enabled.

---

`176-178`: **Guard the component copy step.**  
You assume `packages/sdc/components/` exists. Add a check:
```bash
if [ -d packages/sdc/components ]; then
  cp -r packages/sdc/components/* build/web/themes/custom/civictheme_sdc/components/
else
  fail "No SDC components found to copy."
  exit 1
fi
```  
This ensures the script fails early and clearly if the source is missing.

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3de1d867c293860b2dda7b8fd154c6b5a22ac486 and 9f3401cf224449ec62f407c6cfb994d5a6f89b5a.

</details>

<details>
<summary>📒 Files selected for processing (9)</summary>

* `.devtools/README.md` (1 hunks)
* `.devtools/assemble.sh` (1 hunks)
* `.devtools/provision.sh` (1 hunks)
* `.devtools/start.sh` (1 hunks)
* `.devtools/stop.sh` (1 hunks)
* `.github/workflows/test-drupal.yml` (2 hunks)
* `.gitignore` (1 hunks)
* `README.md` (1 hunks)
* `composer.json` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (3)</summary>

<details>
<summary>.devtools/stop.sh (3)</summary><blockquote>

<details>
<summary>.devtools/start.sh (2)</summary>

* `info` (27-27)
* `pass` (28-28)

</details>
<details>
<summary>.devtools/provision.sh (2)</summary>

* `info` (31-31)
* `pass` (32-32)

</details>
<details>
<summary>.devtools/assemble.sh (2)</summary>

* `info` (40-40)
* `pass` (41-41)

</details>

</blockquote></details>
<details>
<summary>.devtools/provision.sh (3)</summary><blockquote>

<details>
<summary>.devtools/stop.sh (2)</summary>

* `info` (14-14)
* `pass` (15-15)

</details>
<details>
<summary>.devtools/start.sh (2)</summary>

* `info` (27-27)
* `pass` (28-28)

</details>
<details>
<summary>.devtools/assemble.sh (2)</summary>

* `info` (40-40)
* `pass` (41-41)

</details>

</blockquote></details>
<details>
<summary>.devtools/start.sh (3)</summary><blockquote>

<details>
<summary>.devtools/stop.sh (3)</summary>

* `info` (14-14)
* `note` (13-13)
* `pass` (15-15)

</details>
<details>
<summary>.devtools/provision.sh (3)</summary>

* `info` (31-31)
* `note` (30-30)
* `pass` (32-32)

</details>
<details>
<summary>.devtools/assemble.sh (3)</summary>

* `info` (40-40)
* `note` (39-39)
* `pass` (41-41)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>README.md</summary>

[uncategorized] ~161-~161: You might be missing the article “the” here.
Context: ...ema  SDC components are validated using `sdc_devel` module running on a vanilla  ...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

---

[uncategorized] ~164-~164: A comma might be missing here.
Context: ...ld and run a local Drupal site with SDC components run the following commands: ```bash ./d...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

</details>

</details>
<details>
<summary>🪛 Shellcheck (0.10.0)</summary>

<details>
<summary>.devtools/assemble.sh</summary>

[warning] 76-76: extension appears unused. Verify use (or export if used externally).

(SC2034)

---

[warning] 77-77: type appears unused. Verify use (or export if used externally).

(SC2034)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (3)</summary><blockquote>

<details>
<summary>.gitignore (1)</summary>

`4-4`: **Approve ignoring `build` directory.**
The `build` directory should be excluded from version control to prevent committing generated artifacts produced by the new `.devtools` scripts.

</details>
<details>
<summary>.devtools/provision.sh (1)</summary>

`36-36`: **Verify Drush binary path in wrapper function.**  
The `drush()` wrapper hardcodes `build/vendor/bin/drush`. Please confirm this path is always valid in both local and CI runs. Alternatively, consider detecting `drush` on `PATH` or making the path configurable via an environment variable to improve flexibility.

</details>
<details>
<summary>.github/workflows/test-drupal.yml (1)</summary>

`25-29`: **Verify the Composer cache directory is in use.**  
You cache `/tmp/composer-cache`, but Composer defaults to `~/.composer/cache` unless `COMPOSER_CACHE_DIR` is exported. Please confirm you set:
```yaml
env:
  COMPOSER_CACHE_DIR: /tmp/composer-cache
```
in the job, or adjust the cache path to match Composer’s default to ensure dependencies are actually cached.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T10:17:15Z
user: login: coderabbitai[bot]
body: outdated
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T00:51:43Z
user: login: AlexSkrypnyk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T02:57:19Z
user: login: AlexSkrypnyk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T02:57:49Z
user: login: AlexSkrypnyk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T02:58:16Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 2**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>.github/workflows/test-drupal.yml (1)</summary><blockquote>

`17-18`: **Consider matrix testing for Drupal versions**

To broaden coverage and catch version-specific issues, you can refactor the workflow to use a matrix strategy over `DRUPAL_VERSION`. This will allow testing against multiple Drupal versions automatically.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (10)</summary><blockquote>

<details>
<summary>tools/sdc/.gitignore (1)</summary><blockquote>

`1-1`: **Specify the build directory explicitly**  
To ensure that the `build` directory is ignored (and not a file named `build`), append a slash:
```diff
- build
+ build/
```

</blockquote></details>
<details>
<summary>tools/sdc/ct_dev/ct_dev.module (1)</summary><blockquote>

`11-20`: **Remove unused severity constants**  
The variables `$rule_name_allow`, `$rule_name_error`, `$rule_name_warning`, and `$rule_name_notice` are declared but never used. For clarity and maintainability, remove them or document their purpose if they’ll be used in future rule alterations.

</blockquote></details>
<details>
<summary>tools/sdc/README.md (3)</summary><blockquote>

`7-8`: **Grammar: add missing article "the"**

Current sentence lacks the article before the module name. It should read:

"SDC components are validated using the `sdc_devel` module running on a vanilla Drupal site."

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~7-~7: You might be missing the article “the” here.
Context: ...nt.  SDC components are validated using `sdc_devel` module running on a vanilla D...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

</details>

</details>

---

`12-12`: **Grammar: add missing comma**

For clarity, add a comma after "SDC components":

```diff
- To build and run a local Drupal site with SDC components run the following commands:
+ To build and run a local Drupal site with SDC components, run the following commands:
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[uncategorized] ~12-~12: A comma might be missing here.
Context: ...ld and run a local Drupal site with SDC components run the following commands: ```bash ./b...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

</details>

</details>

---

`18-18`: **Add language identifier to fenced code block**

Specify the language (e.g., `bash`) for syntax highlighting:

```diff
- ```
+ ```bash
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

18-18: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)

</details>

</details>

</blockquote></details>
<details>
<summary>tools/sdc/build.sh (5)</summary><blockquote>

`14-15`: **Enable `pipefail` for robust error handling**

Consider using `set -euo pipefail` instead of just `-eu` to ensure that errors in pipelines are caught:

```diff
- set -eu
+[ -n "${DEBUG:-}" ] && set -x
+set -euo pipefail
```

---

`66-71`: **Remove redundant `DRUPAL_VERSION` check**

Since `DRUPAL_VERSION` is initialized with a default (`"${DRUPAL_VERSION:-11}"`), the subsequent empty check can never trigger. You can safely remove this block to simplify the script.

---

`82-84`: **Remove or use the unused `sed_opts` variable**

The `sed_opts` array is defined for macOS compatibility but never used. Either apply it in your `sed` commands or remove this variable to clean up the script.

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Shellcheck (0.10.0)</summary>

[warning] 83-83: sed_opts appears unused. Verify use (or export if used externally).

(SC2034)

</details>

</details>

---

`190-192`: **Avoid global `killall -9` on `php` processes**

Using `killall -9 php` may terminate unrelated PHP processes. Consider capturing the PID of the server process and terminating it explicitly with `kill` for safer cleanup.

---

`199-203`: **Improve port check compatibility**

`netstat` may not be available in all environments. Consider falling back to `ss` or using `nc`/`curl` for an HTTP-based readiness check instead:

```bash
if ! command -v netstat >/dev/null; then
  nc -zv "${WEBSERVER_HOST}" "${WEBSERVER_PORT}" || { echo "ERROR: Server not listening"; exit 1; }
else
  netstat "${netstat_opts[@]}" | grep -q "${WEBSERVER_PORT}" || { echo "ERROR: Unable to start inbuilt PHP server"; exit 1; }
fi
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 3de1d867c293860b2dda7b8fd154c6b5a22ac486 and 96e4322eebdc9725d0e462cbf6014ddf2e9e96d0.

</details>

<details>
<summary>📒 Files selected for processing (8)</summary>

* `.github/workflows/test-drupal.yml` (2 hunks)
* `README.md` (1 hunks)
* `composer.json` (1 hunks)
* `tools/sdc/.gitignore` (1 hunks)
* `tools/sdc/README.md` (1 hunks)
* `tools/sdc/build.sh` (1 hunks)
* `tools/sdc/ct_dev/ct_dev.info.yml` (1 hunks)
* `tools/sdc/ct_dev/ct_dev.module` (1 hunks)

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>tools/sdc/README.md</summary>

[uncategorized] ~7-~7: You might be missing the article “the” here.
Context: ...nt.  SDC components are validated using `sdc_devel` module running on a vanilla D...

(AI_EN_LECTOR_MISSING_DETERMINER_THE)

---

[uncategorized] ~12-~12: A comma might be missing here.
Context: ...ld and run a local Drupal site with SDC components run the following commands: ```bash ./b...

(AI_EN_LECTOR_MISSING_PUNCTUATION_COMMA)

</details>

</details>
<details>
<summary>🪛 markdownlint-cli2 (0.17.2)</summary>

<details>
<summary>tools/sdc/README.md</summary>

18-18: Fenced code blocks should have a language specified
null

(MD040, fenced-code-language)

</details>

</details>
<details>
<summary>🪛 Shellcheck (0.10.0)</summary>

<details>
<summary>tools/sdc/build.sh</summary>

[warning] 83-83: sed_opts appears unused. Verify use (or export if used externally).

(SC2034)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (5)</summary><blockquote>

<details>
<summary>composer.json (1)</summary>

`14-17`: <details>
<summary>❓ Verification inconclusive</summary>

**Broadened `twig/twig` version constraint**  
The change from a fixed `~3.20.0` to `^3.16.0` accommodates Drupal 11.1 compatibility while allowing patch updates. Confirm that no custom Twig extensions rely on features introduced after v3.16.0.   
Run the following to verify the installed version matches the expected range:

---

🏁 Script executed:

```shell
#!/usr/bin/env bash
# Verify installed twig/twig version from composer.lock
jq -r '.packages[] | select(.name=="twig/twig") | .version' composer.lock
```

Length of output: 147

---

🏁 Script executed:

```shell
#!/usr/bin/env bash
# Locate composer.lock and verify twig/twig version
composer_lock=$(fd composer.lock | head -n1)
if [[ -z "$composer_lock" ]]; then
  echo "composer.lock not found in the repository."
  exit 1
fi

echo "Using composer.lock at: $composer_lock"
jq -r '.packages[] | select(.name=="twig/twig") | .version' "$composer_lock"
```

Length of output: 166

---



</details>

**Verify installed `twig/twig` version manually**

File: composer.json  
Lines: 14-17  

```json
    "require-dev": {
        "twig/twig": "^3.16.0",
        "vincentlanglet/twig-cs-fixer": "^3.0"
    },
```

The `composer.lock` file wasn’t found, so we couldn’t automatically verify the installed `twig/twig` version. Please run a command such as:
```bash
composer show twig/twig
```
to confirm the installed version satisfies `^3.16.0` and that no custom Twig extensions depend on features introduced after v3.16.0.

</details>
<details>
<summary>README.md (1)</summary>

`159-162`: **Link to SDC schema validation documentation**  
Adding the “Validating SDC schema” section improves discoverability of the new tooling. The relative link to `tools/sdc/README.md` is correct and requires no further changes.

</details>
<details>
<summary>tools/sdc/ct_dev/ct_dev.info.yml (1)</summary>

`1-5`: **Validate Drupal module metadata**  
The new `ct_dev.info.yml` correctly declares the module metadata and core compatibility. Consider adding a `version` key if you plan to manage releases of this module separately.

</details>
<details>
<summary>.github/workflows/test-drupal.yml (2)</summary>

`22-23`: **Upgrade checkout action to v4**

Updating to `actions/checkout@v4` ensures faster checkouts and improved features. This change looks good.

---

`46-51`: **Consolidated build step is effective**

Calling `tools/sdc/build.sh` centralizes setup and improves maintainability. This integration into CI is a positive improvement.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T03:08:20Z
user: login: richardgaunt
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T05:14:12Z
user: login: coderabbitai[bot]
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T05:14:18Z
user: login: AlexSkrypnyk
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-13T12:29:27Z
user: login: richardgaunt
body: @AlexSkrypnyk Thanks Alex I have tested and have a nit PR to fix for linux machines. I will merge this into `project/sdc` so you continue with the linting process.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-14T13:00:18Z

Review comments:
diff_hunk: @@ -12,7 +12,7 @@
         "php": ">=8.2"
     },
     "require-dev": {
-        "twig/twig": "~3.20.0",
+        "twig/twig": "~3.16.0",
path: composer.json
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

Why was the version of `twig/twig` downgraded?  Is there a compatibility issue with the newer version?
created_at: 2025-05-12T10:08:24Z
author_association: NONE
diff_hunk: @@ -12,7 +12,7 @@
         "php": ">=8.2"
     },
     "require-dev": {
-        "twig/twig": "~3.20.0",
+        "twig/twig": "~3.16.0",
path: composer.json
user: login: AlexSkrypnyk
body: Drupal 11.1 is not compatible with 3.20
created_at: 2025-05-12T10:10:25Z
author_association: COLLABORATOR
diff_hunk: @@ -14,71 +14,45 @@ jobs:
   test-drupal-sdc:
     runs-on: ubuntu-latest
 
-    services:
-      mysql:
-        image: mysql:8.0
-        env:
-          MYSQL_ALLOW_EMPTY_PASSWORD: yes
-          MYSQL_DATABASE: drupal
-        ports:
-          - 3306:3306
-        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+    env:
+      DRUPAL_VERSION: 11.1
path: .github/workflows/test-drupal.yml
user: login: AlexSkrypnyk
body: this will allow use to run a matrix of Drupal versions in the future if we decide to
created_at: 2025-05-12T10:11:04Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,11 @@
+This directory contains scripts used for development.
+
+These can be used locally and in the CI environment.
+
+
+```bash
+./devtools/assemble.sh     # Assemble the codebase
+./devtools/start.sh        # Start the PHP server
+./devtools/provision.sh    # Provision the Drupal website
+./devtools/stop.sh         # Stop the PHP server
+```
path: .devtools/README.md
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix script paths in documentation.**
The code block uses `./devtools/...` but the actual directory is `.devtools`. Update to:
```diff
- ./devtools/assemble.sh     # Assemble the codebase
- ./devtools/start.sh        # Start the PHP server
- ./devtools/provision.sh    # Provision the Drupal website
- ./devtools/stop.sh         # Stop the PHP server
+ ./.devtools/assemble.sh     # Assemble the codebase
+ ./.devtools/start.sh        # Start the PHP server
+ ./.devtools/provision.sh    # Provision the Drupal website
+ ./.devtools/stop.sh         # Stop the PHP server
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
./.devtools/assemble.sh     # Assemble the codebase
./.devtools/start.sh        # Start the PHP server
./.devtools/provision.sh    # Provision the Drupal website
./.devtools/stop.sh         # Stop the PHP server
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T10:17:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -156,6 +156,26 @@ a non-zero status if any component needs updating.
 > Note: If the SDC and Twig implementations significantly diverge in the future, we will switch
 > to only updating the docblock headers rather than the entire component.
 
+#### Validating SDC schema
+
+SDC components are validated using `sdc_devel` module running on a vanilla 
+Drupal site.
+
+To build and run a local Drupal site with SDC components run the following commands:
+```bash
+./devtools/assemble.sh     # Assemble the codebase
+./devtools/start.sh        # Start the PHP server
+./devtools/provision.sh    # Provision the Drupal website
+```
path: README.md
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Fix script paths in README.md.**
The listed commands use `./devtools/...` but should target the `.devtools` directory:
```diff
- ./devtools/assemble.sh     # Assemble the codebase
- ./devtools/start.sh        # Start the PHP server
- ./devtools/provision.sh    # Provision the Drupal website
+ ./.devtools/assemble.sh     # Assemble the codebase
+ ./.devtools/start.sh        # Start the PHP server
+ ./.devtools/provision.sh    # Provision the Drupal website
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
./.devtools/assemble.sh     # Assemble the codebase
./.devtools/start.sh        # Start the PHP server
./.devtools/provision.sh    # Provision the Drupal website
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T10:17:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,35 @@
+#!/usr/bin/env bash
+##
+# Stop development environment.
+#
+# shellcheck disable=SC2015,SC2094,SC2002
+
+set -eu
+[ -n "${DEBUG:-}" ] && set -x
+
+#-------------------------------------------------------------------------------
+
+# @formatter:off
+note() { printf "       %s\n" "${1}"; }
+info() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[34m[INFO] %s\033[0m\n" "${1}" || printf "[INFO] %s\n" "${1}"; }
+pass() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[32m[ OK ] %s\033[0m\n" "${1}" || printf "[ OK ] %s\n" "${1}"; }
+fail() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[31m[FAIL] %s\033[0m\n" "${1}" || printf "[FAIL] %s\n" "${1}"; }
+# @formatter:on
+
+#-------------------------------------------------------------------------------
+
+echo "==============================="
+echo "      💻 STOP ENVIRONMENT      "
+echo "==============================="
+echo
+
+info "Stopping previously started services, if any."
+killall -9 php >/dev/null 2>&1 || true
+sleep 1
path: .devtools/stop.sh
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Avoid using `killall -9 php`.**
Force-killing all PHP processes can impact other services. Instead, capture the PHP server PID in `start.sh` (e.g., write `$!` to `.devtools/php.pid`) and terminate only that PID in `stop.sh`. For example:
```diff
+ if [ -f .devtools/php.pid ]; then
+   kill "$(cat .devtools/php.pid)" || true
+   rm .devtools/php.pid
+ else
+   echo "No PID file found; nothing to stop."
+ fi
- killall -9 php >/dev/null 2>&1 || true
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
# .devtools/stop.sh

# … previous lines …

if [ -f .devtools/php.pid ]; then
  kill "$(cat .devtools/php.pid)" || true
  rm .devtools/php.pid
else
  echo "No PID file found; nothing to stop."
fi
sleep 1

# … following lines …
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T10:17:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+##
+# Start development environment.
+#
+# shellcheck disable=SC2015,SC2094,SC2002
+
+set -eu
+[ -n "${DEBUG:-}" ] && set -x
+
+#-------------------------------------------------------------------------------
+# Variables (passed from environment; provided for reference only).
+#-------------------------------------------------------------------------------
+
+# Webserver hostname.
+WEBSERVER_HOST="${WEBSERVER_HOST:-localhost}"
+
+# Webserver port.
+WEBSERVER_PORT="${WEBSERVER_PORT:-8000}"
+
+# Webserver wait timeout.
+WEBSERVER_WAIT_TIMEOUT="${WEBSERVER_WAIT_TIMEOUT:-5}"
+
+#-------------------------------------------------------------------------------
+
+# @formatter:off
+note() { printf "       %s\n" "${1}"; }
+info() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[34m[INFO] %s\033[0m\n" "${1}" || printf "[INFO] %s\n" "${1}"; }
+pass() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[32m[ OK ] %s\033[0m\n" "${1}" || printf "[ OK ] %s\n" "${1}"; }
+fail() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[31m[FAIL] %s\033[0m\n" "${1}" || printf "[FAIL] %s\n" "${1}"; }
+# @formatter:on
+
+#-------------------------------------------------------------------------------
+
+echo "==============================="
+echo "      💻 START ENVIRONMENT     "
+echo "==============================="
+echo
+
+info "Stopping previously started services, if any."
+killall -9 php >/dev/null 2>&1 || true
+
path: .devtools/start.sh
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Avoid indiscriminate killing of PHP processes.**  
`killall -9 php` will terminate *all* PHP processes on the machine, which can disrupt unrelated services. Instead, capture the PID when you start the server (e.g., `echo $! > .devtools/php.pid`) and kill only that PID on subsequent runs.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-12T10:17:14Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+##
+# Start development environment.
+#
+# shellcheck disable=SC2015,SC2094,SC2002
+
+set -eu
+[ -n "${DEBUG:-}" ] && set -x
+
+#-------------------------------------------------------------------------------
+# Variables (passed from environment; provided for reference only).
+#-------------------------------------------------------------------------------
+
+# Webserver hostname.
+WEBSERVER_HOST="${WEBSERVER_HOST:-localhost}"
+
+# Webserver port.
+WEBSERVER_PORT="${WEBSERVER_PORT:-8000}"
+
+# Webserver wait timeout.
+WEBSERVER_WAIT_TIMEOUT="${WEBSERVER_WAIT_TIMEOUT:-5}"
+
+#-------------------------------------------------------------------------------
+
+# @formatter:off
+note() { printf "       %s\n" "${1}"; }
+info() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[34m[INFO] %s\033[0m\n" "${1}" || printf "[INFO] %s\n" "${1}"; }
+pass() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[32m[ OK ] %s\033[0m\n" "${1}" || printf "[ OK ] %s\n" "${1}"; }
+fail() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[31m[FAIL] %s\033[0m\n" "${1}" || printf "[FAIL] %s\n" "${1}"; }
+# @formatter:on
+
+#-------------------------------------------------------------------------------
+
+echo "==============================="
+echo "      💻 START ENVIRONMENT     "
+echo "==============================="
+echo
+
+info "Stopping previously started services, if any."
+killall -9 php >/dev/null 2>&1 || true
+
+info "Starting the PHP webserver."
+nohup php -S "${WEBSERVER_HOST}:${WEBSERVER_PORT}" -t "$(pwd)/build/web" "$(pwd)/build/web/.ht.router.php" >/tmp/php.log 2>&1 &
+
path: .devtools/start.sh
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Capture server PID and parameterize logs**  
After launching the server with `nohup … &`, record `$!` into a PID file and write logs to a project-scoped path (e.g. `.devtools/php.log`) rather than `/tmp/php.log`. This avoids conflicts and simplifies targeted shutdown.

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T00:51:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,173 @@
+#!/usr/bin/env bash
+##
+# Assemble a codebase using project code and all required dependencies.
+#
+# Allows to use the latest Drupal core as well as specified versions (for
+# testing backward compatibility).
+#
+# - Retrieves the scaffold from drupal-composer/drupal-project or custom scaffold.
+# - Builds Drupal site codebase with the current extension and it's dependencies.
+# - Adds development dependencies.
+# - Installs composer dependencies.
+#
+# This script will re-build the codebase from scratch every time it runs.
+
+# shellcheck disable=SC2015,SC2094,SC2002
+
+set -eu
+[ -n "${DEBUG:-}" ] && set -x
+
+#-------------------------------------------------------------------------------
+# Variables (passed from environment; provided for reference only).
+#-------------------------------------------------------------------------------
+
+# Drupal core version to use.
+DRUPAL_VERSION="${DRUPAL_VERSION:-11}"
+
+# Commit SHA of the drupal-project to install custom core version. If not
+# provided - will be calculated from `$DRUPAL_VERSION` above.
+DRUPAL_PROJECT_SHA="${DRUPAL_PROJECT_SHA:-}"
+
+# Repository for "drupal-composer/drupal-project" project.
+# May be overwritten to use forked repos that may have not been accepted
+# yet (i.e., when major Drupal version is about to be released).
+DRUPAL_PROJECT_REPO="${DRUPAL_PROJECT_REPO:-https://github.com/drupal-composer/drupal-project.git}"
+
+#-------------------------------------------------------------------------------
+
+# @formatter:off
+note() { printf "       %s\n" "${1}"; }
+info() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[34m[INFO] %s\033[0m\n" "${1}" || printf "[INFO] %s\n" "${1}"; }
+pass() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[32m[ OK ] %s\033[0m\n" "${1}" || printf "[ OK ] %s\n" "${1}"; }
+fail() { [ "${TERM:-}" != "dumb" ] && tput colors >/dev/null 2>&1 && printf "\033[31m[FAIL] %s\033[0m\n" "${1}" || printf "[FAIL] %s\n" "${1}"; }
+# @formatter:on
+
+#-------------------------------------------------------------------------------
+
+echo "==============================="
+echo "         🏗️ ASSEMBLE           "
+echo "==============================="
+echo
+
+# Validate required variables.
+if [ -z "${DRUPAL_VERSION}" ]; then
+  fail "ERROR: DRUPAL_VERSION is not set."
+  exit 1
+fi
+
+if [ -z "${DRUPAL_PROJECT_REPO}" ]; then
+  fail "ERROR: DRUPAL_PROJECT_REPO is not set."
+  exit 1
+fi
+
+# Make sure Composer doesn't run out of memory.
+export COMPOSER_MEMORY_LIMIT=-1
+
+info "Validate tools."
+! command -v git >/dev/null && fail "ERROR: Git is required for this script to run." && exit 1
+! command -v php >/dev/null && fail "ERROR: PHP is required for this script to run." && exit 1
+! command -v composer >/dev/null && fail "ERROR: Composer (https://getcomposer.org/) is required for this script to run." && exit 1
+pass "Tools are valid."
+
+# Set the sed options based on the OS.
+sed_opts=(-i) && [ "$(uname)" == "Darwin" ] && sed_opts=(-i '')
+
+extension="civictheme_sdc"
+
path: .devtools/assemble.sh
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Remove or use the `extension` variable**  
`extension="civictheme_sdc"` is never referenced later. Either remove this unused variable or use it when naming directories and files (e.g., `mkdir -p build/web/themes/custom/${extension}`).

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Shellcheck (0.10.0)</summary>

[warning] 75-75: extension appears unused. Verify use (or export if used externally).

(SC2034)

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T00:51:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,71 +14,40 @@ jobs:
   test-drupal-sdc:
     runs-on: ubuntu-latest
 
-    services:
-      mysql:
-        image: mysql:8.0
-        env:
-          MYSQL_ALLOW_EMPTY_PASSWORD: yes
-          MYSQL_DATABASE: drupal
-        ports:
-          - 3306:3306
-        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+    env:
+      DRUPAL_VERSION: 11.1
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
-
-      - name: Setup PHP
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: '8.3'
-          extensions: mbstring, xml, ctype, iconv, mysql, curl, intl, gd, zip
-          tools: composer:v2
+        uses: actions/checkout@v4
 
       - name: Cache Composer dependencies
-        uses: actions/cache@v3
-        with:
-          path: ~/.composer/cache
-          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
-          restore-keys: |
-            ${{ runner.os }}-composer-
-
-      - name: Setup Drupal project
-        uses: bluehorndigital/setup-drupal@v1.1.0
+        uses: actions/cache@v4
         with:
-          path: drupal
-          version: '^11.1'
-          dependencies: 'drush/drush'
+          path: /tmp/composer-cache
+          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
 
-      - name: Add Drupal dependencies
-        run: composer require drupal/sdc_devel
-        working-directory: drupal
-
-      - name: Install Drupal site
-        run: vendor/bin/drush site-install minimal --db-url=mysql://root:@127.0.0.1:3306/drupal --account-name=admin --account-pass=admin --site-name="Drupal 11" -y
-        working-directory: drupal
-
-      - name: Create custom theme
+      - name: Upgrade sqlite3
         run: |
-          mkdir -p web/themes/custom/civictheme_sdc/components
-          cat << EOF > web/themes/custom/civictheme_sdc/civictheme_sdc.info.yml
-          name: CivicTheme SDC
-          type: theme
-          description: 'A theme for testing SDC components'
-          base theme: stark
-          core_version_requirement: ^11
-          EOF
-        working-directory: drupal
+          wget https://www.sqlite.org/2024/sqlite-autoconf-3450300.tar.gz -O /tmp/sqlite.tar.gz
+          tar -xzf /tmp/sqlite.tar.gz -C /tmp
+          cd /tmp/sqlite-autoconf-3450300
+          ./configure CFLAGS="-DSQLITE_ENABLE_COLUMN_METADATA=1" --prefix=/usr/local
+          make && sudo make install
+          sudo ldconfig
 
-      - name: Copy SDC components into custom theme
-        run: cp -r ${{ github.workspace }}/packages/sdc/components/* drupal/web/themes/custom/civictheme_sdc/components/
+      - name: Setup PHP
+        uses: shivammathur/setup-php@v2
+        with:
+          php-version: 8.3
+          extensions: gd, sqlite, pdo_sqlite
+          coverage: pcov
 
-      - name: Enable theme and modules
-        run: |
-          vendor/bin/drush en sdc_devel -y
-          vendor/bin/drush theme:enable civictheme_sdc
-          vendor/bin/drush config:set system.theme default civictheme_sdc -y
-        working-directory: drupal
+      - name: Build the Drupal site
+        run: ./build.sh
path: .github/workflows/test-drupal.yml
user: login: AlexSkrypnyk
body: single command to build the site
created_at: 2025-05-13T02:57:18Z
author_association: COLLABORATOR
diff_hunk: @@ -12,7 +12,7 @@
         "php": ">=8.2"
     },
     "require-dev": {
-        "twig/twig": "~3.20.0",
+        "twig/twig": "^3.16.0",
path: composer.json
user: login: AlexSkrypnyk
body: 3.16 is the lowest version in Drupal 11.0
created_at: 2025-05-13T02:57:48Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,20 @@
+<?php
+
+/**
+ * @file
+ * CivicTheme Development module.
+ */
+
+/**
+ * Implements hook_twig_validator_rule_info_alter().
+ */
+function ct_dev_twig_validator_rule_info_alter(array &$info) {
+  // @see https://www.drupal.org/project/sdc_devel/issues/3517321
+  $rule_name_ignore = -1;
+  $rule_name_allow = 0;
+  $rule_name_error = 3;
+  $rule_name_warning = 4;
+  $rule_name_notice = 5;
+
+  $info['filter']['rule_on_name'][$rule_name_ignore][] = 'raw';
path: tools/sdc/ct_dev/ct_dev.module
user: login: AlexSkrypnyk
body: a rule to allow `raw` filter.
created_at: 2025-05-13T02:58:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,33 @@
+# SDC tools
+
+This directory contains scripts used for SDC development.
+
+These can be used locally and in the CI environment.
+
+SDC components are validated using `sdc_devel` module running on a vanilla
+Drupal site.
+
+Run the commands from the `tools/sdc-tools` directory.
path: tools/sdc/README.md
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Path mismatch: correct the directory name**

The README references `tools/sdc-tools`, but the scripts reside in `tools/sdc`. Update the path to:

```
Run the commands from the `tools/sdc` directory.
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-05-13T03:08:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,71 +14,40 @@ jobs:
   test-drupal-sdc:
     runs-on: ubuntu-latest
 
-    services:
-      mysql:
-        image: mysql:8.0
-        env:
-          MYSQL_ALLOW_EMPTY_PASSWORD: yes
-          MYSQL_DATABASE: drupal
-        ports:
-          - 3306:3306
-        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+    env:
+      DRUPAL_VERSION: 11.1
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
-
-      - name: Setup PHP
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: '8.3'
-          extensions: mbstring, xml, ctype, iconv, mysql, curl, intl, gd, zip
-          tools: composer:v2
+        uses: actions/checkout@v4
 
       - name: Cache Composer dependencies
-        uses: actions/cache@v3
-        with:
-          path: ~/.composer/cache
-          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
-          restore-keys: |
-            ${{ runner.os }}-composer-
-
-      - name: Setup Drupal project
-        uses: bluehorndigital/setup-drupal@v1.1.0
+        uses: actions/cache@v4
         with:
-          path: drupal
-          version: '^11.1'
-          dependencies: 'drush/drush'
+          path: /tmp/composer-cache
+          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
 
path: .github/workflows/test-drupal.yml
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Critical: Cache is misconfigured, Composer won't use it**

You're caching `/tmp/composer-cache`, but Composer uses its default cache directory (e.g., `~/.composer/cache`). Either set `COMPOSER_CACHE_DIR` to `/tmp/composer-cache` in your steps or change the cache `path` to match Composer's actual cache location. Otherwise, the cache step has no effect.

Example diff:

```diff
-      - name: Cache Composer dependencies
-        uses: actions/cache@v4
-        with:
-          path: /tmp/composer-cache
+      - name: Cache Composer dependencies
+        uses: actions/cache@v4
+        with:
+          path: ~/.composer/cache
+          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
```



> Committable suggestion skipped: line range outside the PR's diff.

<!-- This is an auto-generated reply by CodeRabbit -->
created_at: 2025-05-13T03:08:19Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,71 +14,40 @@ jobs:
   test-drupal-sdc:
     runs-on: ubuntu-latest
 
-    services:
-      mysql:
-        image: mysql:8.0
-        env:
-          MYSQL_ALLOW_EMPTY_PASSWORD: yes
-          MYSQL_DATABASE: drupal
-        ports:
-          - 3306:3306
-        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+    env:
+      DRUPAL_VERSION: 11.1
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
-
-      - name: Setup PHP
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: '8.3'
-          extensions: mbstring, xml, ctype, iconv, mysql, curl, intl, gd, zip
-          tools: composer:v2
+        uses: actions/checkout@v4
 
       - name: Cache Composer dependencies
-        uses: actions/cache@v3
-        with:
-          path: ~/.composer/cache
-          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
-          restore-keys: |
-            ${{ runner.os }}-composer-
-
-      - name: Setup Drupal project
-        uses: bluehorndigital/setup-drupal@v1.1.0
+        uses: actions/cache@v4
         with:
-          path: drupal
-          version: '^11.1'
-          dependencies: 'drush/drush'
+          path: /tmp/composer-cache
+          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
 
path: .github/workflows/test-drupal.yml
user: login: richardgaunt
body: @AlexSkrypnyk This looks reasonable to me
created_at: 2025-05-13T05:14:12Z
author_association: COLLABORATOR
diff_hunk: @@ -14,71 +14,40 @@ jobs:
   test-drupal-sdc:
     runs-on: ubuntu-latest
 
-    services:
-      mysql:
-        image: mysql:8.0
-        env:
-          MYSQL_ALLOW_EMPTY_PASSWORD: yes
-          MYSQL_DATABASE: drupal
-        ports:
-          - 3306:3306
-        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+    env:
+      DRUPAL_VERSION: 11.1
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
-
-      - name: Setup PHP
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: '8.3'
-          extensions: mbstring, xml, ctype, iconv, mysql, curl, intl, gd, zip
-          tools: composer:v2
+        uses: actions/checkout@v4
 
       - name: Cache Composer dependencies
-        uses: actions/cache@v3
-        with:
-          path: ~/.composer/cache
-          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
-          restore-keys: |
-            ${{ runner.os }}-composer-
-
-      - name: Setup Drupal project
-        uses: bluehorndigital/setup-drupal@v1.1.0
+        uses: actions/cache@v4
         with:
-          path: drupal
-          version: '^11.1'
-          dependencies: 'drush/drush'
+          path: /tmp/composer-cache
+          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
 
path: .github/workflows/test-drupal.yml
user: login: coderabbitai[bot]
body: > Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

<!-- This is an auto-generated mute comment by CodeRabbit -->
created_at: 2025-05-13T05:14:18Z
author_association: CONTRIBUTOR
diff_hunk: @@ -14,71 +14,40 @@ jobs:
   test-drupal-sdc:
     runs-on: ubuntu-latest
 
-    services:
-      mysql:
-        image: mysql:8.0
-        env:
-          MYSQL_ALLOW_EMPTY_PASSWORD: yes
-          MYSQL_DATABASE: drupal
-        ports:
-          - 3306:3306
-        options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
+    env:
+      DRUPAL_VERSION: 11.1
 
     steps:
       - name: Checkout code
-        uses: actions/checkout@v3
-
-      - name: Setup PHP
-        uses: shivammathur/setup-php@v2
-        with:
-          php-version: '8.3'
-          extensions: mbstring, xml, ctype, iconv, mysql, curl, intl, gd, zip
-          tools: composer:v2
+        uses: actions/checkout@v4
 
       - name: Cache Composer dependencies
-        uses: actions/cache@v3
-        with:
-          path: ~/.composer/cache
-          key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
-          restore-keys: |
-            ${{ runner.os }}-composer-
-
-      - name: Setup Drupal project
-        uses: bluehorndigital/setup-drupal@v1.1.0
+        uses: actions/cache@v4
         with:
-          path: drupal
-          version: '^11.1'
-          dependencies: 'drush/drush'
+          path: /tmp/composer-cache
+          key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }}
 
path: .github/workflows/test-drupal.yml
user: login: AlexSkrypnyk
body: this is outdated. it is already like this
created_at: 2025-05-13T12:29:27Z
author_association: COLLABORATOR

PR comments:
user: login: github-actions[bot]
created_at: 2025-05-12T10:10:42Z
body: 📷 View [Visual Regression](https://6821c997c8b5489df9e90200--civictheme-uikit-visual-regression.netlify.app)

**reg-suit detected visual differences.**

:red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: 


:large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: 

<details>
                <summary>What do the circles mean?</summary>
                The number of circles represent the number of changed images. <br />
                :red_circle: : Changed items,
                :white_circle: : New items,
                :black_circle: : Deleted items, and
                :large_blue_circle: : Passing items
                <br />
             </details><br />
<details>
          <summary>How can I change the check status?</summary>
          If reviewers approve this PR, the reg context status will be green automatically.
          <br />
       </details><br />
author_association: NONE
user: login: codecov[bot]
created_at: 2025-05-12T10:10:48Z
body: ## [Codecov](https://app.codecov.io/gh/civictheme/uikit/pull/588?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=civictheme) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 12.64%. Comparing base [(`3de1d86`)](https://app.codecov.io/gh/civictheme/uikit/commit/3de1d867c293860b2dda7b8fd154c6b5a22ac486?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=civictheme) to head [(`d964146`)](https://app.codecov.io/gh/civictheme/uikit/commit/d964146cd6b966debeb748305538c37c969d2384?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=civictheme).
> Report is 2 commits behind head on project/sdc.

<details><summary>Additional details and impacted files</summary>


```diff
@@             Coverage Diff              @@
##           project/sdc     #588   +/-   ##
============================================
  Coverage        12.64%   12.64%           
============================================
  Files               72       72           
  Lines             2262     2262           
  Branches           512      512           
============================================
  Hits               286      286           
  Misses            1596     1596           
  Partials           380      380           
```

| [Flag](https://app.codecov.io/gh/civictheme/uikit/pull/588/flags?src=pr&el=flags&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=civictheme) | Coverage Δ | |
|---|---|---|
| [sdc](https://app.codecov.io/gh/civictheme/uikit/pull/588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=civictheme) | `12.64% <ø> (ø)` | |
| [twig](https://app.codecov.io/gh/civictheme/uikit/pull/588/flags?src=pr&el=flag&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=civictheme) | `12.64% <ø> (ø)` | |

Flags with carried forward coverage won't be shown. [Click here](https://docs.codecov.io/docs/carryforward-flags?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=civictheme#carryforward-flags-in-the-pull-request-comment) to find out more.

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/civictheme/uikit/pull/588?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=civictheme).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=civictheme).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
- :package: [JS Bundle Analysis](https://docs.codecov.com/docs/javascript-bundle-analysis): Save yourself from yourself by tracking and limiting bundle sizes in JS merges.
</details>
author_association: NONE
user: login: AlexSkrypnyk
created_at: 2025-05-12T10:12:02Z
body: @coderabbitai full review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-05-12T10:12:07Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Full review triggered.

</details>
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-13T00:45:29Z
body: 📷 View [Visual Regression](https://682296993f77d65fce7825cd--civictheme-uikit-visual-regression.netlify.app)

**reg-suit detected visual differences.**

:red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: 


:large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: 

<details>
                <summary>What do the circles mean?</summary>
                The number of circles represent the number of changed images. <br />
                :red_circle: : Changed items,
                :white_circle: : New items,
                :black_circle: : Deleted items, and
                :large_blue_circle: : Passing items
                <br />
             </details><br />
<details>
          <summary>How can I change the check status?</summary>
          If reviewers approve this PR, the reg context status will be green automatically.
          <br />
       </details><br />
author_association: NONE
user: login: AlexSkrypnyk
created_at: 2025-05-13T00:45:50Z
body: @coderabbitai full review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-05-13T00:45:54Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Full review triggered.

</details>
author_association: CONTRIBUTOR
user: login: AlexSkrypnyk
created_at: 2025-05-13T00:46:29Z
body: @richardgaunt 
I've added a module to handle `sdc_devel` exceptions.
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-05-13T00:48:16Z
body: 📷 View [Visual Regression](https://6822973295a3fb5944986662--civictheme-uikit-visual-regression.netlify.app)

**reg-suit detected visual differences.**

:red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: 


:large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: 

<details>
                <summary>What do the circles mean?</summary>
                The number of circles represent the number of changed images. <br />
                :red_circle: : Changed items,
                :white_circle: : New items,
                :black_circle: : Deleted items, and
                :large_blue_circle: : Passing items
                <br />
             </details><br />
<details>
          <summary>How can I change the check status?</summary>
          If reviewers approve this PR, the reg context status will be green automatically.
          <br />
       </details><br />
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T02:52:04Z
body: 📷 View [Visual Regression](https://6822b520f5de6e814e76fa68--civictheme-uikit-visual-regression.netlify.app)

**reg-suit detected visual differences.**

:red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: 


:large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: 

<details>
                <summary>What do the circles mean?</summary>
                The number of circles represent the number of changed images. <br />
                :red_circle: : Changed items,
                :white_circle: : New items,
                :black_circle: : Deleted items, and
                :large_blue_circle: : Passing items
                <br />
             </details><br />
<details>
          <summary>How can I change the check status?</summary>
          If reviewers approve this PR, the reg context status will be green automatically.
          <br />
       </details><br />
author_association: NONE
user: login: coderabbitai[bot]
created_at: 2025-05-13T02:55:53Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

The updates introduce a new build system for SDC development with Drupal, replacing direct Drupal setup in CI with a script-driven approach using SQLite. Documentation is expanded, a custom development module is added, Composer dependency constraints are relaxed, and workflow automation is streamlined. Git now ignores build artifacts in the SDC tools directory.

## Changes

| File(s)                                                                 | Change Summary                                                                                                                             |
|-------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------|
| .github/workflows/test-drupal.yml                                       | Workflow now uses a build script and SQLite, removes MySQL and direct Drupal install steps, updates caching, PHP setup, and actions.       |
| README.md                                                               | Adds instructions for validating the SDC schema, referencing new documentation in `tools/sdc/README.md`.                                   |
| composer.json                                                           | Relaxes the `twig/twig` dependency from `~3.20.0` to `^3.16.0`.                                                                            |
| tools/sdc/.gitignore                                                    | New file; instructs Git to ignore the `build` directory within `tools/sdc`.                                                                |
| tools/sdc/README.md                                                     | New documentation; explains SDC development scripts, build process, and validation procedures.                                             |
| tools/sdc/build.sh                                                      | New script; automates assembling, provisioning, starting, and stopping a Drupal environment for SDC development.                           |
| tools/sdc/ct_dev/ct_dev.info.yml                                        | New file; declares the CivicTheme Development Drupal module and its metadata.                                                              |
| tools/sdc/ct_dev/ct_dev.module                                          | New file; adds a hook to alter Twig validator rules, ignoring the `raw` filter for validation.                                             |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant Developer
    participant BuildScript (build.sh)
    participant DrupalSite
    participant PHP Webserver

    Developer->>BuildScript: Run (no args or with assemble/provision/start/stop)
    BuildScript->>BuildScript: Validate environment/tools
    BuildScript->>BuildScript: Assemble Drupal project (composer, symlinks, modules)
    BuildScript->>BuildScript: Provision Drupal (install, enable modules/themes)
    BuildScript->>PHP Webserver: Start built-in server
    PHP Webserver->>DrupalSite: Serve requests
    BuildScript->>DrupalSite: Validate SDC schema (via Drush)
    Developer->>BuildScript: Stop environment (optional)
    BuildScript->>PHP Webserver: Stop server
```

## Poem

> In the garden of code, a new script appears,  
> With build and provision, it conquers our fears.  
> No more MySQL, SQLite’s now king,  
> The SDC bunnies hop, and happily sing.  
> A module for devs, a README for lore—  
> Our Drupal dreams flourish, more than before!  
> 🐰✨

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=civictheme/uikit&utm_content=588):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: AlexSkrypnyk
created_at: 2025-05-13T02:56:57Z
body: @coderabbit full review
author_association: COLLABORATOR
user: login: coderabbitai[bot]
created_at: 2025-05-13T02:57:02Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>✅ Actions performed</summary>

Full review triggered.

</details>
author_association: CONTRIBUTOR
user: login: AlexSkrypnyk
created_at: 2025-05-13T02:59:46Z
body: @richardgaunt 
please review this PR
author_association: COLLABORATOR
user: login: github-actions[bot]
created_at: 2025-05-13T12:33:09Z
body: 📷 View [Visual Regression](https://68233c6ee697967825eafc1d--civictheme-uikit-visual-regression.netlify.app)

**reg-suit detected visual differences.**

:red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: 


:large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: 

<details>
                <summary>What do the circles mean?</summary>
                The number of circles represent the number of changed images. <br />
                :red_circle: : Changed items,
                :white_circle: : New items,
                :black_circle: : Deleted items, and
                :large_blue_circle: : Passing items
                <br />
             </details><br />
<details>
          <summary>How can I change the check status?</summary>
          If reviewers approve this PR, the reg context status will be green automatically.
          <br />
       </details><br />
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-13T12:39:23Z
body: 📷 View [Visual Regression](https://68233ddf9e6ceb415b820813--civictheme-uikit-visual-regression.netlify.app)

**reg-suit detected visual differences.**

:red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: 


:large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: 

<details>
                <summary>What do the circles mean?</summary>
                The number of circles represent the number of changed images. <br />
                :red_circle: : Changed items,
                :white_circle: : New items,
                :black_circle: : Deleted items, and
                :large_blue_circle: : Passing items
                <br />
             </details><br />
<details>
          <summary>How can I change the check status?</summary>
          If reviewers approve this PR, the reg context status will be green automatically.
          <br />
       </details><br />
author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-14T12:54:44Z
body: 📷 View [Visual Regression](https://6824930322f113e18d615c09--civictheme-uikit-visual-regression.netlify.app)

**reg-suit detected visual differences.**

:red_circle: :red_circle: :red_circle: :red_circle: :red_circle: :red_circle: 


:large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: :large_blue_circle: 

<details>
                <summary>What do the circles mean?</summary>
                The number of circles represent the number of changed images. <br />
                :red_circle: : Changed items,
                :white_circle: : New items,
                :black_circle: : Deleted items, and
                :large_blue_circle: : Passing items
                <br />
             </details><br />
<details>
          <summary>How can I change the check status?</summary>
          If reviewers approve this PR, the reg context status will be green automatically.
          <br />
       </details><br />
author_association: NONE