Skip to content

Developer Setup

How to get a working local setup for contributing to the Groups Initiative — both the Drupal module (developed in a DDEV site) and the documentation site (Astro/Starlight).

  • Git
  • For the Drupal module: DDEV and Docker, plus a Drupal 11 DDEV site (docroot web). The module requires PHP 8.3+; the reference site here runs PHP 8.4.
  • For the docs site: Node.js 24 (the current LTS — Astro 7 requires >=22.12, and CI, the repo (.nvmrc), and the local toolchain all standardize on 24) and npm.
Terminal window
git clone git@git.drupal.org:project/groupsdrupalorg.git
cd groupsdrupalorg

SSH host gotcha — The Git SSH host is git.drupal.org, not git.drupalcode.org (that’s the web UI host). Cloning or pushing with the wrong host fails as a connection timeout on port 22 — it looks like a firewall problem, not an authentication error. Drupal.org also requires SSH for pushes; HTTPS pushes are rejected.

Contributions are tied to an issue and use Drupal’s shared issue-fork workflow rather than personal forks:

  • Each issue gets a fork named issue/groupsdrupalorg-<issue-number>.
  • Branch names are <issue-number>-short-slug, branched off 1.x.
  • A maintainer merges the resulting merge request.

For commit-message format, squashing, and merge-request conventions, see Contribution Standards.

See the Drupal docs on GitLab issues for drupal.org projects for the full workflow.

Terminal window
cd docs
npm install

This is a module-only repository — you develop it inside your own Drupal 11 site. The recommended loop mounts your module working copy into a DDEV site so you edit it in place from its own clone (no copy, no composer require).

In your Drupal 11 DDEV site (docroot web), add a Docker Compose override that bind-mounts your module clone into web/modules/custom/groupsdrupalorg, then restart:

# .ddev/docker-compose.groupsdrupalorg.yaml (in your DDEV SITE, not the module repo)
services:
web:
volumes:
- "/absolute/path/to/your/groupsdrupalorg:/var/www/html/web/modules/custom/groupsdrupalorg"
Terminal window
ddev restart
  • Machine name is groupsdrupalorg. The mount target directory must be named groupsdrupalorg (matching the module machine name / project slug), or Drupal won’t find it.
  • A bind-mount — not a copy, not a host symlink. A host symlink into the site dangles inside the container (which only sees the project directory); the Compose bind-mount is what makes the working copy visible in-container. The host’s web/modules/custom/ stays empty — the mount exists inside the container.
  • Don’t composer require drupal/groupsdrupalorg in the site while the mount is active. It lives in modules/custom/ so Composer’s installer-paths (which manage modules/contrib/) don’t fight the working copy.
Terminal window
ddev drush pm:install groupsdrupalorg # install + enable
ddev drush pm:list --status=enabled | grep groupsdrupalorg
ddev drush pm:uninstall groupsdrupalorg # when you're done

The module ships phpcs.xml.dist and phpstan.neon (both scoped to the module) and a PHPUnit suite under tests/, run through the DDEV site (which supplies the Drupal test tooling — drupal/core-dev, drupal/coder, mglaman/phpstan-drupal; a standard drupal.org dev site has these, else add them to the site’s require-dev).

One-time setup. The module ships DDEV commands (check:all / check:phpcs / check:phpstan / check:phpunit). Register them in your DDEV site once, from the site root, pointing at your module clone. The bind-mount is container-only — the site’s own web/modules/custom/ is empty on the host — so copy (or symlink) from where you cloned the module, not via a relative path inside the site:

Terminal window
mkdir -p .ddev/commands/web
# copy (simplest):
cp /path/to/your/groupsdrupalorg/.ddev/commands/web/check-* .ddev/commands/web/
# …or symlink to your clone so they auto-update as the module changes:
# ln -sf /path/to/your/groupsdrupalorg/.ddev/commands/web/check-* .ddev/commands/web/

ddev picks them up on the next run (no restart needed) — verify with ddev -h.

Then run any of them from the site:

Terminal window
ddev check:all # PHPCS + PHPStan + PHPUnit — the same checks CI enforces
ddev check:phpcs # coding standards only (Drupal, DrupalPractice)
ddev check:phpstan
ddev check:phpunit # e.g. ddev check:phpunit --filter SettingsPageTest

(The colon is a naming convention — DDEV takes each command name from its ## Usage: line, so the shipped filenames stay colon-free for Windows/WSL2.)

Without that setup, the raw equivalents (what the commands wrap) are:

Terminal window
ddev exec "cd web/modules/custom/groupsdrupalorg && /var/www/html/vendor/bin/phpcs"
ddev exec "cd web/modules/custom/groupsdrupalorg && /var/www/html/vendor/bin/phpstan analyse -c phpstan.neon"
ddev exec "SIMPLETEST_DB=mysql://db:db@db/db vendor/bin/phpunit -c web/core/phpunit.xml.dist web/modules/custom/groupsdrupalorg/tests"

The same PHPCS / PHPStan / PHPUnit checks run in CI on every merge request (the authoritative gate) and are wired into the Git hooks below.

Browser tests (Playwright) are a separate, later addition — tracked in issue #3578772.

Enable the Git hooks with lefthook (one-time)

Section titled “Enable the Git hooks with lefthook (one-time)”

The repo uses lefthook as its single Git-hook manager. The hooks validate docs content before you commit or push. Install lefthook once, then enable the hooks per clone:

Terminal window
# 1. Install the lefthook binary (pick one):
brew install lefthook # macOS/Homebrew
# or: go install github.com/evilmartians/lefthook@latest
# or run it via npx (no install): use `npx lefthook` in place of `lefthook` below
# 2. Release the legacy hooks path (only needed if you set it previously), then install:
git config --unset core.hooksPath # ignore "not set" if it was never configured
lefthook install

After that:

  • pre-commit runs a fast, staged-only Keystatic editability check over any staged .md / .mdoc docs content (check:keystatic --staged). It no-ops instantly on commits that touch no docs content.
  • pre-push runs the full docs build — npm run build (astro check + check:keystatic + astro build + internal link validation + content gates) — and blocks the push if it fails, the same checks CI runs. Pushes that don’t touch docs/ skip it.

Notes:

  • The docs checks require the docs dependencies (npm install, above).
  • To bypass the hooks for a single commit or push: git commit --no-verify / git push --no-verify. Local hooks are a convenience; CI is the authoritative gate.
  • The same lefthook.yml also runs the module PHP checks — phpcs at pre-commit, PHPStan + PHPUnit at pre-push — on staged/pushed module files. These need the module’s dev dependencies present (composer install in the module repo); the hook skips them gracefully when the tools aren’t installed.

The repo ships a test-first coding pipeline for AI coding agents, with a per-story review-rigor dial — base / second-opinion / panel. The base rung needs no setup. For second-opinion or panel, bind your two models once with the setup script — the Author (your own coding model) and the Critic (the independent outside reviewer, reached through LiteLLM, so it can be any provider, including a fully local one):

Terminal window
.claude/scripts/setup-pipeline.sh # prompts for your models + key; writes .env (git-ignored)

.env is git-ignored and never committed. See Running the Pipeline for the full walkthrough — the providers, local models, reasoning-model tuning, and how an agent drives each rung.

See Editing These Docs for how to run the live-reloading dev server and add or edit pages.

From the docs/ directory:

Terminal window
npm run build # full build: astro check + check:keystatic + link validation (what pre-push and CI run)
npm run check # astro check only (types + frontmatter), no build
npm run check:keystatic # Keystatic editability gate only (Layer 1 round-trip + Layer 2 static)

Internal links and heading anchors are validated here and block the build. External links are checked separately in CI (non-blocking) and reported on the Link Health Report page.

A page 500s in dev (AstroUserError), especially after a config change

Section titled “A page 500s in dev (AstroUserError), especially after a config change”

The dev server hot-reloads content edits (.md/.mdx) fine, but changes to astro.config.mjs — the sidebar, logo/favicon, or a component override — and newly added dependencies are not always picked up by a running server. The module graph can go stale and start returning AstroUserError 500s on some routes (often a page that didn’t exist when the server started), even though npm run build passes.

Fix: restart the dev server. Stop it (Ctrl-C) and run npm run dev again.

If it still errors after a clean restart, clear Astro’s caches and restart:

Terminal window
rm -rf .astro node_modules/.vite
npm run dev

Rule of thumb: after editing astro.config.mjs, restart npm run dev rather than relying on hot reload. If a build passes but dev shows an error, it’s almost always a stale server, not the page.